winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

Filesystem.cpp (19202B)


      1 // Copyright (c) Microsoft Corporation.
      2 // Licensed under the MIT License.
      3 #include "pch.h"
      4 #include "Public/winget/Filesystem.h"
      5 #include "Public/AppInstallerStrings.h"
      6 #include "Public/AppInstallerLogging.h"
      7 #include "Public/winget/Runtime.h"
      8 
      9 using namespace std::chrono_literals;
     10 using namespace std::string_view_literals;
     11 using namespace AppInstaller::Runtime;
     12 
     13 namespace AppInstaller::Filesystem
     14 {
     15     namespace anon
     16     {
     17         constexpr std::string_view s_AppDataDir_Settings = "Settings"sv;
     18         constexpr std::string_view s_AppDataDir_State = "State"sv;
     19 
     20         constexpr std::string_view s_LocalAppDataEnvironmentVariable = "%LOCALAPPDATA%";
     21 
     22         // Contains the information about an ACE entry for a given principal.
     23         struct ACEDetails
     24         {
     25             ACEPrincipal Principal;
     26             PSID SID;
     27             TRUSTEE_TYPE TrusteeType;
     28         };
     29 
     30         DWORD AccessPermissionsFrom(ACEPermissions permissions)
     31         {
     32             DWORD result = 0;
     33 
     34             if (permissions == ACEPermissions::All)
     35             {
     36                 result |= GENERIC_ALL;
     37             }
     38             else
     39             {
     40                 if (WI_IsFlagSet(permissions, ACEPermissions::Read))
     41                 {
     42                     result |= GENERIC_READ;
     43                 }
     44 
     45                 if (WI_IsFlagSet(permissions, ACEPermissions::Write))
     46                 {
     47                     result |= GENERIC_WRITE | FILE_DELETE_CHILD;
     48                 }
     49 
     50                 if (WI_IsFlagSet(permissions, ACEPermissions::Execute))
     51                 {
     52                     result |= GENERIC_EXECUTE;
     53                 }
     54             }
     55 
     56             return result;
     57         }
     58 
     59         // Gets the path to the appdata root.
     60         // *Only used by non packaged version!*
     61         std::filesystem::path GetPathToAppDataRoot(bool anonymize)
     62         {
     63             std::filesystem::path result = anonymize ? s_LocalAppDataEnvironmentVariable : GetKnownFolderPath(FOLDERID_LocalAppData);
     64             result /= "Microsoft/WinGet";
     65 
     66             return result;
     67         }
     68 
     69         // Gets the path to the app data relative directory.
     70         std::filesystem::path GetPathToAppDataDir(const std::filesystem::path& relative, bool anonymize)
     71         {
     72             THROW_HR_IF(E_INVALIDARG, !relative.has_relative_path());
     73             THROW_HR_IF(E_INVALIDARG, relative.has_root_path());
     74             THROW_HR_IF(E_INVALIDARG, !relative.has_filename());
     75 
     76             std::filesystem::path result = GetPathToAppDataRoot(anonymize);
     77             result /= relative;
     78 
     79             return result;
     80         }
     81     }
     82 
     83     DWORD GetVolumeInformationFlagsByHandle(HANDLE anyFileHandle)
     84     {
     85         DWORD flags = 0;
     86         wchar_t fileSystemName[MAX_PATH];
     87         THROW_LAST_ERROR_IF(!GetVolumeInformationByHandleW(
     88             anyFileHandle, /*hFile*/
     89             NULL, /*lpVolumeNameBuffer*/
     90             0, /*nVolumeNameSize*/
     91             NULL, /*lpVolumeSerialNumber*/
     92             NULL, /*lpMaximumComponentLength*/
     93             &flags, /*lpFileSystemFlags*/
     94             fileSystemName, /*lpFileSystemNameBuffer*/
     95             MAX_PATH /*nFileSystemNameSize*/));
     96 
     97         // Vista and older does not report all flags, fix them up here
     98         if (!(flags & FILE_SUPPORTS_HARD_LINKS) && !_wcsicmp(fileSystemName, L"NTFS"))
     99         {
    100             flags |= FILE_SUPPORTS_HARD_LINKS | FILE_SUPPORTS_EXTENDED_ATTRIBUTES | FILE_SUPPORTS_OPEN_BY_FILE_ID | FILE_SUPPORTS_USN_JOURNAL;
    101         }
    102 
    103         return flags;
    104     }
    105 
    106     DWORD GetVolumeInformationFlags(const std::filesystem::path& anyPath)
    107     {
    108         wil::unique_hfile fileHandle{ CreateFileW(
    109             anyPath.c_str(), /*lpFileName*/
    110             0, /*dwDesiredAccess*/
    111             FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, /*dwShareMode*/
    112             NULL, /*lpSecurityAttributes*/
    113             OPEN_EXISTING, /*dwCreationDisposition*/
    114             FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, /*dwFlagsAndAttributes*/
    115             NULL /*hTemplateFile*/) };
    116 
    117         THROW_LAST_ERROR_IF(fileHandle.get() == INVALID_HANDLE_VALUE);
    118 
    119         return GetVolumeInformationFlagsByHandle(fileHandle.get());
    120     }
    121 
    122     bool SupportsNamedStreams(const std::filesystem::path& path)
    123     {
    124         return (GetVolumeInformationFlags(path) & FILE_NAMED_STREAMS) != 0;
    125     }
    126 
    127     bool SupportsHardLinks(const std::filesystem::path& path)
    128     {
    129         return (GetVolumeInformationFlags(path) & FILE_SUPPORTS_HARD_LINKS) != 0;
    130     }
    131 
    132     bool SupportsReparsePoints(const std::filesystem::path& path)
    133     {
    134         return (GetVolumeInformationFlags(path) & FILE_SUPPORTS_REPARSE_POINTS) != 0;
    135     }
    136 
    137     bool PathEscapesBaseDirectory(const std::filesystem::path& target, const std::filesystem::path& base)
    138     {
    139         const auto& targetPath = std::filesystem::weakly_canonical(target);
    140         const auto& basePath = std::filesystem::weakly_canonical(base);
    141         auto [a, b] = std::mismatch(targetPath.begin(), targetPath.end(), basePath.begin(), basePath.end());
    142         return (b != basePath.end());
    143     }
    144 
    145     // Complicated rename algorithm due to somewhat arbitrary failures.
    146     // 1. First, try to rename.
    147     // 2. Then, create an empty file for the target, and attempt to rename.
    148     // 3. Then, try repeatedly for 500ms in case it is a timing thing.
    149     // 4. Attempt to use a hard link if available.
    150     // 5. Copy the file if nothing else has worked so far.
    151     void RenameFile(const std::filesystem::path& from, const std::filesystem::path& to)
    152     {
    153         // 1. First, try to rename.
    154         try
    155         {
    156             // std::filesystem::rename() handles motw correctly if applicable.
    157             std::filesystem::rename(from, to);
    158             return;
    159         }
    160         CATCH_LOG();
    161 
    162         // 2. Then, create an empty file for the target, and attempt to rename.
    163         //    This seems to fix things in certain cases, so we do it.
    164         try
    165         {
    166             {
    167                 std::ofstream targetFile{ to };
    168             }
    169             std::filesystem::rename(from, to);
    170             return;
    171         }
    172         CATCH_LOG();
    173 
    174         // 3. Then, try repeatedly for 500ms in case it is a timing thing.
    175         for (int i = 0; i < 5; ++i)
    176         {
    177             try
    178             {
    179                 std::this_thread::sleep_for(100ms);
    180                 std::filesystem::rename(from, to);
    181                 return;
    182             }
    183             CATCH_LOG();
    184         }
    185 
    186         // 4. Attempt to use a hard link if available.
    187         if (SupportsHardLinks(from))
    188         {
    189             try
    190             {
    191                 // Create a hard link to the file; the installer will be left in the temp directory afterward
    192                 // but it is better to succeed the operation and leave a file around than to fail.
    193                 // First we have to remove the target file as the function will not overwrite.
    194                 std::filesystem::remove(to);
    195                 std::filesystem::create_hard_link(from, to);
    196                 return;
    197             }
    198             CATCH_LOG();
    199         }
    200 
    201         // 5. Copy the file if nothing else has worked so far.
    202         // Create a copy of the file; the installer will be left in the temp directory afterward
    203         // but it is better to succeed the operation and leave a file around than to fail.
    204         std::filesystem::copy_file(from, to, std::filesystem::copy_options::overwrite_existing);
    205     }
    206 
    207 #ifndef AICLI_DISABLE_TEST_HOOKS
    208     static bool* s_CreateSymlinkResult_TestHook_Override = nullptr;
    209 
    210     void TestHook_SetCreateSymlinkResult_Override(bool* status)
    211     {
    212         s_CreateSymlinkResult_TestHook_Override = status;
    213     }
    214 #endif
    215 
    216     bool CreateSymlink(const std::filesystem::path& target, const std::filesystem::path& link)
    217     {
    218 #ifndef AICLI_DISABLE_TEST_HOOKS
    219         if (s_CreateSymlinkResult_TestHook_Override)
    220         {
    221             return *s_CreateSymlinkResult_TestHook_Override;
    222         }
    223 #endif
    224         try
    225         {
    226             std::filesystem::create_symlink(target, link);
    227             return true;
    228         }
    229         catch (std::filesystem::filesystem_error& error)
    230         {
    231             if (error.code().value() == ERROR_PRIVILEGE_NOT_HELD)
    232             {
    233                 return false;
    234             }
    235             else
    236             {
    237                 throw;
    238             }
    239         }
    240     }
    241 
    242     bool VerifySymlink(const std::filesystem::path& symlink, const std::filesystem::path& target)
    243     {
    244         const std::filesystem::path& symlinkTargetPath = std::filesystem::weakly_canonical(symlink);
    245         return symlinkTargetPath == std::filesystem::weakly_canonical(target);
    246     }
    247 
    248     void AppendExtension(std::filesystem::path& target, const std::string& value)
    249     {
    250         if (target.extension() != value)
    251         {
    252             target += value;
    253         }
    254     }
    255 
    256     bool SymlinkExists(const std::filesystem::path& symlinkPath)
    257     {
    258         return std::filesystem::is_symlink(std::filesystem::symlink_status(symlinkPath));
    259     }
    260 
    261     std::filesystem::path GetExpandedPath(const std::string& path)
    262     {
    263         std::string trimPath = path;
    264         Utility::Trim(trimPath);
    265 
    266         try
    267         {
    268             return std::filesystem::weakly_canonical(Utility::ExpandEnvironmentVariables(Utility::ConvertToUTF16(trimPath)));
    269         }
    270         catch (...)
    271         {
    272             return Utility::ConvertToUTF16(path);
    273         }
    274     }
    275 
    276     bool ReplaceCommonPathPrefix(std::filesystem::path& source, const std::filesystem::path& prefix, std::string_view replacement)
    277     {
    278         auto prefixItr = prefix.begin();
    279         auto sourceItr = source.begin();
    280 
    281         while (prefixItr != prefix.end() && sourceItr != source.end())
    282         {
    283             if (!Utility::ICUCaseInsensitiveEquals(prefixItr->u8string(), sourceItr->u8string()))
    284             {
    285                 break;
    286             }
    287 
    288             ++prefixItr;
    289             ++sourceItr;
    290         }
    291 
    292         // Only replace source if we found all of prefix
    293         if (prefixItr == prefix.end())
    294         {
    295             std::filesystem::path temp{ replacement };
    296 
    297             for (; sourceItr != source.end(); ++sourceItr)
    298             {
    299                 temp /= *sourceItr;
    300             }
    301 
    302             source = std::move(temp);
    303 
    304             return true;
    305         }
    306 
    307         return false;
    308     }
    309 
    310     std::filesystem::path GetKnownFolderPath(const KNOWNFOLDERID& id)
    311     {
    312         wil::unique_cotaskmem_string knownFolder = nullptr;
    313         THROW_IF_FAILED(SHGetKnownFolderPath(id, KF_FLAG_NO_ALIAS | KF_FLAG_DONT_VERIFY | KF_FLAG_NO_PACKAGE_REDIRECTION, NULL, &knownFolder));
    314         return knownFolder.get();
    315     }
    316 
    317     bool IsSameVolume(const std::filesystem::path& path1, const std::filesystem::path& path2)
    318     {
    319         WCHAR volumeName1[MAX_PATH];
    320         WCHAR volumeName2[MAX_PATH];
    321 
    322         // Note: GetVolumePathNameW will return false if the volume drive does not exist.
    323         if (!GetVolumePathNameW(path1.c_str(), volumeName1, MAX_PATH) || !GetVolumePathNameW(path2.c_str(), volumeName2, MAX_PATH))
    324         {
    325             return false;
    326         }
    327         return Utility::ICUCaseInsensitiveEquals(Utility::ConvertToUTF8(volumeName1), Utility::ConvertToUTF8(volumeName2));
    328     }
    329 
    330     bool IsParentPath(const std::filesystem::path& path, const std::filesystem::path& parentPath)
    331     {
    332         return std::filesystem::weakly_canonical(path.parent_path()) == std::filesystem::weakly_canonical(parentPath);
    333     }
    334 
    335     void PathDetails::SetOwner(ACEPrincipal owner)
    336     {
    337         Owner = owner;
    338         ACL[owner] = ACEPermissions::All;
    339     }
    340 
    341     bool PathDetails::ShouldApplyACL() const
    342     {
    343         // Could be expanded to actually check the current owner/ACL on the path, but isn't worth it currently
    344         return !ACL.empty();
    345     }
    346 
    347     void PathDetails::ApplyACL() const
    348     {
    349         bool hasCurrentUser = ACL.count(ACEPrincipal::CurrentUser) != 0;
    350         bool hasSystem = ACL.count(ACEPrincipal::System) != 0;
    351 
    352         // Configuring permissions for both CurrentUser and SYSTEM while not having owner set as one of them is not valid because
    353         // below we use only the owner permissions in the case of running as SYSTEM.
    354         if ((hasCurrentUser && hasSystem) &&
    355             IsRunningAsSystem() &&
    356             (!Owner || (Owner.value() != ACEPrincipal::CurrentUser && Owner.value() != ACEPrincipal::System)))
    357         {
    358             THROW_HR(HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
    359         }
    360 
    361         auto userToken = wil::get_token_information<TOKEN_USER>();
    362         auto adminSID = wil::make_static_sid(SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS);
    363         auto systemSID = wil::make_static_sid(SECURITY_NT_AUTHORITY, SECURITY_LOCAL_SYSTEM_RID);
    364         PSID ownerSID = nullptr;
    365 
    366         anon::ACEDetails aceDetails[] =
    367         {
    368             { ACEPrincipal::CurrentUser, userToken->User.Sid, TRUSTEE_IS_USER },
    369             { ACEPrincipal::Admins, adminSID.get(), TRUSTEE_IS_WELL_KNOWN_GROUP},
    370             { ACEPrincipal::System, systemSID.get(), TRUSTEE_IS_USER},
    371         };
    372 
    373         ULONG entriesCount = 0;
    374         std::array<EXPLICIT_ACCESS_W, ARRAYSIZE(aceDetails)> explicitAccess;
    375 
    376         // If the current user is SYSTEM, we want to take either the owner or the only configured set of permissions.
    377         // The check above should prevent us from getting into situations outside of the ones below.
    378         std::optional<ACEPrincipal> principalToIgnore;
    379         if (hasCurrentUser && hasSystem && EqualSid(userToken->User.Sid, systemSID.get()))
    380         {
    381             principalToIgnore = (Owner.value() == ACEPrincipal::CurrentUser ? ACEPrincipal::System : ACEPrincipal::CurrentUser);
    382         }
    383 
    384         for (const auto& ace : aceDetails)
    385         {
    386             if (principalToIgnore && principalToIgnore.value() == ace.Principal)
    387             {
    388                 continue;
    389             }
    390 
    391             if (Owner && Owner.value() == ace.Principal)
    392             {
    393                 ownerSID = ace.SID;
    394             }
    395 
    396             auto itr = ACL.find(ace.Principal);
    397             if (itr != ACL.end())
    398             {
    399                 EXPLICIT_ACCESS_W& entry = explicitAccess[entriesCount++];
    400                 entry = {};
    401 
    402                 entry.grfAccessPermissions = anon::AccessPermissionsFrom(itr->second);
    403                 entry.grfAccessMode = SET_ACCESS;
    404                 entry.grfInheritance = CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE;
    405 
    406                 entry.Trustee.pMultipleTrustee = nullptr;
    407                 entry.Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE;
    408                 entry.Trustee.TrusteeForm = TRUSTEE_IS_SID;
    409                 entry.Trustee.TrusteeType = ace.TrusteeType;
    410                 entry.Trustee.ptstrName = reinterpret_cast<LPWCH>(ace.SID);
    411             }
    412         }
    413 
    414         wil::unique_any<PACL, decltype(&::LocalFree), ::LocalFree> acl;
    415         THROW_IF_WIN32_ERROR(SetEntriesInAclW(entriesCount, explicitAccess.data(), nullptr, &acl));
    416 
    417         std::wstring path = Path.wstring();
    418         SECURITY_INFORMATION securityInformation = DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION;
    419 
    420         if (ownerSID)
    421         {
    422             securityInformation |= OWNER_SECURITY_INFORMATION;
    423         }
    424 
    425         DWORD result = SetNamedSecurityInfoW(&path[0], SE_FILE_OBJECT, securityInformation, ownerSID, nullptr, acl.get(), nullptr);
    426 
    427         // We can be denied access attempting to set the owner when the owner is already correct.
    428         // Determine if the owner is correct; if so, try again without attempting to set the owner.
    429         if (result == ERROR_ACCESS_DENIED && ownerSID)
    430         {
    431             wil::unique_hlocal_security_descriptor securityDescriptor;
    432             PSID currentOwnerSID = nullptr;
    433             DWORD getResult = GetNamedSecurityInfoW(&path[0], SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, &currentOwnerSID, nullptr, nullptr, nullptr, &securityDescriptor);
    434 
    435             if (SUCCEEDED_WIN32_LOG(getResult) && currentOwnerSID && EqualSid(currentOwnerSID, ownerSID))
    436             {
    437                 result = SetNamedSecurityInfoW(&path[0], SE_FILE_OBJECT, securityInformation & ~OWNER_SECURITY_INFORMATION, nullptr, nullptr, acl.get(), nullptr);
    438             }
    439         }
    440 
    441         THROW_IF_WIN32_ERROR(result);
    442     }
    443 
    444     std::filesystem::path InitializeAndGetPathTo(PathDetails&& details)
    445     {
    446         if (details.Create)
    447         {
    448             if (details.Path.is_absolute())
    449             {
    450                 if (std::filesystem::exists(details.Path) && !std::filesystem::is_directory(details.Path))
    451                 {
    452                     std::filesystem::remove(details.Path);
    453                 }
    454 
    455                 std::filesystem::create_directories(details.Path);
    456 
    457                 // Set the ACLs on the directory if needed. We do this after creating the directory because an attacker could
    458                 // have created the directory beforehand so we must be able to place the correct ACL on any directory or fail
    459                 // to operate.
    460                 if (details.ShouldApplyACL())
    461                 {
    462                     details.ApplyACL();
    463                 }
    464             }
    465             else
    466             {
    467                 AICLI_LOG(Core, Warning, << "InitializeAndGetPathTo directory creation requested for path that was not absolute: " << details.Path);
    468             }
    469         }
    470 
    471         return std::move(details.Path);
    472     }
    473 
    474     PathDetails GetPathDetailsFor(PathName path, bool forDisplay)
    475     {
    476         PathDetails result;
    477         // We should not create directories by default when they are retrieved for display purposes.
    478         result.Create = !forDisplay;
    479 
    480         switch (path)
    481         {
    482         case PathName::UnpackagedLocalStateRoot:
    483             result.Path = anon::GetPathToAppDataDir(anon::s_AppDataDir_State, forDisplay);
    484             result.SetOwner(ACEPrincipal::CurrentUser);
    485             result.ACL[ACEPrincipal::System] = ACEPermissions::All;
    486             result.ACL[ACEPrincipal::Admins] = ACEPermissions::All;
    487             break;
    488         case PathName::UnpackagedSettingsRoot:
    489             result.Path = anon::GetPathToAppDataDir(anon::s_AppDataDir_Settings, forDisplay);
    490             result.SetOwner(ACEPrincipal::CurrentUser);
    491             result.ACL[ACEPrincipal::System] = ACEPermissions::All;
    492             result.ACL[ACEPrincipal::Admins] = ACEPermissions::All;
    493             break;
    494         default:
    495             THROW_HR(E_UNEXPECTED);
    496         }
    497 
    498         return result;
    499     }
    500 
    501     std::filesystem::path GetExecutablePathForProcess(HANDLE process)
    502     {
    503         wil::unique_cotaskmem_string imageName = nullptr;
    504         if (SUCCEEDED(wil::QueryFullProcessImageNameW(process, 0, imageName)) &&
    505             (imageName.get() != nullptr))
    506         {
    507             return imageName.get();
    508         }
    509 
    510         return {};
    511     }
    512 }