diff --git a/native/windows/src/filesystem.cc b/native/windows/src/filesystem.cc index d60e28ae94..0544286e23 100644 --- a/native/windows/src/filesystem.cc +++ b/native/windows/src/filesystem.cc @@ -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); } diff --git a/scripts/verify-windows-native.mjs b/scripts/verify-windows-native.mjs index 4cabb7b0fa..6e405cba54 100644 --- a/scripts/verify-windows-native.mjs +++ b/scripts/verify-windows-native.mjs @@ -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,