diff --git a/native/windows/src/filesystem.cc b/native/windows/src/filesystem.cc index e5129dbb79..d60e28ae94 100644 --- a/native/windows/src/filesystem.cc +++ b/native/windows/src/filesystem.cc @@ -67,7 +67,10 @@ std::unique_ptr openFile(const std::wstring& requested, DWORD access, bool result->requestedPath = requested; const auto& path = result->path; require(directory || path.size() > 3, "Target is not a regular file", requested, ERROR_DIRECTORY); - result->handles.push_back(openComponent(path.substr(0, 3), 0, true)); + // The volume root cannot be replaced or deleted. Match the later root + // reopen's write sharing here so a root writer cannot block admission. + result->handles.push_back(openComponent(path.substr(0, 3), 0, true, + FILE_SHARE_READ | FILE_SHARE_WRITE)); wchar_t filesystem[64]{}; DWORD flags = 0; require(GetVolumeInformationByHandleW(result->handles.front().get(), nullptr, 0, nullptr, nullptr, diff --git a/scripts/lib/windows-volume-root-append-lease.mjs b/scripts/lib/windows-volume-root-append-lease.mjs new file mode 100644 index 0000000000..a0253f70ae --- /dev/null +++ b/scripts/lib/windows-volume-root-append-lease.mjs @@ -0,0 +1,118 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { once } from "node:events"; +import { createInterface } from "node:readline"; +import { resolve } from "node:path"; + +const withDeadline = async (promise, milliseconds, description) => { + let timer; + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout( + () => + reject(new Error(`${description} exceeded ${milliseconds} ms.`)), + milliseconds, + ); + }), + ]); + } finally { + clearTimeout(timer); + } +}; + +export async function verifyVolumeRootAppendLease({ + fixturePath, + volumeRoot, + verifyAdmission, +}) { + const lease = spawn( + resolve(fixturePath), + ["volume-root-append-lease", volumeRoot], + { stdio: ["pipe", "pipe", "pipe"], windowsHide: true }, + ); + const lines = createInterface({ input: lease.stdout }); + let leaseStderr = ""; + lease.stderr.setEncoding("utf8"); + lease.stderr.on("data", (chunk) => { + if (leaseStderr.length < 8192) leaseStderr += chunk; + }); + + const closed = once(lease, "close").then(([code, signal]) => ({ + code, + signal, + })); + const leaseLine = once(lines, "line").then(([line]) => ({ line })); + const leaseError = once(lease, "error").then(([error]) => { + throw error; + }); + let verificationFailure; + try { + const ready = await withDeadline( + Promise.race([leaseLine, closed, leaseError]), + 10_000, + "Volume-root append lease readiness", + ); + assert.equal( + "line" in ready, + true, + `Volume-root append lease exited before readiness (code ${ready.code}, signal ${ready.signal}): ${leaseStderr}`, + ); + const observation = JSON.parse(ready.line); + assert.equal( + observation.writeAccessGranted, + true, + `Windows test token cannot hold FILE_ADD_SUBDIRECTORY on ${volumeRoot} (Win32 ${observation.win32Code}).`, + ); + await verifyAdmission(); + } catch (error) { + verificationFailure = error; + } + + lines.close(); + if ( + lease.exitCode === null && + lease.signalCode === null && + !lease.stdin.destroyed + ) + lease.stdin.end("x"); + + let releaseFailure; + try { + const result = await withDeadline( + closed, + 10_000, + "Volume-root append lease release", + ).catch(async (error) => { + lease.kill(); + try { + return await withDeadline( + closed, + 5_000, + "Volume-root append lease termination", + ); + } catch (terminationError) { + throw new AggregateError( + [error, terminationError], + `Volume-root append lease could not be released: ${leaseStderr}`, + ); + } + }); + assert.equal( + result.code, + 0, + `Volume-root append lease exited with code ${result.code}, signal ${result.signal}: ${leaseStderr}`, + ); + } catch (error) { + releaseFailure = error; + } + + if (verificationFailure !== undefined && releaseFailure !== undefined) + throw new AggregateError( + [verificationFailure, releaseFailure], + "Volume-root append lease verification and cleanup both failed.", + ); + if (verificationFailure !== undefined) throw verificationFailure; + if (releaseFailure !== undefined) throw releaseFailure; +} diff --git a/scripts/verify-windows-native.mjs b/scripts/verify-windows-native.mjs index fb4226bf49..4cabb7b0fa 100644 --- a/scripts/verify-windows-native.mjs +++ b/scripts/verify-windows-native.mjs @@ -22,6 +22,7 @@ import { dirname, join, parse, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { verifyWindowsProtocolInput } from "./lib/windows-protocol-input.mjs"; import { verifyWindowsJobMemoryLimit } from "./lib/windows-job-memory-limit.mjs"; +import { verifyVolumeRootAppendLease } from "./lib/windows-volume-root-append-lease.mjs"; if (process.platform !== "win32") throw new Error( @@ -251,6 +252,17 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe } const unrelatedFile = join(workspace, "unrelated-file"); await writeFile(unrelatedFile, "preserve unrelated writes"); + if (process.argv[3] !== undefined) { + await verifyVolumeRootAppendLease({ + fixturePath: process.argv[3], + volumeRoot: parse(source).root, + verifyAdmission: () => { + const rootGuard = native.call("open", [source]); + native.call("close", [rootGuard.handle]); + }, + }); + report.controls.volumeRootAdmissionWithAppendHandle = true; + } const admitted = native.call("open", [source]); try { await assert.rejects( diff --git a/tests/fixtures/windows/processBoundary.cc b/tests/fixtures/windows/processBoundary.cc index 05ee218e81..acc958b67b 100644 --- a/tests/fixtures/windows/processBoundary.cc +++ b/tests/fixtures/windows/processBoundary.cc @@ -97,6 +97,23 @@ int wmain(int count, wchar_t** args) { ? "true" : "false", observed); return 0; } + if (count == 3 && std::wstring(args[1]) == L"volume-root-append-lease") { + // A real directory append right participates in Windows' write-share + // check. The lease performs no mutation; it stays open while the native + // admission probe requests the same volume root for directory listing. + HANDLE root = CreateFileW(args[2], FILE_ADD_SUBDIRECTORY, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (root == INVALID_HANDLE_VALUE) { + std::printf("{\"writeAccessGranted\":false,\"win32Code\":%lu}\n", GetLastError()); + return 0; + } + std::puts("{\"writeAccessGranted\":true}"); + std::fflush(stdout); + const int release = std::getchar(); + CloseHandle(root); + return release == 'x' || release == EOF ? 0 : 10; + } if (count > 1 && std::wstring(args[1]) == L"breakaway") { wchar_t executable[32768]; GetModuleFileNameW(nullptr, executable, 32768); std::wstring line = L"\"" + std::wstring(executable) + L"\" exit";