Skip to content

Commit ac56256

Browse files
bobowenyjugl
authored andcommitted
Bug 1980886 p3 - Ensure no old inherited ACEs remain when moving files on Windows. r=yjuglaret
Differential Revision: https://phabricator.services.mozilla.com/D265007
1 parent 21c8e2c commit ac56256

File tree

4 files changed

+209
-29
lines changed

4 files changed

+209
-29
lines changed

xpcom/base/nsWindowsHelpers.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,19 @@ struct CloseHandleDeleter {
328328
}
329329
};
330330

331+
using AutoFreeSecurityDescriptor =
332+
mozilla::UniquePtr<SECURITY_DESCRIPTOR, LocalFreeDeleter>;
333+
334+
struct DestroyPrivateObjectSecurityDeleter {
335+
void operator()(PSECURITY_DESCRIPTOR aSecDescPtr) {
336+
::DestroyPrivateObjectSecurity(&aSecDescPtr);
337+
}
338+
};
339+
340+
using AutoDestroySecurityDescriptor =
341+
mozilla::UniquePtr<SECURITY_DESCRIPTOR,
342+
DestroyPrivateObjectSecurityDeleter>;
343+
331344
// One caller of this function is early in startup and several others are not,
332345
// so they have different ways of determining the two parameters. This function
333346
// exists just so any future code that needs to determine whether the dynamic

xpcom/io/nsLocalFileWin.cpp

Lines changed: 96 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
66

77
#include "mozilla/ArrayUtils.h"
8+
#include "mozilla/Assertions.h"
89
#include "mozilla/DebugOnly.h"
910
#include "mozilla/ProfilerLabels.h"
1011
#include "mozilla/TextUtils.h"
@@ -96,7 +97,6 @@ nsresult NewLocalFile(const nsAString& aPath, bool aUseDOSDevicePathSyntax,
9697
file.forget(aResult);
9798
return NS_OK;
9899
}
99-
100100
} // anonymous namespace
101101

