tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit ac562564b2c8f351d0eade43b2833faa33c3c683
parent 21c8e2c40719b4b84aa8160a03271f476cc238d8
Author: Bob Owen <bobowencode@gmail.com>
Date:   Mon,  6 Oct 2025 08:04:25 +0000

Bug 1980886 p3 - Ensure no old inherited ACEs remain when moving files on Windows. r=yjuglaret

Differential Revision: https://phabricator.services.mozilla.com/D265007

Diffstat:
Mxpcom/base/nsWindowsHelpers.h | 13+++++++++++++
Mxpcom/io/nsLocalFileWin.cpp | 122++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mxpcom/io/nsLocalFileWin.h | 16++++++++++++++++
Mxpcom/tests/gtest/TestFile.cpp | 87++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
4 files changed, 209 insertions(+), 29 deletions(-)

diff --git a/xpcom/base/nsWindowsHelpers.h b/xpcom/base/nsWindowsHelpers.h @@ -328,6 +328,19 @@ struct CloseHandleDeleter { } }; +using AutoFreeSecurityDescriptor = + mozilla::UniquePtr<SECURITY_DESCRIPTOR, LocalFreeDeleter>; + +struct DestroyPrivateObjectSecurityDeleter { + void operator()(PSECURITY_DESCRIPTOR aSecDescPtr) { + ::DestroyPrivateObjectSecurity(&aSecDescPtr); + } +}; + +using AutoDestroySecurityDescriptor = + mozilla::UniquePtr<SECURITY_DESCRIPTOR, + DestroyPrivateObjectSecurityDeleter>; + // One caller of this function is early in startup and several others are not, // so they have different ways of determining the two parameters. This function // exists just so any future code that needs to determine whether the dynamic diff --git a/xpcom/io/nsLocalFileWin.cpp b/xpcom/io/nsLocalFileWin.cpp @@ -5,6 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" #include "mozilla/DebugOnly.h" #include "mozilla/ProfilerLabels.h" #include "mozilla/TextUtils.h" @@ -96,7 +97,6 @@ nsresult NewLocalFile(const nsAString& aPath, bool aUseDOSDevicePathSyntax, file.forget(aResult); return NS_OK; } - } // anonymous namespace static HWND GetMostRecentNavigatorHWND() { @@ -203,6 +203,73 @@ bool nsLocalFile::CheckForReservedFileName(const nsString& aFileName) { return false; } +/* static */ +bool nsLocalFile::ChildAclMatchesAclInheritedFromParent( + const NotNull<ACL*> aChildDacl, bool aIsChildDir, + const AutoFreeSecurityDescriptor& aChildSecDesc, nsIFile* aParentDir) { + // If we fail at any point return false. + ACL* parentDacl = nullptr; + AutoFreeSecurityDescriptor parentSecDesc; + nsAutoString parentPath; + MOZ_ALWAYS_SUCCEEDS(aParentDir->GetTarget(parentPath)); + DWORD errCode = ::GetNamedSecurityInfoW( + parentPath.getW(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, + nullptr, &parentDacl, nullptr, getter_Transfers(parentSecDesc)); + if (errCode != ERROR_SUCCESS || !parentDacl) { + NS_ERROR(nsPrintfCString( + "Failed to get parent dir DACL for comparison: %lx", errCode) + .get()); + return false; + } + + // Create a new security descriptor with a DACL that just inherits from the + // parent. We can then compare the current childs DACL to make sure the counts + // of inherited ACEs match. We pass the child security descriptor as the + // creator to get the owner and group information. + AutoDestroySecurityDescriptor newSecDesc; + GENERIC_MAPPING mapping; + mapping.GenericRead = FILE_GENERIC_READ; + mapping.GenericWrite = FILE_GENERIC_WRITE; + mapping.GenericExecute = FILE_GENERIC_EXECUTE; + mapping.GenericAll = FILE_ALL_ACCESS; + if (!::CreatePrivateObjectSecurityEx( + parentSecDesc.get(), aChildSecDesc.get(), + getter_Transfers(newSecDesc), nullptr, aIsChildDir, + SEF_DACL_AUTO_INHERIT | SEF_AVOID_OWNER_CHECK | + SEF_AVOID_PRIVILEGE_CHECK, + nullptr, &mapping)) { + // There may be legitimate reasons for this to fail, so we might have to + // remove this if it causes problems. + NS_ERROR(nsPrintfCString( + "Failed to create new inherited DACL for comparison: %lx", + ::GetLastError()) + .get()); + return false; + } + + BOOL daclPresent; + ACL* newDacl = nullptr; + BOOL daclDefaulted; + if (!::GetSecurityDescriptorDacl(newSecDesc.get(), &daclPresent, &newDacl, + &daclDefaulted) || + !daclPresent || !newDacl) { + NS_ERROR( + nsPrintfCString("Failed to get new DACL from security descriptor: %lx", + ::GetLastError()) + .get()); + return false; + } + + auto getInheritedAceCount = [](const ACL* aAcl) { + AclAceRange aclAceRange(WrapNotNull(aAcl)); + return std::count_if( + aclAceRange.begin(), aclAceRange.end(), + [](const auto& hdr) { return hdr.AceFlags & INHERITED_ACE; }); + }; + + return getInheritedAceCount(aChildDacl) == getInheritedAceCount(newDacl); +} + class nsDriveEnumerator : public nsSimpleEnumerator, public nsIDirectoryEnumerator { public: @@ -1800,6 +1867,10 @@ nsresult nsLocalFile::MoveOrCopyAsSingleFileOrDir(nsIFile* aDestParent, return NS_ERROR_FILE_ACCESS_DENIED; } + // Determine if we are a directory before any move/copy. + bool isDir = false; + MOZ_ALWAYS_SUCCEEDS(IsDirectory(&isDir)); + int copyOK = 0; if (move) { copyOK = ::MoveFileExW(filePath.get(), destPath.get(), @@ -1850,31 +1921,30 @@ nsresult nsLocalFile::MoveOrCopyAsSingleFileOrDir(nsIFile* aDestParent, } else if (move && !(aOptions & SkipNtfsAclReset)) { // Set security permissions to inherit from parent. // Note: propagates to all children: slow for big file trees - PACL pOldDACL = nullptr; - PSECURITY_DESCRIPTOR pSD = nullptr; - ::GetNamedSecurityInfoW((LPWSTR)destPath.get(), SE_FILE_OBJECT, - DACL_SECURITY_INFORMATION, nullptr, nullptr, - &pOldDACL, nullptr, &pSD); - UniquePtr<VOID, LocalFreeDeleter> autoFreeSecDesc(pSD); - if (pOldDACL) { - // Test the current DACL, if we find one that is inherited then we can - // skip the reset. This avoids a request for SeTcbPrivilege, which can - // cause a lot of audit events if enabled (Bug 1816694). - bool inherited = false; - for (DWORD i = 0; i < pOldDACL->AceCount; ++i) { - VOID* pAce = nullptr; - if (::GetAce(pOldDACL, i, &pAce) && - static_cast<PACE_HEADER>(pAce)->AceFlags & INHERITED_ACE) { - inherited = true; - break; - } - } - - if (!inherited) { - ::SetNamedSecurityInfoW( - (LPWSTR)destPath.get(), SE_FILE_OBJECT, - DACL_SECURITY_INFORMATION | UNPROTECTED_DACL_SECURITY_INFORMATION, - nullptr, nullptr, pOldDACL, nullptr); + ACL* childDacl = nullptr; + AutoFreeSecurityDescriptor childSecDesc; + // We need owner and group information for the parent ACL check. + DWORD errCode = ::GetNamedSecurityInfoW( + destPath.getW(), SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION | + GROUP_SECURITY_INFORMATION, + nullptr, nullptr, &childDacl, nullptr, getter_Transfers(childSecDesc)); + if (errCode == ERROR_SUCCESS && childDacl) { + // Compare the number of inherited ACEs on the child to the number we + // expect from the parent. If they don't match then we reset. This is + // because we can get old inherited ACEs from the previous dir if moved + // within the same volume. We check this to prevent unnecessary calls to + // SetNamedSecurityInfoW, this avoids a request for SeTcbPrivilege, which + // can cause a lot of audit events if enabled (Bug 1816694). + if (!ChildAclMatchesAclInheritedFromParent(WrapNotNull(childDacl), isDir, + childSecDesc, aDestParent)) { + // We don't expect this to fail, but it shouldn't crash in release. + MOZ_ALWAYS_TRUE( + ERROR_SUCCESS == + ::SetNamedSecurityInfoW(destPath.get(), SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION | + UNPROTECTED_DACL_SECURITY_INFORMATION, + nullptr, nullptr, childDacl, nullptr)); } } } diff --git a/xpcom/io/nsLocalFileWin.h b/xpcom/io/nsLocalFileWin.h @@ -14,6 +14,7 @@ #include "nsIFile.h" #include "nsILocalFileWin.h" #include "nsIClassInfoImpl.h" +#include "nsWindowsHelpers.h" #include "prio.h" #include "mozilla/Attributes.h" @@ -52,6 +53,21 @@ class nsLocalFile final : public nsILocalFileWin { // (com1, com2, etc...) and returns true if so. static bool CheckForReservedFileName(const nsString& aFileName); + /** + * Checks whether the inherited ACEs in aChildDacl only come from the parent. + * + * @param aChildDacl the DACL for the child file object + * @param aIsChildDir true if child is a directory + * @param aChildSecDesc the SECURITY_DESCRIPTOR for the child, this will + * correspond to aChildDacl + * @param aParentDir the parent nsIFile + * @return true if inherited ACEs are only from aParentDir + * false if child has other inherited ACEs or a failure occurs + */ + static bool ChildAclMatchesAclInheritedFromParent( + const mozilla::NotNull<ACL*> aChildDacl, bool aIsChildDir, + const AutoFreeSecurityDescriptor& aChildSecDesc, nsIFile* aParentDir); + // PRFileInfo64 does not hvae an accessTime field; struct FileInfo { PRFileType type; diff --git a/xpcom/tests/gtest/TestFile.cpp b/xpcom/tests/gtest/TestFile.cpp @@ -7,19 +7,25 @@ #include "prsystem.h" #include "nsIFile.h" -#ifdef XP_WIN -# include "nsILocalFileWin.h" -#endif #include "nsComponentManagerUtils.h" #include "nsString.h" #include "nsDirectoryServiceDefs.h" #include "nsDirectoryServiceUtils.h" #include "nsPrintfCString.h" +#ifdef XP_WIN +# include <aclapi.h> +# include "mozilla/RandomNum.h" +# include "nsILocalFileWin.h" +# include "nsLocalFile.h" +# include "nsWindowsHelpers.h" +#endif + #include "gtest/gtest.h" #include "mozilla/gtest/MozAssertions.h" #ifdef XP_WIN +using namespace mozilla; bool gTestWithPrefix_Win = false; #endif @@ -40,6 +46,56 @@ static void SetUseDOSDevicePathSyntax(nsIFile* aFile) { winFile->SetUseDOSDevicePathSyntax(true); } } + +static auto GetSecurityInfoStructured(nsIFile* aFile) { + nsAutoString pathStr; + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(aFile->GetTarget(pathStr))); + + PACL pDacl = nullptr; + AutoFreeSecurityDescriptor secDesc; + DWORD errCode = ::GetNamedSecurityInfoW( + pathStr.getW(), SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION | + GROUP_SECURITY_INFORMATION, + nullptr, nullptr, &pDacl, nullptr, getter_Transfers(secDesc)); + MOZ_RELEASE_ASSERT(errCode == ERROR_SUCCESS && pDacl); + + return std::make_tuple(std::move(pathStr), WrapNotNull(pDacl), + std::move(secDesc)); +} + +static void AddAcesForRandomSidToDir(nsIFile* aDir) { + auto [dirPath, pDirDacl, secDesc] = GetSecurityInfoStructured(aDir); + + constexpr BYTE kSubAuthorityCount = 4; + BYTE randomSidBuffer[SECURITY_SID_SIZE(4)]; + ASSERT_TRUE( + GenerateRandomBytesFromOS(randomSidBuffer, sizeof(randomSidBuffer))); + auto* randomSid = reinterpret_cast<SID*>(randomSidBuffer); + randomSid->Revision = SID_REVISION; + randomSid->SubAuthorityCount = kSubAuthorityCount; + randomSid->IdentifierAuthority = SECURITY_NULL_SID_AUTHORITY; + ASSERT_TRUE(::IsValidSid(randomSid)); + + EXPLICIT_ACCESS_W newAccess[2]; + newAccess[0].grfAccessMode = GRANT_ACCESS; + newAccess[0].grfAccessPermissions = GENERIC_READ; + newAccess[0].grfInheritance = SUB_OBJECTS_ONLY_INHERIT; + ::BuildTrusteeWithSidW(&newAccess[0].Trustee, randomSid); + newAccess[1].grfAccessMode = DENY_ACCESS; + newAccess[1].grfAccessPermissions = GENERIC_WRITE; + newAccess[1].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT; + ::BuildTrusteeWithSidW(&newAccess[1].Trustee, randomSid); + UniquePtr<ACL, LocalFreeDeleter> newDacl; + ASSERT_EQ(::SetEntriesInAclW(std::size(newAccess), newAccess, pDirDacl, + getter_Transfers(newDacl)), + (ULONG)ERROR_SUCCESS); + + ASSERT_EQ(::SetNamedSecurityInfoW(dirPath.get(), SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, nullptr, nullptr, + newDacl.get(), nullptr), + (ULONG)ERROR_SUCCESS); +} #endif static already_AddRefed<nsIFile> NewFile(nsIFile* aBase) { @@ -284,6 +340,17 @@ static bool TestMove(nsIFile* aBase, nsIFile* aDestDir, const char* aName, return false; } +#ifdef XP_WIN + // Ensure ACE inheritance is correct. + auto [childPathStr, childDacl, childSecDesc] = + GetSecurityInfoStructured(file); + bool isDir = false; + EXPECT_NS_SUCCEEDED(file->IsDirectory(&isDir)); + EXPECT_TRUE(nsLocalFile::ChildAclMatchesAclInheritedFromParent( + childDacl, isDir, childSecDesc, aDestDir)) + << newName.get() << " ACL, does not match destination dir"; +#endif + return true; } @@ -532,6 +599,20 @@ static void SetupAndTestFunctions(const nsAString& aDirName, // Test moving across directories and renaming at the same time ASSERT_TRUE(TestMove(subdir, base, "file2.txt", "file4.txt")); +#ifdef XP_WIN + // On Windows if we move a file or directory to a directory on the same volume + // where the inherited ACLs differ between the source and target dirs, then we + // retain the ACEs from the original dir. Add inherited ACEs for random SIDs + // to subdir to ensure we reset the inheritance to just the target directory. + AddAcesForRandomSidToDir(subdir); + ASSERT_TRUE(TestCreate(subdir, "file8.txt", nsIFile::NORMAL_FILE_TYPE, 0600)); + ASSERT_TRUE(TestMove(subdir, base, "file8.txt", "file8.txt")); + + // Test that dir moves also get the ACL reset when required. + ASSERT_TRUE(TestCreate(subdir, "subdir2", nsIFile::DIRECTORY_TYPE, 0700)); + ASSERT_TRUE(TestMove(subdir, base, "subdir2", "subdir2")); +#endif + // Test copying across directories ASSERT_TRUE(TestCopy(base, subdir, "file4.txt", "file5.txt"));