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, ¤tOwnerSID, 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 }