102102
static HWND GetMostRecentNavigatorHWND() {
@@ -203,6 +203,73 @@ bool nsLocalFile::CheckForReservedFileName(const nsString& aFileName) {
203203
return false;
204204
}
205205

206+
/* static */
207+
bool nsLocalFile::ChildAclMatchesAclInheritedFromParent(
208+
const NotNull<ACL*> aChildDacl, bool aIsChildDir,
209+
const AutoFreeSecurityDescriptor& aChildSecDesc, nsIFile* aParentDir) {
210+
// If we fail at any point return false.
211+
ACL* parentDacl = nullptr;
212+
AutoFreeSecurityDescriptor parentSecDesc;
213+
nsAutoString parentPath;
214+
MOZ_ALWAYS_SUCCEEDS(aParentDir->GetTarget(parentPath));
215+
DWORD errCode = ::GetNamedSecurityInfoW(
216+
parentPath.getW(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr,
217+
nullptr, &parentDacl, nullptr, getter_Transfers(parentSecDesc));
218+
if (errCode != ERROR_SUCCESS || !parentDacl) {
219+
NS_ERROR(nsPrintfCString(
220+
"Failed to get parent dir DACL for comparison: %lx", errCode)
221+
.get());
222+
return false;
223+
}
224+
225+
// Create a new security descriptor with a DACL that just inherits from the
226+
// parent. We can then compare the current childs DACL to make sure the counts
227+
// of inherited ACEs match. We pass the child security descriptor as the
228+
// creator to get the owner and group information.
229+
AutoDestroySecurityDescriptor newSecDesc;
230+
GENERIC_MAPPING mapping;
231+
mapping.GenericRead = FILE_GENERIC_READ;
232+
mapping.GenericWrite = FILE_GENERIC_WRITE;
233+
mapping.GenericExecute = FILE_GENERIC_EXECUTE;
234+
mapping.GenericAll = FILE_ALL_ACCESS;
235+
if (!::CreatePrivateObjectSecurityEx(
236+
parentSecDesc.get(), aChildSecDesc.get(),
237+
getter_Transfers(newSecDesc), nullptr, aIsChildDir,
238+
SEF_DACL_AUTO_INHERIT | SEF_AVOID_OWNER_CHECK |
239+
SEF_AVOID_PRIVILEGE_CHECK,
240+
nullptr, &mapping)) {
241+
// There may be legitimate reasons for this to fail, so we might have to
242+
// remove this if it causes problems.
243+
NS_ERROR(nsPrintfCString(
244+
"Failed to create new inherited DACL for comparison: %lx",
245+
::GetLastError())
246+
.get());
247+
return false;
248+
}
249+
250+
BOOL daclPresent;
251+
ACL* newDacl = nullptr;
252+
BOOL daclDefaulted;
253+
if (!::GetSecurityDescriptorDacl(newSecDesc.get(), &daclPresent, &newDacl,
254+
&daclDefaulted) ||
255+
!daclPresent || !newDacl) {
256+
NS_ERROR(
257+
nsPrintfCString("Failed to get new DACL from security descriptor: %lx",
258+
::GetLastError())
259+
.get());
260+
return false;
261+
}
262+
263+
auto getInheritedAceCount = [](const ACL* aAcl) {
264+
AclAceRange aclAceRange(WrapNotNull(aAcl));
265+
return std::count_if(
266+
aclAceRange.begin(), aclAceRange.end(),
267+
[](const auto& hdr) { return hdr.AceFlags & INHERITED_ACE; });
268+
};
269+
270+
return getInheritedAceCount(aChildDacl) == getInheritedAceCount(newDacl);
271+
}
272+
206273
class nsDriveEnumerator : public nsSimpleEnumerator,
207274
public nsIDirectoryEnumerator {
208275
public:
@@ -1800,6 +1867,10 @@ nsresult nsLocalFile::MoveOrCopyAsSingleFileOrDir(nsIFile* aDestParent,
18001867
return NS_ERROR_FILE_ACCESS_DENIED;
18011868
}
18021869

1870+
// Determine if we are a directory before any move/copy.
1871+
bool isDir = false;
1872+
MOZ_ALWAYS_SUCCEEDS(IsDirectory(&isDir));
1873+
18031874
int copyOK = 0;
18041875
if (move) {
18051876
copyOK = ::MoveFileExW(filePath.get(), destPath.get(),
@@ -1850,31 +1921,30 @@ nsresult nsLocalFile::MoveOrCopyAsSingleFileOrDir(nsIFile* aDestParent,
18501921
} else if (move && !(aOptions & SkipNtfsAclReset)) {
18511922
// Set security permissions to inherit from parent.
18521923
// Note: propagates to all children: slow for big file trees
1853-
PACL pOldDACL = nullptr;
1854-
PSECURITY_DESCRIPTOR pSD = nullptr;
1855-
::GetNamedSecurityInfoW((LPWSTR)destPath.get(), SE_FILE_OBJECT,
1856-
DACL_SECURITY_INFORMATION, nullptr, nullptr,
1857-
&pOldDACL, nullptr, &pSD);
1858-
UniquePtr<VOID, LocalFreeDeleter> autoFreeSecDesc(pSD);
1859-
if (pOldDACL) {
1860-
// Test the current DACL, if we find one that is inherited then we can
1861-
// skip the reset. This avoids a request for SeTcbPrivilege, which can
1862-
// cause a lot of audit events if enabled (Bug 1816694).
1863-
bool inherited = false;
1864-
for (DWORD i = 0; i < pOldDACL->AceCount; ++i) {
1865-
VOID* pAce = nullptr;
1866-
if (::GetAce(pOldDACL, i, &pAce) &&
1867-
static_cast<PACE_HEADER>(pAce)->AceFlags & INHERITED_ACE) {
1868-
inherited = true;
1869-
break;
1870-
}
1871-
}
1872-
1873-
if (!inherited) {
1874-
::SetNamedSecurityInfoW(
1875-
(LPWSTR)destPath.get(), SE_FILE_OBJECT,
1876-
DACL_SECURITY_INFORMATION | UNPROTECTED_DACL_SECURITY_INFORMATION,
1877-
nullptr, nullptr, pOldDACL, nullptr);
1924+
ACL* childDacl = nullptr;
1925+
AutoFreeSecurityDescriptor childSecDesc;
1926+
// We need owner and group information for the parent ACL check.
1927+
DWORD errCode = ::GetNamedSecurityInfoW(
1928+
destPath.getW(), SE_FILE_OBJECT,
1929+
DACL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION |
1930+
GROUP_SECURITY_INFORMATION,
1931+
nullptr, nullptr, &childDacl, nullptr, getter_Transfers(childSecDesc));
1932+
if (errCode == ERROR_SUCCESS && childDacl) {
1933+
// Compare the number of inherited ACEs on the child to the number we
1934+
// expect from the parent. If they don't match then we reset. This is
1935+
// because we can get old inherited ACEs from the previous dir if moved
1936+
// within the same volume. We check this to prevent unnecessary calls to
1937+
// SetNamedSecurityInfoW, this avoids a request for SeTcbPrivilege, which
1938+
// can cause a lot of audit events if enabled (Bug 1816694).
1939+
if (!ChildAclMatchesAclInheritedFromParent(WrapNotNull(childDacl), isDir,
1940+
childSecDesc, aDestParent)) {
1941+
// We don't expect this to fail, but it shouldn't crash in release.
1942+
MOZ_ALWAYS_TRUE(
1943+
ERROR_SUCCESS ==
1944+
::SetNamedSecurityInfoW(destPath.get(), SE_FILE_OBJECT,
1945+
DACL_SECURITY_INFORMATION |
1946+
UNPROTECTED_DACL_SECURITY_INFORMATION,
1947+
nullptr, nullptr, childDacl, nullptr));
18781948
}
18791949
}
18801950
}

xpcom/io/nsLocalFileWin.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "nsIFile.h"
1515
#include "nsILocalFileWin.h"
1616
#include "nsIClassInfoImpl.h"
17+
#include "nsWindowsHelpers.h"
1718
#include "prio.h"
1819

1920
#include "mozilla/Attributes.h"
@@ -52,6 +53,21 @@ class nsLocalFile final : public nsILocalFileWin {
5253
// (com1, com2, etc...) and returns true if so.
5354
static bool CheckForReservedFileName(const nsString& aFileName);
5455

56+
/**
57+
* Checks whether the inherited ACEs in aChildDacl only come from the parent.
58+
*
59+
* @param aChildDacl the DACL for the child file object
60+
* @param aIsChildDir true if child is a directory
61+
* @param aChildSecDesc the SECURITY_DESCRIPTOR for the child, this will
62+
* correspond to aChildDacl
63+
* @param aParentDir the parent nsIFile
64+
* @return true if inherited ACEs are only from aParentDir
65+
* false if child has other inherited ACEs or a failure occurs
66+
*/
67+
static bool ChildAclMatchesAclInheritedFromParent(
68+
const mozilla::NotNull<ACL*> aChildDacl, bool aIsChildDir,
69+
const AutoFreeSecurityDescriptor& aChildSecDesc, nsIFile* aParentDir);
70+
5571
// PRFileInfo64 does not hvae an accessTime field;
5672
struct FileInfo {
5773
PRFileType type;

xpcom/tests/gtest/TestFile.cpp

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,25 @@
77
#include "prsystem.h"
88

99
#include "nsIFile.h"
10-
#ifdef XP_WIN
11-
# include "nsILocalFileWin.h"
12-
#endif
1310
#include "nsComponentManagerUtils.h"
1411
#include "nsString.h"
1512
#include "nsDirectoryServiceDefs.h"
1613
#include "nsDirectoryServiceUtils.h"
1714
#include "nsPrintfCString.h"
1815

16+
#ifdef XP_WIN
17+
# include <aclapi.h>
18+
# include "mozilla/RandomNum.h"
19+
# include "nsILocalFileWin.h"
20+
# include "nsLocalFile.h"
21+
# include "nsWindowsHelpers.h"
22+
#endif
23+
1924
#include "gtest/gtest.h"
2025
#include "mozilla/gtest/MozAssertions.h"
2126

2227
#ifdef XP_WIN
28+
using namespace mozilla;
2329
bool gTestWithPrefix_Win = false;
2430
#endif
2531

@@ -40,6 +46,56 @@ static void SetUseDOSDevicePathSyntax(nsIFile* aFile) {
4046
winFile->SetUseDOSDevicePathSyntax(true);
4147
}
4248
}
49+
50+
static auto GetSecurityInfoStructured(nsIFile* aFile) {
51+
nsAutoString pathStr;
52+
MOZ_RELEASE_ASSERT(NS_SUCCEEDED(aFile->GetTarget(pathStr)));
53+
54+
PACL pDacl = nullptr;
55+
AutoFreeSecurityDescriptor secDesc;
56+
DWORD errCode = ::GetNamedSecurityInfoW(
57+
pathStr.getW(), SE_FILE_OBJECT,
58+
DACL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION |
59+
GROUP_SECURITY_INFORMATION,
60+
nullptr, nullptr, &pDacl, nullptr, getter_Transfers(secDesc));
61+
MOZ_RELEASE_ASSERT(errCode == ERROR_SUCCESS && pDacl);
62+
63+
return std::make_tuple(std::move(pathStr), WrapNotNull(pDacl),
64+
std::move(secDesc));
65+
}
66+
67+
static void AddAcesForRandomSidToDir(nsIFile* aDir) {
68+
auto [dirPath, pDirDacl, secDesc] = GetSecurityInfoStructured(aDir);
69+
70+
constexpr BYTE kSubAuthorityCount = 4;
71+
BYTE randomSidBuffer[SECURITY_SID_SIZE(4)];
72+
ASSERT_TRUE(
73+
GenerateRandomBytesFromOS(randomSidBuffer, sizeof(randomSidBuffer)));
74+
auto* randomSid = reinterpret_cast<SID*>(randomSidBuffer);
75+
randomSid->Revision = SID_REVISION;
76+
randomSid->SubAuthorityCount = kSubAuthorityCount;
77+
randomSid->IdentifierAuthority = SECURITY_NULL_SID_AUTHORITY;
78+
ASSERT_TRUE(::IsValidSid(randomSid));
79+
80+
EXPLICIT_ACCESS_W newAccess[2];
81+
newAccess[0].grfAccessMode = GRANT_ACCESS;
82+
newAccess[0].grfAccessPermissions = GENERIC_READ;
83+
newAccess[0].grfInheritance = SUB_OBJECTS_ONLY_INHERIT;
84+
::BuildTrusteeWithSidW(&newAccess[0].Trustee, randomSid);
85+
newAccess[1].grfAccessMode = DENY_ACCESS;
86+
newAccess[1].grfAccessPermissions = GENERIC_WRITE;
87+
newAccess[1].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
88+
::BuildTrusteeWithSidW(&newAccess[1].Trustee, randomSid);
89+
UniquePtr<ACL, LocalFreeDeleter> newDacl;
90+
ASSERT_EQ(::SetEntriesInAclW(std::size(newAccess), newAccess, pDirDacl,
91+
getter_Transfers(newDacl)),
92+
(ULONG)ERROR_SUCCESS);
93+
94+
ASSERT_EQ(::SetNamedSecurityInfoW(dirPath.get(), SE_FILE_OBJECT,
95+
DACL_SECURITY_INFORMATION, nullptr, nullptr,
96+
newDacl.get(), nullptr),
97+
(ULONG)ERROR_SUCCESS);
98+
}
4399
#endif
44100

45101
static already_AddRefed<nsIFile> NewFile(nsIFile* aBase) {
@@ -284,6 +340,17 @@ static bool TestMove(nsIFile* aBase, nsIFile* aDestDir, const char* aName,
284340
return false;
285341
}
286342

343+
#ifdef XP_WIN
344+
// Ensure ACE inheritance is correct.
345+
auto [childPathStr, childDacl, childSecDesc] =
346+
GetSecurityInfoStructured(file);
347+
bool isDir = false;
348+
EXPECT_NS_SUCCEEDED(file->IsDirectory(&isDir));
349+
EXPECT_TRUE(nsLocalFile::ChildAclMatchesAclInheritedFromParent(
350+
childDacl, isDir, childSecDesc, aDestDir))
351+
<< newName.get() << " ACL, does not match destination dir";
352+
#endif
353+
287354
return true;
288355
}
289356

@@ -532,6 +599,20 @@ static void SetupAndTestFunctions(const nsAString& aDirName,
532599
// Test moving across directories and renaming at the same time
533600
ASSERT_TRUE(TestMove(subdir, base, "file2.txt", "file4.txt"));
534601

602+
#ifdef XP_WIN
603+
// On Windows if we move a file or directory to a directory on the same volume
604+
// where the inherited ACLs differ between the source and target dirs, then we
605+
// retain the ACEs from the original dir. Add inherited ACEs for random SIDs
606+
// to subdir to ensure we reset the inheritance to just the target directory.
607+
AddAcesForRandomSidToDir(subdir);
608+
ASSERT_TRUE(TestCreate(subdir, "file8.txt", nsIFile::NORMAL_FILE_TYPE, 0600));
609+
ASSERT_TRUE(TestMove(subdir, base, "file8.txt", "file8.txt"));
610+
611+
// Test that dir moves also get the ACL reset when required.
612+
ASSERT_TRUE(TestCreate(subdir, "subdir2", nsIFile::DIRECTORY_TYPE, 0700));
613+
ASSERT_TRUE(TestMove(subdir, base, "subdir2", "subdir2"));
614+
#endif
615+
535616
// Test copying across directories
536617
ASSERT_TRUE(TestCopy(base, subdir, "file4.txt", "file5.txt"));
537618

0 commit comments

Comments
 (0)