fix(windows): honor directory readonly semantics during cleanup

This commit is contained in:
morluto committed 2026-10-15 08:16:22 +08:00
1 parent fa318995f5
commit 1a3a96d36b
2 files changed
+46 -10

No files matched your search

+4 -1
View File
@@ -439,7 +439,10 @@ static void writeBytes(HANDLE file, const void* bytes, DWORD count, const std::w
static void dispose(HANDLE handle, const std::wstring& path) {
FILE_BASIC_INFO info{};
require(GetFileInformationByHandleEx(handle, FileBasicInfo, &info, sizeof(info)), "Read cleanup attributes failed", path);
if ((info.FileAttributes & FILE_ATTRIBUTE_READONLY) != 0) {
// READONLY prevents file deletion but is not honored on directories. Clearing
// it on a directory would need attribute-write rights cleanup does not need.
if ((info.FileAttributes & FILE_ATTRIBUTE_READONLY) != 0 &&
(info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) {
info.FileAttributes &= ~FILE_ATTRIBUTE_READONLY;
require(SetFileInformationByHandle(handle, FileBasicInfo, &info, sizeof(info)), "Clear owned readonly attribute failed", path);
}
+42 -9
View File
@@ -48,6 +48,13 @@ const { PrivateRuntimeRootUnavailableError } = await import(
)
);
const workspace = await mkdtemp(join(tmpdir(), "rea-native-conformance-"));
// A pwsh caller can supply module paths incompatible with Windows PowerShell.
// Each independent observer must discover its own module path.
const windowsPowerShellEnvironment = Object.fromEntries(
Object.entries(process.env).filter(
([name]) => name.toLowerCase() !== "psmodulepath",
),
);
const environment = Object.entries(process.env)
.filter(([, value]) => value !== undefined)
.map(([key, value]) => `${key}=${value}`);
@@ -317,18 +324,13 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
"-NoProfile",
"-NonInteractive",
"-Command",
"$a=Get-Acl -LiteralPath $env:REA_TEST_PRIVATE_RUNTIME_PATH; $u=[Security.Principal.WindowsIdentity]::GetCurrent().User; @{protected=$a.AreAccessRulesProtected; ownerMatchesUser=($a.GetOwner([Security.Principal.SecurityIdentifier]).Value -eq $u.Value); trusteesOnlyUserAndSystem=(@($a.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier]) | Where-Object {$_.IdentityReference.Value -ne $u.Value -and $_.IdentityReference.Value -ne 'S-1-5-18'}).Count -eq 0); count=@($a.Access).Count} | ConvertTo-Json -Compress",
"$ErrorActionPreference='Stop'; $p=$env:REA_TEST_PRIVATE_RUNTIME_PATH; $a=Get-Acl -LiteralPath $p; $u=[Security.Principal.WindowsIdentity]::GetCurrent().User; [IO.File]::SetAttributes($p,[IO.FileAttributes]([int][IO.File]::GetAttributes($p) -bor 1)); @{protected=$a.AreAccessRulesProtected; ownerMatchesUser=($a.GetOwner([Security.Principal.SecurityIdentifier]).Value -eq $u.Value); trusteesOnlyUserAndSystem=(@($a.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier]) | Where-Object {$_.IdentityReference.Value -ne $u.Value -and $_.IdentityReference.Value -ne 'S-1-5-18'}).Count -eq 0); count=@($a.Access).Count; rootReadonly=(([int][IO.File]::GetAttributes($p) -band 1) -ne 0)} | ConvertTo-Json -Compress",
],
{
encoding: "utf8",
// A pwsh caller can supply module paths incompatible with the
// Windows PowerShell observer. Let that observer build its own path.
timeout: 15_000,
env: {
...Object.fromEntries(
Object.entries(process.env).filter(
([name]) => name.toLowerCase() !== "psmodulepath",
),
),
...windowsPowerShellEnvironment,
REA_TEST_PRIVATE_RUNTIME_PATH: runtime.path,
},
},
@@ -339,6 +341,7 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
ownerMatchesUser: true,
trusteesOnlyUserAndSystem: true,
count: 2,
rootReadonly: true,
});
native.call("runtime_mkdir", [runtime.handle, "inputs"]);
native.call("runtime_write", [
@@ -484,7 +487,10 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
{
encoding: "utf8",
timeout: 15_000,
env: { ...process.env, REA_SNAPSHOT_READER_PATH: snapshotPath },
env: {
...windowsPowerShellEnvironment,
REA_SNAPSHOT_READER_PATH: snapshotPath,
},
},
).trim();
assert.equal(readerDigest, readbackSnapshot.sha256);
@@ -574,12 +580,39 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
asynchronousSnapshot: true,
snapshotCancellation: true,
};
const readonlyChildren = JSON.parse(
execFileSync(
"powershell.exe",
[
"-NoProfile",
"-NonInteractive",
"-Command",
"$ErrorActionPreference='Stop'; $items=@($env:REA_TEST_READONLY_CHILDREN | ConvertFrom-Json); @($items | ForEach-Object { [IO.File]::SetAttributes($_,[IO.FileAttributes]([int][IO.File]::GetAttributes($_) -bor 1)); $a=[int][IO.File]::GetAttributes($_); @{readonly=(($a -band 1) -ne 0); directory=(($a -band 16) -ne 0)} }) | ConvertTo-Json -Compress",
],
{
encoding: "utf8",
timeout: 15_000,
env: {
...windowsPowerShellEnvironment,
REA_TEST_READONLY_CHILDREN: JSON.stringify([
join(runtime.path, "inputs"),
join(runtime.path, "inputs", "target.bin"),
]),
},
},
),
);
assert.deepEqual(readonlyChildren, [
{ readonly: true, directory: true },
{ readonly: true, directory: false },
]);
} finally {
native.call("runtime_close", [runtime.handle]);
}
assert.equal(await exists(runtime.path), false);
assert.equal(await readFile(sentinel, "utf8"), "preserve");
report.controls.cleanupDoesNotTraverseJunctions = true;
report.controls.readonlyRootAndChildrenRemoved = true;
const fixture = join(workspace, "tree.mjs");
await writeFile(
fixture,