commit f1ae1267c330f7ec463a58fc6c7b0e2ea6f44a16 parent 6e35ddf12f28d925b261648a60142515f8059e94 Author: JohnMcPMS <johnmcp@microsoft.com> Date: Tue, 21 May 2024 09:22:25 -0700 Move paths code to shared (#4484) ## Change Move the filesystem and path related code to the shared lib project as a foundation for a future change. Diffstat:
14 files changed, 565 insertions(+), 530 deletions(-)
diff --git a/src/AppInstallerCLITests/Runtime.cpp b/src/AppInstallerCLITests/Runtime.cpp @@ -7,6 +7,7 @@ #include <winget/Filesystem.h> using namespace AppInstaller; +using namespace AppInstaller::Filesystem; using namespace AppInstaller::Runtime; using namespace TestCommon; diff --git a/src/AppInstallerCLITests/TestHooks.h b/src/AppInstallerCLITests/TestHooks.h @@ -28,7 +28,7 @@ namespace AppInstaller namespace Runtime { void TestHook_SetPathOverride(PathName target, const std::filesystem::path& path); - void TestHook_SetPathOverride(PathName target, const PathDetails& details); + void TestHook_SetPathOverride(PathName target, const Filesystem::PathDetails& details); void TestHook_ClearPathOverrides(); } diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -441,7 +441,6 @@ <ExcludedFromBuild Condition="'$(Configuration)'=='Fuzzing'">true</ExcludedFromBuild> </ClInclude> <ClInclude Include="Public\winget\NameNormalization.h" /> - <ClInclude Include="Public\winget\Filesystem.h" /> <ClInclude Include="Public\winget\NetworkSettings.h" /> <ClInclude Include="Public\winget\PackageDependenciesValidationUtil.h" /> <ClInclude Include="Public\winget\PackageVersionDataManifest.h" /> @@ -468,7 +467,6 @@ <ClCompile Include="DependenciesGraph.cpp" /> <ClCompile Include="DODownloader.cpp" /> <ClCompile Include="FileCache.cpp" /> - <ClCompile Include="Filesystem.cpp" /> <ClCompile Include="FolderFileWatcher.cpp" /> <ClCompile Include="Deployment.cpp" /> <ClCompile Include="Downloader.cpp" /> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -144,9 +144,6 @@ <ClInclude Include="Public\winget\Debugging.h"> <Filter>Public\winget</Filter> </ClInclude> - <ClInclude Include="Public\winget\Filesystem.h"> - <Filter>Public\winget</Filter> - </ClInclude> <ClInclude Include="Public\winget\PortableARPEntry.h"> <Filter>Public\winget</Filter> </ClInclude> @@ -311,9 +308,6 @@ <ClCompile Include="Debugging.cpp"> <Filter>Source Files</Filter> </ClCompile> - <ClCompile Include="Filesystem.cpp"> - <Filter>Source Files</Filter> - </ClCompile> <ClCompile Include="PortableARPEntry.cpp"> <Filter>Source Files</Filter> </ClCompile> diff --git a/src/AppInstallerCommonCore/Filesystem.cpp b/src/AppInstallerCommonCore/Filesystem.cpp @@ -1,258 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "Public/AppInstallerStrings.h" -#include "public/winget/Filesystem.h" - -namespace AppInstaller::Filesystem -{ - using namespace std::chrono_literals; - using namespace std::string_view_literals; - - DWORD GetVolumeInformationFlagsByHandle(HANDLE anyFileHandle) - { - DWORD flags = 0; - wchar_t fileSystemName[MAX_PATH]; - THROW_LAST_ERROR_IF(!GetVolumeInformationByHandleW( - anyFileHandle, /*hFile*/ - NULL, /*lpVolumeNameBuffer*/ - 0, /*nVolumeNameSize*/ - NULL, /*lpVolumeSerialNumber*/ - NULL, /*lpMaximumComponentLength*/ - &flags, /*lpFileSystemFlags*/ - fileSystemName, /*lpFileSystemNameBuffer*/ - MAX_PATH /*nFileSystemNameSize*/)); - - // Vista and older does not report all flags, fix them up here - if (!(flags & FILE_SUPPORTS_HARD_LINKS) && !_wcsicmp(fileSystemName, L"NTFS")) - { - flags |= FILE_SUPPORTS_HARD_LINKS | FILE_SUPPORTS_EXTENDED_ATTRIBUTES | FILE_SUPPORTS_OPEN_BY_FILE_ID | FILE_SUPPORTS_USN_JOURNAL; - } - - return flags; - } - - DWORD GetVolumeInformationFlags(const std::filesystem::path& anyPath) - { - wil::unique_hfile fileHandle{ CreateFileW( - anyPath.c_str(), /*lpFileName*/ - 0, /*dwDesiredAccess*/ - FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, /*dwShareMode*/ - NULL, /*lpSecurityAttributes*/ - OPEN_EXISTING, /*dwCreationDisposition*/ - FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, /*dwFlagsAndAttributes*/ - NULL /*hTemplateFile*/) }; - - THROW_LAST_ERROR_IF(fileHandle.get() == INVALID_HANDLE_VALUE); - - return GetVolumeInformationFlagsByHandle(fileHandle.get()); - } - - bool SupportsNamedStreams(const std::filesystem::path& path) - { - return (GetVolumeInformationFlags(path) & FILE_NAMED_STREAMS) != 0; - } - - bool SupportsHardLinks(const std::filesystem::path& path) - { - return (GetVolumeInformationFlags(path) & FILE_SUPPORTS_HARD_LINKS) != 0; - } - - bool SupportsReparsePoints(const std::filesystem::path& path) - { - return (GetVolumeInformationFlags(path) & FILE_SUPPORTS_REPARSE_POINTS) != 0; - } - - bool PathEscapesBaseDirectory(const std::filesystem::path& target, const std::filesystem::path& base) - { - const auto& targetPath = std::filesystem::weakly_canonical(target); - const auto& basePath = std::filesystem::weakly_canonical(base); - auto [a, b] = std::mismatch(targetPath.begin(), targetPath.end(), basePath.begin(), basePath.end()); - return (b != basePath.end()); - } - - // Complicated rename algorithm due to somewhat arbitrary failures. - // 1. First, try to rename. - // 2. Then, create an empty file for the target, and attempt to rename. - // 3. Then, try repeatedly for 500ms in case it is a timing thing. - // 4. Attempt to use a hard link if available. - // 5. Copy the file if nothing else has worked so far. - void RenameFile(const std::filesystem::path& from, const std::filesystem::path& to) - { - // 1. First, try to rename. - try - { - // std::filesystem::rename() handles motw correctly if applicable. - std::filesystem::rename(from, to); - return; - } - CATCH_LOG(); - - // 2. Then, create an empty file for the target, and attempt to rename. - // This seems to fix things in certain cases, so we do it. - try - { - { - std::ofstream targetFile{ to }; - } - std::filesystem::rename(from, to); - return; - } - CATCH_LOG(); - - // 3. Then, try repeatedly for 500ms in case it is a timing thing. - for (int i = 0; i < 5; ++i) - { - try - { - std::this_thread::sleep_for(100ms); - std::filesystem::rename(from, to); - return; - } - CATCH_LOG(); - } - - // 4. Attempt to use a hard link if available. - if (SupportsHardLinks(from)) - { - try - { - // Create a hard link to the file; the installer will be left in the temp directory afterward - // but it is better to succeed the operation and leave a file around than to fail. - // First we have to remove the target file as the function will not overwrite. - std::filesystem::remove(to); - std::filesystem::create_hard_link(from, to); - return; - } - CATCH_LOG(); - } - - // 5. Copy the file if nothing else has worked so far. - // Create a copy of the file; the installer will be left in the temp directory afterward - // but it is better to succeed the operation and leave a file around than to fail. - std::filesystem::copy_file(from, to, std::filesystem::copy_options::overwrite_existing); - } - -#ifndef AICLI_DISABLE_TEST_HOOKS - static bool* s_CreateSymlinkResult_TestHook_Override = nullptr; - - void TestHook_SetCreateSymlinkResult_Override(bool* status) - { - s_CreateSymlinkResult_TestHook_Override = status; - } -#endif - - bool CreateSymlink(const std::filesystem::path& target, const std::filesystem::path& link) - { -#ifndef AICLI_DISABLE_TEST_HOOKS - if (s_CreateSymlinkResult_TestHook_Override) - { - return *s_CreateSymlinkResult_TestHook_Override; - } -#endif - try - { - std::filesystem::create_symlink(target, link); - return true; - } - catch (std::filesystem::filesystem_error& error) - { - if (error.code().value() == ERROR_PRIVILEGE_NOT_HELD) - { - return false; - } - else - { - throw; - } - } - } - - bool VerifySymlink(const std::filesystem::path& symlink, const std::filesystem::path& target) - { - const std::filesystem::path& symlinkTargetPath = std::filesystem::weakly_canonical(symlink); - return symlinkTargetPath == std::filesystem::weakly_canonical(target); - } - - void AppendExtension(std::filesystem::path& target, const std::string& value) - { - if (target.extension() != value) - { - target += value; - } - } - - bool SymlinkExists(const std::filesystem::path& symlinkPath) - { - return std::filesystem::is_symlink(std::filesystem::symlink_status(symlinkPath)); - } - - std::filesystem::path GetExpandedPath(const std::string& path) - { - std::string trimPath = path; - Utility::Trim(trimPath); - - try - { - return std::filesystem::weakly_canonical(Utility::ExpandEnvironmentVariables(Utility::ConvertToUTF16(trimPath))); - } - catch (...) - { - return Utility::ConvertToUTF16(path); - } - } - - bool ReplaceCommonPathPrefix(std::filesystem::path& source, const std::filesystem::path& prefix, std::string_view replacement) - { - auto prefixItr = prefix.begin(); - auto sourceItr = source.begin(); - - while (prefixItr != prefix.end() && sourceItr != source.end()) - { - if (!Utility::ICUCaseInsensitiveEquals(prefixItr->u8string(), sourceItr->u8string())) - { - break; - } - - ++prefixItr; - ++sourceItr; - } - - // Only replace source if we found all of prefix - if (prefixItr == prefix.end()) - { - std::filesystem::path temp{ replacement }; - - for (; sourceItr != source.end(); ++sourceItr) - { - temp /= *sourceItr; - } - - source = std::move(temp); - - return true; - } - - return false; - } - - std::filesystem::path GetKnownFolderPath(const KNOWNFOLDERID& id) - { - wil::unique_cotaskmem_string knownFolder = nullptr; - THROW_IF_FAILED(SHGetKnownFolderPath(id, KF_FLAG_NO_ALIAS | KF_FLAG_DONT_VERIFY | KF_FLAG_NO_PACKAGE_REDIRECTION, NULL, &knownFolder)); - return knownFolder.get(); - } - - bool IsSameVolume(const std::filesystem::path& path1, const std::filesystem::path& path2) - { - WCHAR volumeName1[MAX_PATH]; - WCHAR volumeName2[MAX_PATH]; - - // Note: GetVolumePathNameW will return false if the volume drive does not exist. - if (!GetVolumePathNameW(path1.c_str(), volumeName1, MAX_PATH) || !GetVolumePathNameW(path2.c_str(), volumeName2, MAX_PATH)) - { - return false; - } - return Utility::ICUCaseInsensitiveEquals(Utility::ConvertToUTF8(volumeName1), Utility::ConvertToUTF8(volumeName2)); - } -}- \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h b/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h @@ -3,6 +3,7 @@ #pragma once #include <AppInstallerVersions.h> #include <winget/LocIndependent.h> +#include <winget/Filesystem.h> #include <winget/Runtime.h> #include <filesystem> @@ -57,56 +58,15 @@ namespace AppInstaller::Runtime Max }; - // The principal that an ACE applies to. - enum class ACEPrincipal : uint32_t - { - CurrentUser, - Admins, - System, - }; - - // The permissions granted to a specific ACE. - enum class ACEPermissions : uint32_t - { - // This is not "Deny All", but rather, "Not mentioned" - None = 0x0, - Read = 0x1, - Write = 0x2, - Execute = 0x4, - ReadWrite = Read | Write, - ReadExecute = Read | Execute, - ReadWriteExecute = Read | Write | Execute, - // All means that full control will be granted - All = 0xFFFFFFFF - }; - - DEFINE_ENUM_FLAG_OPERATORS(ACEPermissions); - - // Information about a path that we use and how to set it up. - struct PathDetails - { - std::filesystem::path Path; - // Default to creating the directory with inherited ownership and permissions - bool Create = true; - std::optional<ACEPrincipal> Owner; - std::map<ACEPrincipal, ACEPermissions> ACL; - - // Shorthand for setting Owner and giving them ACEPermissions::All - void SetOwner(ACEPrincipal owner); - - // Determines if the ACL should be applied. - bool ShouldApplyACL() const; - - // Applies the ACL unconditionally. - void ApplyACL() const; - }; - // Gets the PathDetails used for the given path. // This is exposed primarily to allow for testing, GetPathTo should be preferred. - PathDetails GetPathDetailsFor(PathName path, bool forDisplay = false); + Filesystem::PathDetails GetPathDetailsFor(PathName path, bool forDisplay = false); // Gets the path to the requested location. - std::filesystem::path GetPathTo(PathName path, bool forDisplay = false); + inline std::filesystem::path GetPathTo(PathName path, bool forDisplay = false) + { + return Filesystem::GetPathTo(path, forDisplay); + } // Gets a new temp file path. std::filesystem::path GetNewTempFilePath(); diff --git a/src/AppInstallerCommonCore/Public/winget/Filesystem.h b/src/AppInstallerCommonCore/Public/winget/Filesystem.h @@ -1,49 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#pragma once -#include <filesystem> -#include <shtypes.h> - -namespace AppInstaller::Filesystem -{ - // Checks if the file system at path supports named streams/ADS - bool SupportsNamedStreams(const std::filesystem::path& path); - - // Checks if the file system at path supports hard links - bool SupportsHardLinks(const std::filesystem::path& path); - - // Checks if the file system at path support reparse points - bool SupportsReparsePoints(const std::filesystem::path& path); - - // Checks if the canonical form of the path points to a location outside of the provided base path. - bool PathEscapesBaseDirectory(const std::filesystem::path& target, const std::filesystem::path& base); - - // Renames the file to a new path. - void RenameFile(const std::filesystem::path& from, const std::filesystem::path& to); - - // Creates a symlink that points to the target path. - bool CreateSymlink(const std::filesystem::path& target, const std::filesystem::path& link); - - // Verifies that a symlink points to the target path. - bool VerifySymlink(const std::filesystem::path& symlink, const std::filesystem::path& target); - - // Appends the .exe extension to the path if not present. - void AppendExtension(std::filesystem::path& value, const std::string& extension); - - // Checks if the path is a symlink and exists. - bool SymlinkExists(const std::filesystem::path& symlinkPath); - bool CreateSymlink(const std::filesystem::path& path, const std::filesystem::path& target); - - // Get expanded file system path. - std::filesystem::path GetExpandedPath(const std::string& path); - - // If `source` begins with all of `prefix`, replace that with `replacement`. - // Returns true if replacement happened, false otherwise. - bool ReplaceCommonPathPrefix(std::filesystem::path& source, const std::filesystem::path& prefix, std::string_view replacement); - - // Gets the path of a known folder. - std::filesystem::path GetKnownFolderPath(const KNOWNFOLDERID& id); - - // Verifies that the paths are on the same volume. - bool IsSameVolume(const std::filesystem::path& path1, const std::filesystem::path& path2); -}- \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Runtime.cpp b/src/AppInstallerCommonCore/Runtime.cpp @@ -5,9 +5,9 @@ #include "Public/AppInstallerLogging.h" #include "Public/AppInstallerRuntime.h" #include "Public/AppInstallerStrings.h" -#include "Public/winget/Filesystem.h" #include "Public/winget/UserSettings.h" #include "Public/winget/Registry.h" +#include <winget/Filesystem.h> #define WINGET_DEFAULT_LOG_DIRECTORY "DiagOutputDir" @@ -184,43 +184,6 @@ namespace AppInstaller::Runtime return result; } - DWORD AccessPermissionsFrom(ACEPermissions permissions) - { - DWORD result = 0; - - if (permissions == ACEPermissions::All) - { - result |= GENERIC_ALL; - } - else - { - if (WI_IsFlagSet(permissions, ACEPermissions::Read)) - { - result |= GENERIC_READ; - } - - if (WI_IsFlagSet(permissions, ACEPermissions::Write)) - { - result |= GENERIC_WRITE | FILE_DELETE_CHILD; - } - - if (WI_IsFlagSet(permissions, ACEPermissions::Execute)) - { - result |= GENERIC_EXECUTE; - } - } - - return result; - } - - // Contains the information about an ACE entry for a given principal. - struct ACEDetails - { - ACEPrincipal Principal; - PSID SID; - TRUSTEE_TYPE TrusteeType; - }; - // Try to replace LOCALAPPDATA first as it is the likely location, fall back to trying USERPROFILE. void ReplaceProfilePathsWithEnvironmentVariable(std::filesystem::path& path) { @@ -238,99 +201,6 @@ namespace AppInstaller::Runtime s_runtimePathStateName.emplace(std::move(suitablePathPart)); } - void PathDetails::SetOwner(ACEPrincipal owner) - { - Owner = owner; - ACL[owner] = ACEPermissions::All; - } - - bool PathDetails::ShouldApplyACL() const - { - // Could be expanded to actually check the current owner/ACL on the path, but isn't worth it currently - return !ACL.empty(); - } - - void PathDetails::ApplyACL() const - { - bool hasCurrentUser = ACL.count(ACEPrincipal::CurrentUser) != 0; - bool hasSystem = ACL.count(ACEPrincipal::System) != 0; - - // Configuring permissions for both CurrentUser and SYSTEM while not having owner set as one of them is not valid because - // below we use only the owner permissions in the case of running as SYSTEM. - if ((hasCurrentUser && hasSystem) && - IsRunningAsSystem() && - (!Owner || (Owner.value() != ACEPrincipal::CurrentUser && Owner.value() != ACEPrincipal::System))) - { - THROW_HR(HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); - } - - auto userToken = wil::get_token_information<TOKEN_USER>(); - auto adminSID = wil::make_static_sid(SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS); - auto systemSID = wil::make_static_sid(SECURITY_NT_AUTHORITY, SECURITY_LOCAL_SYSTEM_RID); - PSID ownerSID = nullptr; - - ACEDetails aceDetails[] = - { - { ACEPrincipal::CurrentUser, userToken->User.Sid, TRUSTEE_IS_USER }, - { ACEPrincipal::Admins, adminSID.get(), TRUSTEE_IS_WELL_KNOWN_GROUP}, - { ACEPrincipal::System, systemSID.get(), TRUSTEE_IS_USER}, - }; - - ULONG entriesCount = 0; - std::array<EXPLICIT_ACCESS_W, ARRAYSIZE(aceDetails)> explicitAccess; - - // If the current user is SYSTEM, we want to take either the owner or the only configured set of permissions. - // The check above should prevent us from getting into situations outside of the ones below. - std::optional<ACEPrincipal> principalToIgnore; - if (hasCurrentUser && hasSystem && EqualSid(userToken->User.Sid, systemSID.get())) - { - principalToIgnore = (Owner.value() == ACEPrincipal::CurrentUser ? ACEPrincipal::System : ACEPrincipal::CurrentUser); - } - - for (const auto& ace : aceDetails) - { - if (principalToIgnore && principalToIgnore.value() == ace.Principal) - { - continue; - } - - if (Owner && Owner.value() == ace.Principal) - { - ownerSID = ace.SID; - } - - auto itr = ACL.find(ace.Principal); - if (itr != ACL.end()) - { - EXPLICIT_ACCESS_W& entry = explicitAccess[entriesCount++]; - entry = {}; - - entry.grfAccessPermissions = AccessPermissionsFrom(itr->second); - entry.grfAccessMode = SET_ACCESS; - entry.grfInheritance = CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE; - - entry.Trustee.pMultipleTrustee = nullptr; - entry.Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE; - entry.Trustee.TrusteeForm = TRUSTEE_IS_SID; - entry.Trustee.TrusteeType = ace.TrusteeType; - entry.Trustee.ptstrName = reinterpret_cast<LPWCH>(ace.SID); - } - } - - wil::unique_any<PACL, decltype(&::LocalFree), ::LocalFree> acl; - THROW_IF_WIN32_ERROR(SetEntriesInAclW(entriesCount, explicitAccess.data(), nullptr, &acl)); - - std::wstring path = Path.wstring(); - SECURITY_INFORMATION securityInformation = DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION; - - if (ownerSID) - { - securityInformation |= OWNER_SECURITY_INFORMATION; - } - - THROW_IF_WIN32_ERROR(SetNamedSecurityInfoW(&path[0], SE_FILE_OBJECT, securityInformation, ownerSID, nullptr, acl.get(), nullptr)); - } - // Contains all of the paths that are common between the runtime contexts. PathDetails GetPathDetailsCommon(PathName path, bool forDisplay) { @@ -616,38 +486,6 @@ namespace AppInstaller::Runtime return result; } - std::filesystem::path GetPathTo(PathName path, bool forDisplay) - { - PathDetails details = GetPathDetailsFor(path, forDisplay); - - if (details.Create) - { - if (details.Path.is_absolute()) - { - if (std::filesystem::exists(details.Path) && !std::filesystem::is_directory(details.Path)) - { - std::filesystem::remove(details.Path); - } - - std::filesystem::create_directories(details.Path); - - // Set the ACLs on the directory if needed. We do this after creating the directory because an attacker could - // have created the directory beforehand so we must be able to place the correct ACL on any directory or fail - // to operate. - if (details.ShouldApplyACL()) - { - details.ApplyACL(); - } - } - else - { - AICLI_LOG(Core, Warning, << "GetPathTo directory creation requested for [" << path << "], but path was not absolute: " << details.Path); - } - } - - return std::move(details.Path); - } - std::filesystem::path GetNewTempFilePath() { GUID guid; diff --git a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj @@ -411,6 +411,7 @@ <ClInclude Include="Public\winget\Certificates.h" /> <ClInclude Include="Public\winget\Compression.h" /> <ClInclude Include="Public\winget\ConfigurationSetProcessorHandlers.h" /> + <ClInclude Include="Public\winget\Filesystem.h" /> <ClInclude Include="Public\winget\GroupPolicy.h" /> <ClInclude Include="Public\winget\IConfigurationStaticsInternals.h" /> <ClInclude Include="Public\winget\ILifetimeWatcher.h" /> @@ -439,6 +440,7 @@ <ClCompile Include="Compression.cpp" /> <ClCompile Include="DateTime.cpp" /> <ClCompile Include="Errors.cpp" /> + <ClCompile Include="Filesystem.cpp" /> <ClCompile Include="GroupPolicy.cpp" /> <ClCompile Include="ICU\SQLiteICU.c"> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader> diff --git a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters @@ -131,6 +131,9 @@ <ClInclude Include="Public\winget\Compression.h"> <Filter>Public\winget</Filter> </ClInclude> + <ClInclude Include="Public\winget\Filesystem.h"> + <Filter>Public\winget</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -214,6 +217,9 @@ <ClCompile Include="Compression.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Filesystem.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerSharedLib/Filesystem.cpp b/src/AppInstallerSharedLib/Filesystem.cpp @@ -0,0 +1,424 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Public/winget/Filesystem.h" +#include "Public/AppInstallerStrings.h" +#include "Public/AppInstallerLogging.h" +#include "Public/winget/Runtime.h" + +using namespace std::chrono_literals; +using namespace std::string_view_literals; +using namespace AppInstaller::Runtime; + +namespace AppInstaller::Filesystem +{ + namespace anon + { + // Contains the information about an ACE entry for a given principal. + struct ACEDetails + { + ACEPrincipal Principal; + PSID SID; + TRUSTEE_TYPE TrusteeType; + }; + + DWORD AccessPermissionsFrom(ACEPermissions permissions) + { + DWORD result = 0; + + if (permissions == ACEPermissions::All) + { + result |= GENERIC_ALL; + } + else + { + if (WI_IsFlagSet(permissions, ACEPermissions::Read)) + { + result |= GENERIC_READ; + } + + if (WI_IsFlagSet(permissions, ACEPermissions::Write)) + { + result |= GENERIC_WRITE | FILE_DELETE_CHILD; + } + + if (WI_IsFlagSet(permissions, ACEPermissions::Execute)) + { + result |= GENERIC_EXECUTE; + } + } + + return result; + } + } + + DWORD GetVolumeInformationFlagsByHandle(HANDLE anyFileHandle) + { + DWORD flags = 0; + wchar_t fileSystemName[MAX_PATH]; + THROW_LAST_ERROR_IF(!GetVolumeInformationByHandleW( + anyFileHandle, /*hFile*/ + NULL, /*lpVolumeNameBuffer*/ + 0, /*nVolumeNameSize*/ + NULL, /*lpVolumeSerialNumber*/ + NULL, /*lpMaximumComponentLength*/ + &flags, /*lpFileSystemFlags*/ + fileSystemName, /*lpFileSystemNameBuffer*/ + MAX_PATH /*nFileSystemNameSize*/)); + + // Vista and older does not report all flags, fix them up here + if (!(flags & FILE_SUPPORTS_HARD_LINKS) && !_wcsicmp(fileSystemName, L"NTFS")) + { + flags |= FILE_SUPPORTS_HARD_LINKS | FILE_SUPPORTS_EXTENDED_ATTRIBUTES | FILE_SUPPORTS_OPEN_BY_FILE_ID | FILE_SUPPORTS_USN_JOURNAL; + } + + return flags; + } + + DWORD GetVolumeInformationFlags(const std::filesystem::path& anyPath) + { + wil::unique_hfile fileHandle{ CreateFileW( + anyPath.c_str(), /*lpFileName*/ + 0, /*dwDesiredAccess*/ + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, /*dwShareMode*/ + NULL, /*lpSecurityAttributes*/ + OPEN_EXISTING, /*dwCreationDisposition*/ + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, /*dwFlagsAndAttributes*/ + NULL /*hTemplateFile*/) }; + + THROW_LAST_ERROR_IF(fileHandle.get() == INVALID_HANDLE_VALUE); + + return GetVolumeInformationFlagsByHandle(fileHandle.get()); + } + + bool SupportsNamedStreams(const std::filesystem::path& path) + { + return (GetVolumeInformationFlags(path) & FILE_NAMED_STREAMS) != 0; + } + + bool SupportsHardLinks(const std::filesystem::path& path) + { + return (GetVolumeInformationFlags(path) & FILE_SUPPORTS_HARD_LINKS) != 0; + } + + bool SupportsReparsePoints(const std::filesystem::path& path) + { + return (GetVolumeInformationFlags(path) & FILE_SUPPORTS_REPARSE_POINTS) != 0; + } + + bool PathEscapesBaseDirectory(const std::filesystem::path& target, const std::filesystem::path& base) + { + const auto& targetPath = std::filesystem::weakly_canonical(target); + const auto& basePath = std::filesystem::weakly_canonical(base); + auto [a, b] = std::mismatch(targetPath.begin(), targetPath.end(), basePath.begin(), basePath.end()); + return (b != basePath.end()); + } + + // Complicated rename algorithm due to somewhat arbitrary failures. + // 1. First, try to rename. + // 2. Then, create an empty file for the target, and attempt to rename. + // 3. Then, try repeatedly for 500ms in case it is a timing thing. + // 4. Attempt to use a hard link if available. + // 5. Copy the file if nothing else has worked so far. + void RenameFile(const std::filesystem::path& from, const std::filesystem::path& to) + { + // 1. First, try to rename. + try + { + // std::filesystem::rename() handles motw correctly if applicable. + std::filesystem::rename(from, to); + return; + } + CATCH_LOG(); + + // 2. Then, create an empty file for the target, and attempt to rename. + // This seems to fix things in certain cases, so we do it. + try + { + { + std::ofstream targetFile{ to }; + } + std::filesystem::rename(from, to); + return; + } + CATCH_LOG(); + + // 3. Then, try repeatedly for 500ms in case it is a timing thing. + for (int i = 0; i < 5; ++i) + { + try + { + std::this_thread::sleep_for(100ms); + std::filesystem::rename(from, to); + return; + } + CATCH_LOG(); + } + + // 4. Attempt to use a hard link if available. + if (SupportsHardLinks(from)) + { + try + { + // Create a hard link to the file; the installer will be left in the temp directory afterward + // but it is better to succeed the operation and leave a file around than to fail. + // First we have to remove the target file as the function will not overwrite. + std::filesystem::remove(to); + std::filesystem::create_hard_link(from, to); + return; + } + CATCH_LOG(); + } + + // 5. Copy the file if nothing else has worked so far. + // Create a copy of the file; the installer will be left in the temp directory afterward + // but it is better to succeed the operation and leave a file around than to fail. + std::filesystem::copy_file(from, to, std::filesystem::copy_options::overwrite_existing); + } + +#ifndef AICLI_DISABLE_TEST_HOOKS + static bool* s_CreateSymlinkResult_TestHook_Override = nullptr; + + void TestHook_SetCreateSymlinkResult_Override(bool* status) + { + s_CreateSymlinkResult_TestHook_Override = status; + } +#endif + + bool CreateSymlink(const std::filesystem::path& target, const std::filesystem::path& link) + { +#ifndef AICLI_DISABLE_TEST_HOOKS + if (s_CreateSymlinkResult_TestHook_Override) + { + return *s_CreateSymlinkResult_TestHook_Override; + } +#endif + try + { + std::filesystem::create_symlink(target, link); + return true; + } + catch (std::filesystem::filesystem_error& error) + { + if (error.code().value() == ERROR_PRIVILEGE_NOT_HELD) + { + return false; + } + else + { + throw; + } + } + } + + bool VerifySymlink(const std::filesystem::path& symlink, const std::filesystem::path& target) + { + const std::filesystem::path& symlinkTargetPath = std::filesystem::weakly_canonical(symlink); + return symlinkTargetPath == std::filesystem::weakly_canonical(target); + } + + void AppendExtension(std::filesystem::path& target, const std::string& value) + { + if (target.extension() != value) + { + target += value; + } + } + + bool SymlinkExists(const std::filesystem::path& symlinkPath) + { + return std::filesystem::is_symlink(std::filesystem::symlink_status(symlinkPath)); + } + + std::filesystem::path GetExpandedPath(const std::string& path) + { + std::string trimPath = path; + Utility::Trim(trimPath); + + try + { + return std::filesystem::weakly_canonical(Utility::ExpandEnvironmentVariables(Utility::ConvertToUTF16(trimPath))); + } + catch (...) + { + return Utility::ConvertToUTF16(path); + } + } + + bool ReplaceCommonPathPrefix(std::filesystem::path& source, const std::filesystem::path& prefix, std::string_view replacement) + { + auto prefixItr = prefix.begin(); + auto sourceItr = source.begin(); + + while (prefixItr != prefix.end() && sourceItr != source.end()) + { + if (!Utility::ICUCaseInsensitiveEquals(prefixItr->u8string(), sourceItr->u8string())) + { + break; + } + + ++prefixItr; + ++sourceItr; + } + + // Only replace source if we found all of prefix + if (prefixItr == prefix.end()) + { + std::filesystem::path temp{ replacement }; + + for (; sourceItr != source.end(); ++sourceItr) + { + temp /= *sourceItr; + } + + source = std::move(temp); + + return true; + } + + return false; + } + + std::filesystem::path GetKnownFolderPath(const KNOWNFOLDERID& id) + { + wil::unique_cotaskmem_string knownFolder = nullptr; + THROW_IF_FAILED(SHGetKnownFolderPath(id, KF_FLAG_NO_ALIAS | KF_FLAG_DONT_VERIFY | KF_FLAG_NO_PACKAGE_REDIRECTION, NULL, &knownFolder)); + return knownFolder.get(); + } + + bool IsSameVolume(const std::filesystem::path& path1, const std::filesystem::path& path2) + { + WCHAR volumeName1[MAX_PATH]; + WCHAR volumeName2[MAX_PATH]; + + // Note: GetVolumePathNameW will return false if the volume drive does not exist. + if (!GetVolumePathNameW(path1.c_str(), volumeName1, MAX_PATH) || !GetVolumePathNameW(path2.c_str(), volumeName2, MAX_PATH)) + { + return false; + } + return Utility::ICUCaseInsensitiveEquals(Utility::ConvertToUTF8(volumeName1), Utility::ConvertToUTF8(volumeName2)); + } + + void PathDetails::SetOwner(ACEPrincipal owner) + { + Owner = owner; + ACL[owner] = ACEPermissions::All; + } + + bool PathDetails::ShouldApplyACL() const + { + // Could be expanded to actually check the current owner/ACL on the path, but isn't worth it currently + return !ACL.empty(); + } + + void PathDetails::ApplyACL() const + { + bool hasCurrentUser = ACL.count(ACEPrincipal::CurrentUser) != 0; + bool hasSystem = ACL.count(ACEPrincipal::System) != 0; + + // Configuring permissions for both CurrentUser and SYSTEM while not having owner set as one of them is not valid because + // below we use only the owner permissions in the case of running as SYSTEM. + if ((hasCurrentUser && hasSystem) && + IsRunningAsSystem() && + (!Owner || (Owner.value() != ACEPrincipal::CurrentUser && Owner.value() != ACEPrincipal::System))) + { + THROW_HR(HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); + } + + auto userToken = wil::get_token_information<TOKEN_USER>(); + auto adminSID = wil::make_static_sid(SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS); + auto systemSID = wil::make_static_sid(SECURITY_NT_AUTHORITY, SECURITY_LOCAL_SYSTEM_RID); + PSID ownerSID = nullptr; + + anon::ACEDetails aceDetails[] = + { + { ACEPrincipal::CurrentUser, userToken->User.Sid, TRUSTEE_IS_USER }, + { ACEPrincipal::Admins, adminSID.get(), TRUSTEE_IS_WELL_KNOWN_GROUP}, + { ACEPrincipal::System, systemSID.get(), TRUSTEE_IS_USER}, + }; + + ULONG entriesCount = 0; + std::array<EXPLICIT_ACCESS_W, ARRAYSIZE(aceDetails)> explicitAccess; + + // If the current user is SYSTEM, we want to take either the owner or the only configured set of permissions. + // The check above should prevent us from getting into situations outside of the ones below. + std::optional<ACEPrincipal> principalToIgnore; + if (hasCurrentUser && hasSystem && EqualSid(userToken->User.Sid, systemSID.get())) + { + principalToIgnore = (Owner.value() == ACEPrincipal::CurrentUser ? ACEPrincipal::System : ACEPrincipal::CurrentUser); + } + + for (const auto& ace : aceDetails) + { + if (principalToIgnore && principalToIgnore.value() == ace.Principal) + { + continue; + } + + if (Owner && Owner.value() == ace.Principal) + { + ownerSID = ace.SID; + } + + auto itr = ACL.find(ace.Principal); + if (itr != ACL.end()) + { + EXPLICIT_ACCESS_W& entry = explicitAccess[entriesCount++]; + entry = {}; + + entry.grfAccessPermissions = anon::AccessPermissionsFrom(itr->second); + entry.grfAccessMode = SET_ACCESS; + entry.grfInheritance = CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE; + + entry.Trustee.pMultipleTrustee = nullptr; + entry.Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE; + entry.Trustee.TrusteeForm = TRUSTEE_IS_SID; + entry.Trustee.TrusteeType = ace.TrusteeType; + entry.Trustee.ptstrName = reinterpret_cast<LPWCH>(ace.SID); + } + } + + wil::unique_any<PACL, decltype(&::LocalFree), ::LocalFree> acl; + THROW_IF_WIN32_ERROR(SetEntriesInAclW(entriesCount, explicitAccess.data(), nullptr, &acl)); + + std::wstring path = Path.wstring(); + SECURITY_INFORMATION securityInformation = DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION; + + if (ownerSID) + { + securityInformation |= OWNER_SECURITY_INFORMATION; + } + + THROW_IF_WIN32_ERROR(SetNamedSecurityInfoW(&path[0], SE_FILE_OBJECT, securityInformation, ownerSID, nullptr, acl.get(), nullptr)); + } + + std::filesystem::path InitializeAndGetPathTo(PathDetails&& details) + { + if (details.Create) + { + if (details.Path.is_absolute()) + { + if (std::filesystem::exists(details.Path) && !std::filesystem::is_directory(details.Path)) + { + std::filesystem::remove(details.Path); + } + + std::filesystem::create_directories(details.Path); + + // Set the ACLs on the directory if needed. We do this after creating the directory because an attacker could + // have created the directory beforehand so we must be able to place the correct ACL on any directory or fail + // to operate. + if (details.ShouldApplyACL()) + { + details.ApplyACL(); + } + } + else + { + AICLI_LOG(Core, Warning, << "InitializeAndGetPathTo directory creation requested for path that was not absolute: " << details.Path); + } + } + + return std::move(details.Path); + } +} diff --git a/src/AppInstallerSharedLib/Public/winget/Filesystem.h b/src/AppInstallerSharedLib/Public/winget/Filesystem.h @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <filesystem> +#include <map> +#include <optional> +#include <shtypes.h> + +namespace AppInstaller::Filesystem +{ + // Checks if the file system at path supports named streams/ADS + bool SupportsNamedStreams(const std::filesystem::path& path); + + // Checks if the file system at path supports hard links + bool SupportsHardLinks(const std::filesystem::path& path); + + // Checks if the file system at path support reparse points + bool SupportsReparsePoints(const std::filesystem::path& path); + + // Checks if the canonical form of the path points to a location outside of the provided base path. + bool PathEscapesBaseDirectory(const std::filesystem::path& target, const std::filesystem::path& base); + + // Renames the file to a new path. + void RenameFile(const std::filesystem::path& from, const std::filesystem::path& to); + + // Creates a symlink that points to the target path. + bool CreateSymlink(const std::filesystem::path& target, const std::filesystem::path& link); + + // Verifies that a symlink points to the target path. + bool VerifySymlink(const std::filesystem::path& symlink, const std::filesystem::path& target); + + // Appends the .exe extension to the path if not present. + void AppendExtension(std::filesystem::path& value, const std::string& extension); + + // Checks if the path is a symlink and exists. + bool SymlinkExists(const std::filesystem::path& symlinkPath); + bool CreateSymlink(const std::filesystem::path& path, const std::filesystem::path& target); + + // Get expanded file system path. + std::filesystem::path GetExpandedPath(const std::string& path); + + // If `source` begins with all of `prefix`, replace that with `replacement`. + // Returns true if replacement happened, false otherwise. + bool ReplaceCommonPathPrefix(std::filesystem::path& source, const std::filesystem::path& prefix, std::string_view replacement); + + // Gets the path of a known folder. + std::filesystem::path GetKnownFolderPath(const KNOWNFOLDERID& id); + + // Verifies that the paths are on the same volume. + bool IsSameVolume(const std::filesystem::path& path1, const std::filesystem::path& path2); + + // The principal that an ACE applies to. + enum class ACEPrincipal : uint32_t + { + CurrentUser, + Admins, + System, + }; + + // The permissions granted to a specific ACE. + enum class ACEPermissions : uint32_t + { + // This is not "Deny All", but rather, "Not mentioned" + None = 0x0, + Read = 0x1, + Write = 0x2, + Execute = 0x4, + ReadWrite = Read | Write, + ReadExecute = Read | Execute, + ReadWriteExecute = Read | Write | Execute, + // All means that full control will be granted + All = 0xFFFFFFFF + }; + + DEFINE_ENUM_FLAG_OPERATORS(ACEPermissions); + + // Information about a path that we use and how to set it up. + struct PathDetails + { + std::filesystem::path Path; + // Default to creating the directory with inherited ownership and permissions + bool Create = true; + std::optional<ACEPrincipal> Owner; + std::map<ACEPrincipal, ACEPermissions> ACL; + + // Shorthand for setting Owner and giving them ACEPermissions::All + void SetOwner(ACEPrincipal owner); + + // Determines if the ACL should be applied. + bool ShouldApplyACL() const; + + // Applies the ACL unconditionally. + void ApplyACL() const; + }; + + // Initializes from the given details and returns the path to it. + // The path is moved out of the details. + std::filesystem::path InitializeAndGetPathTo(PathDetails&& details); + + // Gets the path to the requested location. + template <class PathEnum> + std::filesystem::path GetPathTo(PathEnum path, bool forDisplay = false) + { + return InitializeAndGetPathTo(GetPathDetailsFor(path, forDisplay)); + } +} diff --git a/src/AppInstallerSharedLib/pch.h b/src/AppInstallerSharedLib/pch.h @@ -4,9 +4,11 @@ #define NOMINMAX #include <Windows.h> +#include <AclAPI.h> #include <appmodel.h> #include <icu.h> -#include <sddl.h> +#include <sddl.h> +#include <Shlobj.h> #include <compressapi.h> #define YAML_DECLARE_STATIC @@ -31,6 +33,7 @@ #include <functional> #include <iomanip> #include <limits> +#include <map> #include <memory> #include <mutex> #include <optional> @@ -57,4 +60,4 @@ #include <winrt/Windows.ApplicationModel.Resources.h> #include <winrt/Windows.Foundation.h> #include <winrt/Windows.Globalization.h> -#include <winrt/Windows.System.Profile.h> +#include <winrt/Windows.System.Profile.h> diff --git a/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/HostedEnvironment.cs b/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/HostedEnvironment.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="HostedEnvironment.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -352,6 +352,8 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces // Maybe is already there. if (!this.ValidateModule(moduleSpecification)) { + this.OnDiagnostics(DiagnosticLevel.Verbose, $"Installing module: {moduleSpecification.Name} ..."); + // Ok, we have to get it. if (this.location == PowerShellConfigurationProcessorLocation.Custom) { @@ -360,14 +362,18 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces throw new ArgumentNullException(nameof(this.customLocation)); } + this.OnDiagnostics(DiagnosticLevel.Verbose, $"... calling save module ..."); this.SaveModule(moduleSpecification, this.customLocation); } else { + this.OnDiagnostics(DiagnosticLevel.Verbose, $"... calling install module ..."); using PowerShell pwsh = PowerShell.Create(this.Runspace); this.powerShellGet.InstallModule(pwsh, moduleSpecification, this.location == PowerShellConfigurationProcessorLocation.AllUsers); this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); } + + this.OnDiagnostics(DiagnosticLevel.Verbose, $" ... module installed."); } } @@ -508,19 +514,25 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces private bool ValidateModule(ModuleSpecification moduleSpecification) { + this.OnDiagnostics(DiagnosticLevel.Verbose, $"Validating module: {moduleSpecification.Name} ..."); + var loadedModule = this.GetImportedModule(moduleSpecification); if (loadedModule is not null) { + this.OnDiagnostics(DiagnosticLevel.Verbose, $" ... module is already imported."); return true; } var availableModule = this.GetAvailableModule(moduleSpecification); if (availableModule is not null) { + this.OnDiagnostics(DiagnosticLevel.Verbose, $" ... module is available, importing ..."); this.ImportModule(moduleSpecification); + this.OnDiagnostics(DiagnosticLevel.Verbose, $" ... module imported."); return true; } + this.OnDiagnostics(DiagnosticLevel.Verbose, $" ... module not found."); return false; }