* fix(windows): preserve caller affinity only within one processor group `spawnProcess` read the caller's affinity mask and applied it to the suspended child unconditionally. A mask names processors relative to one processor group, and Windows assigns each new process to a group of its own choosing, so on hosts with more than 64 logical processors the child is often placed in another group. `SetProcessAffinityMask` then rejects the mask with ERROR_INVALID_PARAMETER (87) and the spawn fails, making every owned provider process start intermittently on such hosts. Affinity is a nicety, not an authority or isolation control, so it is now applied only when the caller and the suspended child both occupy the same single processor group. A child placed in another group keeps that group's default affinity and the spawn proceeds. Job Object membership, kill-on-owner-close policy, owner normalization and priority preservation are unchanged, and the mask failure remains fatal within a shared group, where it is not expected. Single-group hosts, including every host with 64 or fewer logical processors, are unaffected: `GetProcessGroupAffinity` reports one group for both processes and the same mask is applied as before. The optional process-boundary fixture gains an `affinity` observation, and the native verification lane asserts that an owned spawn succeeds and that the child's mask matches the caller's whenever both share a single group. Fixes #2181 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(windows): refuse to widen a pinned caller's affinity across groups Addresses review feedback on the first commit. An unconstrained caller loses nothing when Windows places an owned child in another processor group, so that spawn proceeds with the group's default. A caller deliberately pinned to fewer processors than its group holds is a different case: silently accepting the default would hand the provider tree the whole group, which `docs/installation.md` tells operators to rely on affinity to prevent. Such a spawn now fails naming the constraint instead. `GetProcessAffinityMask` already reports the group's active set, which distinguishes the two without another query. Selecting the caller's group at creation through PROC_THREAD_ATTRIBUTE_GROUP_AFFINITY was tried first and rejected: `CreateProcessW` fails with ERROR_INVALID_PARAMETER on a host with a single processor group, both alongside PROC_THREAD_ATTRIBUTE_JOB_LIST and on its own, so it cannot carry the ordinary path. The boundary fixture's affinity observation now reports a process id's own mask rather than its own, because a separate probe process is assigned its own group and cannot stand in for the process that spawns the child. Masks and the group's active set cross as fixed-width hexadecimal strings: a mask can set bits above the safe-integer range, where `JSON.parse` would round two different masks to one value. The verification lane asserts the mask is inherited exactly when caller and child share a group, and otherwise that the caller was unconstrained. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(windows): record what owned-process affinity does not preserve A mask covering its group's whole active set does not distinguish a caller the system placed in that group from one that selected the group deliberately, so an owned child can still be placed elsewhere with its processor count bounded and its group placement lost. Treating that case as constrained instead is not available: on a multi-group host an ordinary process is assigned a group by the system and its mask is that group's active set, so refusing it would fail every owned spawn there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
758 lines
27 KiB
JavaScript
758 lines
27 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import assert from "node:assert/strict";
|
|
import { execFileSync, spawn } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { once } from "node:events";
|
|
import {
|
|
access,
|
|
copyFile,
|
|
mkdir,
|
|
mkdtemp,
|
|
readFile,
|
|
rename,
|
|
readdir,
|
|
rm,
|
|
symlink,
|
|
writeFile,
|
|
open,
|
|
} from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
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";
|
|
|
|
if (process.platform !== "win32")
|
|
throw new Error(
|
|
"Windows native conformance requires a real Windows x64 host.",
|
|
);
|
|
const packageRoot = resolve(process.argv[2] ?? ".");
|
|
const { requireWindowsNativeAuthority } = await import(
|
|
pathToFileURL(join(packageRoot, "dist/windows/WindowsNativeLoader.js"))
|
|
);
|
|
const native = requireWindowsNativeAuthority();
|
|
const { WindowsPrivateRuntime, windowsPrivateRuntime } = await import(
|
|
pathToFileURL(join(packageRoot, "dist/windows/WindowsPrivateRuntime.js"))
|
|
);
|
|
const { ghidraSessionRoot } = await import(
|
|
pathToFileURL(join(packageRoot, "dist/ghidra/GhidraSessionRoot.js"))
|
|
);
|
|
const { PrivateRuntimeRoot } = await import(
|
|
pathToFileURL(join(packageRoot, "dist/process/PrivateRuntimeRoot.js"))
|
|
);
|
|
const { PrivateRuntimeRootUnavailableError } = await import(
|
|
pathToFileURL(
|
|
join(packageRoot, "dist/process/PrivateRuntimeRootUnavailableError.js"),
|
|
)
|
|
);
|
|
const workspace = await mkdtemp(join(tmpdir(), "rea-native-conformance-"));
|
|
const environment = Object.entries(process.env)
|
|
.filter(([, value]) => value !== undefined)
|
|
.map(([key, value]) => `${key}=${value}`);
|
|
const quote = (value) =>
|
|
`"${value.replace(/(\\*)"/gu, '$1$1\\"').replace(/(\\+)$/u, "$1$1")}"`;
|
|
const delay = () => new Promise((resolve) => setTimeout(resolve, 25));
|
|
const exists = (path) =>
|
|
access(path).then(
|
|
() => true,
|
|
() => false,
|
|
);
|
|
const live = (pid) => {
|
|
try {
|
|
process.kill(pid, 0);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
const launch = (arguments_) =>
|
|
native.call("process_spawn", [
|
|
process.execPath,
|
|
[process.execPath, ...arguments_].map(quote).join(" "),
|
|
workspace,
|
|
environment,
|
|
]);
|
|
const wait = async (child) => {
|
|
let output = "";
|
|
for (let attempt = 0; attempt < 400; attempt++) {
|
|
const state = native.call("process_poll", [child.handle]);
|
|
output += state.stdout.toString();
|
|
if (
|
|
state.exitCode !== null &&
|
|
state.activeProcesses === 0 &&
|
|
state.stdoutEnded &&
|
|
state.stderrEnded
|
|
)
|
|
return { ...state, output };
|
|
await delay();
|
|
}
|
|
throw new Error("Owned job did not settle within the fixture deadline.");
|
|
};
|
|
const report = { ok: false, artifact: "verified", controls: {}, processes: {} };
|
|
try {
|
|
// Exercise the loader in a process whose image is not named node.exe. The
|
|
// ordinary Windows lane otherwise cannot detect eager Node-API imports.
|
|
const renamedHost = join(workspace, "rea-renamed-node.exe");
|
|
const renamedHostFixture = join(workspace, "renamed-host.mjs");
|
|
const loaderUrl = pathToFileURL(
|
|
join(packageRoot, "dist/windows/WindowsNativeLoader.js"),
|
|
).href;
|
|
await copyFile(process.execPath, renamedHost);
|
|
await writeFile(
|
|
renamedHostFixture,
|
|
`import {basename} from 'node:path';
|
|
import {requireWindowsNativeAuthority} from ${JSON.stringify(loaderUrl)};
|
|
const {inspection}=requireWindowsNativeAuthority();
|
|
process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVersion:inspection.abiVersion}));\n`,
|
|
);
|
|
const renamedHostResult = execFileSync(renamedHost, [renamedHostFixture], {
|
|
encoding: "utf8",
|
|
timeout: 20_000,
|
|
windowsHide: true,
|
|
});
|
|
assert.deepEqual(JSON.parse(renamedHostResult), {
|
|
executable: "rea-renamed-node.exe",
|
|
abiVersion: 1,
|
|
});
|
|
report.controls.renamedHostNodeApi = true;
|
|
const sourceDirectory = join(workspace, "source");
|
|
await mkdir(sourceDirectory);
|
|
const source = join(sourceDirectory, "target.bin");
|
|
await writeFile(source, Buffer.alloc(1024 * 1024, 0x37));
|
|
const sourceIdentity = native.call("open", [source]);
|
|
try {
|
|
for (const requested of [
|
|
source.replaceAll("\\", "/"),
|
|
source.replace(/\\/u, "/"),
|
|
]) {
|
|
const admitted = native.call("open", [requested]);
|
|
try {
|
|
assert.equal(admitted.requestedPath, requested);
|
|
assert.equal(admitted.finalPath, sourceIdentity.finalPath);
|
|
assert.equal(admitted.fileId, sourceIdentity.fileId);
|
|
assert.equal(native.call("read", [admitted.handle, 0, 1])[0], 0x37);
|
|
} finally {
|
|
native.call("close", [admitted.handle]);
|
|
}
|
|
}
|
|
} finally {
|
|
native.call("close", [sourceIdentity.handle]);
|
|
}
|
|
for (const rejected of [
|
|
source.replace(/^([A-Za-z]):\\/u, "$1:"),
|
|
`${sourceDirectory}/../source/target.bin`,
|
|
`${sourceDirectory}//target.bin`,
|
|
`${source}:stream`,
|
|
`\\\\?\\${source.replaceAll("\\", "/")}`,
|
|
"//localhost/share/target.bin",
|
|
])
|
|
assert.throws(() => native.call("open", [rejected]));
|
|
report.controls.ordinaryDriveSeparatorsAndRequestedIdentity = true;
|
|
// An owned runtime's DELETE lease conflicts with a fresh parent admission.
|
|
// This produces a genuine sharing failure after successful authority caching,
|
|
// without another executable, and proves that no child runtime was created.
|
|
const sharingParent = WindowsPrivateRuntime.create(
|
|
workspace,
|
|
"rea-sharing-parent-",
|
|
);
|
|
try {
|
|
await assert.rejects(
|
|
PrivateRuntimeRoot.create({ parent: sharingParent.observation.path }),
|
|
(failure) => {
|
|
assert.ok(failure instanceof PrivateRuntimeRootUnavailableError);
|
|
assert.equal(failure.retryable, true);
|
|
assert.ok(failure.cause instanceof Error);
|
|
assert.equal(failure.cause.win32Code, 32);
|
|
assert.equal(failure.cause.runtimeAllocation, "not-started");
|
|
return true;
|
|
},
|
|
);
|
|
assert.deepEqual(await readdir(sharingParent.observation.path), []);
|
|
report.controls.transientRuntimeAdmissionBeforeAllocation = true;
|
|
} finally {
|
|
await sharingParent.close();
|
|
}
|
|
const driveRoot = parse(workspace).root;
|
|
const runtimeParent = ghidraSessionRoot({
|
|
base: join(driveRoot, ".Tmp", "scratch"),
|
|
});
|
|
assert.equal(runtimeParent, driveRoot);
|
|
const driveRootRuntime = native.call("runtime_create", [
|
|
runtimeParent,
|
|
"rea-drive-root-",
|
|
]);
|
|
try {
|
|
assert.equal(driveRootRuntime.path.startsWith(`${driveRoot}\\`), false);
|
|
assert.equal(dirname(driveRootRuntime.path), driveRoot);
|
|
const snapshot = await native.call("runtime_snapshot", [
|
|
driveRootRuntime.handle,
|
|
source,
|
|
"probe.bin",
|
|
]);
|
|
const sourceBytes = await readFile(source);
|
|
assert.equal(
|
|
snapshot.sha256,
|
|
createHash("sha256").update(sourceBytes).digest("hex"),
|
|
);
|
|
assert.equal(snapshot.source.requestedPath, source);
|
|
const readback = native.call("runtime_open", [
|
|
driveRootRuntime.handle,
|
|
"probe.bin",
|
|
]);
|
|
try {
|
|
assert.equal(readback.size, sourceBytes.byteLength);
|
|
assert.deepEqual(
|
|
native.call("read", [readback.handle, 0, 65536]),
|
|
sourceBytes.subarray(0, 65536),
|
|
);
|
|
} finally {
|
|
native.call("close", [readback.handle]);
|
|
}
|
|
} finally {
|
|
native.call("runtime_close", [driveRootRuntime.handle]);
|
|
}
|
|
assert.equal(await exists(driveRootRuntime.path), false);
|
|
report.controls.driveRootRuntimeParent = true;
|
|
const snapshotOwner = WindowsPrivateRuntime.create(
|
|
workspace.replace(/\\/u, "/"),
|
|
"single-flight-",
|
|
);
|
|
try {
|
|
const rejectedController = new AbortController();
|
|
const requestedSource = source.replaceAll("\\", "/");
|
|
const accepted = snapshotOwner.snapshot(requestedSource, "accepted.bin");
|
|
const rejected = snapshotOwner.snapshot(
|
|
source,
|
|
"rejected.bin",
|
|
rejectedController.signal,
|
|
);
|
|
rejectedController.abort();
|
|
await assert.rejects(rejected, /Runtime snapshot is still pending/u);
|
|
const completed = await accepted;
|
|
assert.equal(completed.source.requestedPath, requestedSource);
|
|
assert.equal(
|
|
completed.sha256,
|
|
createHash("sha256")
|
|
.update(await readFile(source))
|
|
.digest("hex"),
|
|
);
|
|
assert.equal(
|
|
await exists(join(snapshotOwner.observation.path, "rejected.bin")),
|
|
false,
|
|
);
|
|
report.controls.singleFlightCancellationOwnership = true;
|
|
} finally {
|
|
await snapshotOwner.close();
|
|
}
|
|
const unrelatedFile = join(workspace, "unrelated-file");
|
|
await writeFile(unrelatedFile, "preserve unrelated writes");
|
|
const admitted = native.call("open", [source]);
|
|
try {
|
|
await assert.rejects(
|
|
rename(sourceDirectory, `${sourceDirectory}-replacement`),
|
|
);
|
|
await assert.rejects(open(source, "r+"));
|
|
await rename(unrelatedFile, `${unrelatedFile}-renamed`);
|
|
if (process.argv[3] !== undefined) {
|
|
const observation = JSON.parse(
|
|
execFileSync(
|
|
resolve(process.argv[3]),
|
|
["reparse-directory", sourceDirectory, workspace],
|
|
{ encoding: "utf8" },
|
|
),
|
|
);
|
|
assert.equal(
|
|
observation.reparseConversionDenied,
|
|
true,
|
|
"An admitted ancestor was converted into a junction.",
|
|
);
|
|
report.controls.inPlaceAncestorReparseDenied = true;
|
|
}
|
|
assert.throws(() => native.call("read", [{}, 0, 1]));
|
|
assert.throws(() => native.call("read", [admitted.handle, NaN, 1]));
|
|
assert.equal(native.call("read", [admitted.handle, 0, 1])[0], 0x37);
|
|
} finally {
|
|
native.call("close", [admitted.handle]);
|
|
}
|
|
assert.throws(() => native.call("read", [admitted.handle, 0, 1]));
|
|
const junction = join(workspace, "junction");
|
|
await symlink(sourceDirectory, junction, "junction");
|
|
assert.throws(
|
|
() => native.call("open", [join(junction, "target.bin")]),
|
|
/Reparse/u,
|
|
);
|
|
assert.throws(() => native.call("open", [source + ":stream"]));
|
|
const runtime = native.call("runtime_create", [
|
|
workspace.replaceAll("\\", "/"),
|
|
"private root with spaces-",
|
|
]);
|
|
const sentinel = join(sourceDirectory, "sentinel");
|
|
await writeFile(sentinel, "preserve");
|
|
try {
|
|
const acl = JSON.parse(
|
|
execFileSync(
|
|
"powershell.exe",
|
|
[
|
|
"-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",
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
// A pwsh caller can supply module paths incompatible with the
|
|
// Windows PowerShell observer. Let that observer build its own path.
|
|
env: {
|
|
...Object.fromEntries(
|
|
Object.entries(process.env).filter(
|
|
([name]) => name.toLowerCase() !== "psmodulepath",
|
|
),
|
|
),
|
|
REA_TEST_PRIVATE_RUNTIME_PATH: runtime.path,
|
|
},
|
|
},
|
|
),
|
|
);
|
|
assert.deepEqual(acl, {
|
|
protected: true,
|
|
ownerMatchesUser: true,
|
|
trusteesOnlyUserAndSystem: true,
|
|
count: 2,
|
|
});
|
|
native.call("runtime_mkdir", [runtime.handle, "inputs"]);
|
|
native.call("runtime_write", [
|
|
runtime.handle,
|
|
"descriptor.json",
|
|
Buffer.from("fixture"),
|
|
]);
|
|
assert.throws(() =>
|
|
native.call("runtime_write", [
|
|
runtime.handle,
|
|
"descriptor.json",
|
|
Buffer.from("replace"),
|
|
]),
|
|
);
|
|
assert.throws(() =>
|
|
native.call("runtime_mkdir", [runtime.handle, "..\\escape"]),
|
|
);
|
|
const pending = native.call("runtime_snapshot", [
|
|
runtime.handle,
|
|
source,
|
|
"inputs\\target.bin",
|
|
]);
|
|
assert.ok(pending instanceof Promise);
|
|
assert.throws(
|
|
() => native.call("runtime_close", [runtime.handle]),
|
|
/pending/u,
|
|
);
|
|
const snapshot = await pending;
|
|
assert.equal(
|
|
snapshot.sha256,
|
|
createHash("sha256")
|
|
.update(await readFile(source))
|
|
.digest("hex"),
|
|
);
|
|
assert.equal(snapshot.source.requestedPath, source);
|
|
await assert.rejects(
|
|
writeFile(join(runtime.path, "inputs", "target.bin"), "replace"),
|
|
);
|
|
await symlink(
|
|
sourceDirectory,
|
|
join(runtime.path, "outside-junction"),
|
|
"junction",
|
|
);
|
|
const cancelled = native.call("runtime_snapshot", [
|
|
runtime.handle,
|
|
source,
|
|
"cancelled.bin",
|
|
]);
|
|
native.call("runtime_snapshot_cancel", [runtime.handle]);
|
|
await assert.rejects(cancelled, /cancelled/u);
|
|
assert.equal(await exists(join(runtime.path, "cancelled.bin")), false);
|
|
const readbackRuntime = WindowsPrivateRuntime.create(
|
|
workspace.replaceAll("\\", "/"),
|
|
"readback-",
|
|
);
|
|
try {
|
|
readbackRuntime.writeFile("written.txt", "runtime-write-readback");
|
|
assert.equal(
|
|
readbackRuntime.readFile("written.txt"),
|
|
"runtime-write-readback",
|
|
);
|
|
const readbackSnapshot = await readbackRuntime.snapshot(
|
|
source,
|
|
"snapshot.bin",
|
|
);
|
|
assert.equal(
|
|
readbackSnapshot.sha256,
|
|
createHash("sha256")
|
|
.update(await readFile(source))
|
|
.digest("hex"),
|
|
);
|
|
assert.equal(
|
|
readbackRuntime.readFile("snapshot.bin"),
|
|
await readFile(source, "utf8"),
|
|
);
|
|
const writtenPath = join(readbackRuntime.observation.path, "written.txt");
|
|
await assert.rejects(writeFile(writtenPath, "replace"));
|
|
await assert.rejects(rename(writtenPath, `${writtenPath}.renamed`));
|
|
await assert.rejects(rm(writtenPath));
|
|
assert.equal(
|
|
readbackRuntime.readFile("written.txt"),
|
|
"runtime-write-readback",
|
|
);
|
|
const snapshotPath = join(
|
|
readbackRuntime.observation.path,
|
|
"snapshot.bin",
|
|
);
|
|
// Java RandomAccessFile uses read/write sharing without delete sharing.
|
|
// Exercise that independent Win32 reader contract without requiring Java.
|
|
const readerDigest = execFileSync(
|
|
"powershell.exe",
|
|
[
|
|
"-NoProfile",
|
|
"-NonInteractive",
|
|
"-Command",
|
|
"$ErrorActionPreference = 'Stop'; " +
|
|
"$stream = [IO.File]::Open($env:REA_SNAPSHOT_READER_PATH, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite); " +
|
|
"$hash = [Security.Cryptography.SHA256]::Create(); " +
|
|
"try { [BitConverter]::ToString($hash.ComputeHash($stream)).Replace('-', '').ToLowerInvariant() } " +
|
|
"finally { $hash.Dispose(); $stream.Dispose() }",
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
timeout: 15_000,
|
|
env: { ...process.env, REA_SNAPSHOT_READER_PATH: snapshotPath },
|
|
},
|
|
).trim();
|
|
assert.equal(readerDigest, readbackSnapshot.sha256);
|
|
await assert.rejects(writeFile(snapshotPath, "replace"));
|
|
await assert.rejects(rename(snapshotPath, `${snapshotPath}.renamed`));
|
|
await assert.rejects(rm(snapshotPath));
|
|
assert.equal(
|
|
readbackRuntime.readFile("snapshot.bin"),
|
|
await readFile(source, "utf8"),
|
|
);
|
|
report.controls = {
|
|
...report.controls,
|
|
completedRuntimeFileReadback: true,
|
|
completedRuntimeFileMutationDenied: true,
|
|
snapshotReadWriteSharingReader: true,
|
|
};
|
|
} finally {
|
|
await readbackRuntime.close();
|
|
}
|
|
const retryRuntime = WindowsPrivateRuntime.create(
|
|
workspace,
|
|
"cleanup-retry-",
|
|
);
|
|
const heldPath = join(retryRuntime.observation.path, "snapshot.bin");
|
|
await retryRuntime.snapshot(source, "snapshot.bin");
|
|
const holder = spawn(
|
|
"powershell.exe",
|
|
[
|
|
"-NoProfile",
|
|
"-NonInteractive",
|
|
"-Command",
|
|
"$ErrorActionPreference = 'Stop'; " +
|
|
"$stream = [IO.File]::Open($env:REA_CLEANUP_HELD_PATH, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite); " +
|
|
"try { [Console]::WriteLine('ready'); [Console]::In.ReadLine() | Out-Null } " +
|
|
"finally { $stream.Dispose() }",
|
|
],
|
|
{
|
|
env: { ...process.env, REA_CLEANUP_HELD_PATH: heldPath },
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
},
|
|
);
|
|
const holderClosed = once(holder, "close");
|
|
void holderClosed.catch(() => undefined);
|
|
try {
|
|
const [ready] = await once(holder.stdout, "data", {
|
|
signal: AbortSignal.timeout(15_000),
|
|
});
|
|
assert.equal(String(ready).trim(), "ready");
|
|
await assert.rejects(
|
|
retryRuntime.close(),
|
|
/owned cleanup object|Delete owned object/u,
|
|
);
|
|
assert.equal(
|
|
windowsPrivateRuntime(retryRuntime.observation.path),
|
|
retryRuntime,
|
|
);
|
|
assert.equal(await exists(heldPath), true);
|
|
assert.throws(
|
|
() => retryRuntime.writeFile("after-close.txt", "forbidden"),
|
|
/cleanup is pending/u,
|
|
);
|
|
holder.stdin.end("\n");
|
|
const [exitCode] = await holderClosed;
|
|
assert.equal(exitCode, 0);
|
|
await retryRuntime.close();
|
|
await retryRuntime.close();
|
|
assert.equal(await exists(retryRuntime.observation.path), false);
|
|
assert.throws(
|
|
() => windowsPrivateRuntime(retryRuntime.observation.path),
|
|
/No native private runtime/u,
|
|
);
|
|
report.controls.cleanupFailureRetainsOwnershipForRetry = true;
|
|
} finally {
|
|
holder.kill();
|
|
await holderClosed.catch(() => undefined);
|
|
await retryRuntime.close();
|
|
}
|
|
report.controls = {
|
|
...report.controls,
|
|
protectedDaclReadback: true,
|
|
sourceReplacementDenied: true,
|
|
sourceWriteDenied: true,
|
|
unrelatedFilesystemRenamePreserved: true,
|
|
junctionAdmissionDenied: true,
|
|
alternateStreamsDenied: true,
|
|
forgedAndClosedHandlesDenied: true,
|
|
immutableSnapshotDigest: true,
|
|
asynchronousSnapshot: true,
|
|
snapshotCancellation: true,
|
|
};
|
|
} 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;
|
|
const fixture = join(workspace, "tree.mjs");
|
|
await writeFile(
|
|
fixture,
|
|
`import {spawn} from 'node:child_process'; import {writeFileSync} from 'node:fs';
|
|
const depth=Number(process.argv[2]); writeFileSync(process.argv[3]+depth,String(process.pid));
|
|
if(depth>0)spawn(process.execPath,[import.meta.filename,String(depth-1),process.argv[3]],{detached:true,stdio:'ignore'});
|
|
setInterval(()=>{},1000);\n`,
|
|
);
|
|
const pidPrefix = join(workspace, "pid-");
|
|
const child = launch(["--max-old-space-size=128", fixture, "2", pidPrefix]);
|
|
const unrelated = spawn(
|
|
process.execPath,
|
|
["--max-old-space-size=128", "-e", "setInterval(()=>{},1000)"],
|
|
{ stdio: "ignore" },
|
|
);
|
|
try {
|
|
for (
|
|
let attempt = 0;
|
|
attempt < 200 && !(await exists(pidPrefix + "0"));
|
|
attempt++
|
|
)
|
|
await delay();
|
|
const pids = await Promise.all(
|
|
[0, 1, 2].map(async (depth) =>
|
|
Number(await readFile(pidPrefix + depth, "utf8")),
|
|
),
|
|
);
|
|
assert.ok(native.call("process_poll", [child.handle]).activeProcesses >= 3);
|
|
native.call("process_terminate", [child.handle]);
|
|
assert.equal((await wait(child)).activeProcesses, 0);
|
|
assert.equal(pids.some(live), false);
|
|
assert.ok(unrelated.pid !== undefined && live(unrelated.pid));
|
|
report.processes.descendantsAndDetachedDescendantsTerminated = true;
|
|
report.processes.unrelatedProcessPreserved = true;
|
|
} finally {
|
|
native.call("process_terminate", [child.handle]);
|
|
await wait(child);
|
|
native.call("process_close", [child.handle]);
|
|
if (unrelated.exitCode === null) {
|
|
const closed = new Promise((resolve) => unrelated.once("close", resolve));
|
|
unrelated.kill();
|
|
await closed;
|
|
}
|
|
}
|
|
const ownerClose = launch([
|
|
"--max-old-space-size=128",
|
|
fixture,
|
|
"0",
|
|
join(workspace, "owner-close-"),
|
|
]);
|
|
native.call("process_close", [ownerClose.handle]);
|
|
for (let attempt = 0; attempt < 100 && live(ownerClose.pid); attempt++)
|
|
await delay();
|
|
assert.equal(live(ownerClose.pid), false);
|
|
report.processes.killOnOwnerClose = true;
|
|
report.processes.protocolInput = await verifyWindowsProtocolInput(
|
|
packageRoot,
|
|
workspace,
|
|
);
|
|
report.processes.jobMemoryLimit = await verifyWindowsJobMemoryLimit(
|
|
packageRoot,
|
|
workspace,
|
|
);
|
|
const exited = launch([
|
|
"--max-old-space-size=128",
|
|
"-e",
|
|
"console.log('observed');process.exit(7)",
|
|
]);
|
|
const settled = await wait(exited);
|
|
assert.equal(settled.exitCode, 7);
|
|
assert.match(settled.output, /observed/u);
|
|
native.call("process_close", [exited.handle]);
|
|
report.processes.normalExitAndOutput = true;
|
|
const forwardCommand = process.execPath.replaceAll("\\", "/");
|
|
const forwardProcess = native.call("process_spawn", [
|
|
forwardCommand,
|
|
[forwardCommand, "-e", "console.log('forward executable')"]
|
|
.map(quote)
|
|
.join(" "),
|
|
workspace,
|
|
environment,
|
|
]);
|
|
try {
|
|
const completed = await wait(forwardProcess);
|
|
assert.equal(completed.exitCode, 0);
|
|
assert.match(completed.output, /forward executable/u);
|
|
report.processes.ordinaryDriveSeparators = true;
|
|
} finally {
|
|
native.call("process_close", [forwardProcess.handle]);
|
|
}
|
|
const boundaryFixture = process.argv[3];
|
|
if (boundaryFixture !== undefined) {
|
|
const before = JSON.parse(
|
|
execFileSync(boundaryFixture, [], { encoding: "utf8" }),
|
|
);
|
|
const boundary = native.call("process_spawn", [
|
|
boundaryFixture,
|
|
`${quote(boundaryFixture)} breakaway`,
|
|
workspace,
|
|
environment,
|
|
]);
|
|
const denied = await wait(boundary);
|
|
native.call("process_close", [boundary.handle]);
|
|
assert.equal(JSON.parse(denied.output).breakawayDenied, true);
|
|
const tokenChild = native.call("process_spawn", [
|
|
boundaryFixture,
|
|
quote(boundaryFixture),
|
|
workspace,
|
|
environment,
|
|
]);
|
|
const observed = await wait(tokenChild);
|
|
native.call("process_close", [tokenChild.handle]);
|
|
assert.equal(JSON.parse(observed.output).defaultOwnerIsUser, true);
|
|
const after = JSON.parse(
|
|
execFileSync(boundaryFixture, [], { encoding: "utf8" }),
|
|
);
|
|
assert.deepEqual(after, before);
|
|
report.processes.breakawayDenied = true;
|
|
report.processes.childOwnerNormalizedWithoutChangingCaller = true;
|
|
report.callerToken = before;
|
|
const environmentChild = native.call("process_spawn", [
|
|
boundaryFixture,
|
|
`${quote(boundaryFixture)} environment`,
|
|
workspace,
|
|
[
|
|
"z_REA=z",
|
|
"_REA=underscore",
|
|
"A_REA=a",
|
|
"REA_CASE=first",
|
|
"rea_case=last",
|
|
"REA_ß=sharp",
|
|
"REA_SS=double",
|
|
],
|
|
]);
|
|
const childEnvironment = await wait(environmentChild);
|
|
native.call("process_close", [environmentChild.handle]);
|
|
assert.deepEqual(JSON.parse(childEnvironment.output), {
|
|
ordinallySorted: true,
|
|
lastDuplicateSelected: true,
|
|
unicodeNamesPreserved: true,
|
|
});
|
|
report.processes.windowsEnvironmentSemantics = true;
|
|
// Observed through this process's own id: a separate probe process would
|
|
// get its own processor group, so it cannot stand in for the process that
|
|
// spawns the child.
|
|
const callerAffinity = JSON.parse(
|
|
execFileSync(boundaryFixture, ["affinity", String(process.pid)], {
|
|
encoding: "utf8",
|
|
}),
|
|
);
|
|
const affinityChild = native.call("process_spawn", [
|
|
boundaryFixture,
|
|
`${quote(boundaryFixture)} affinity`,
|
|
workspace,
|
|
environment,
|
|
]);
|
|
const observedAffinity = await wait(affinityChild);
|
|
native.call("process_close", [affinityChild.handle]);
|
|
assert.equal(observedAffinity.exitCode, 0);
|
|
const childAffinity = JSON.parse(observedAffinity.output);
|
|
// Masks cross as hexadecimal strings because a mask can exceed the
|
|
// safe-integer range. Sharing one processor group makes the caller's mask
|
|
// applicable, so it must be inherited exactly. Windows placing the child in
|
|
// another group is only permitted when the caller asked for no bound of its
|
|
// own, which its group's whole active set establishes; a narrower mask has
|
|
// to fail the spawn rather than widen to that group's default.
|
|
const sharedGroup =
|
|
callerAffinity.singleProcessorGroup &&
|
|
childAffinity.singleProcessorGroup &&
|
|
callerAffinity.processorGroup === childAffinity.processorGroup;
|
|
if (sharedGroup)
|
|
assert.equal(childAffinity.affinityMask, callerAffinity.affinityMask);
|
|
else assert.equal(callerAffinity.affinityMask, callerAffinity.systemMask);
|
|
report.processes.callerAffinityBoundsOwnedProcessOrRefusesToWiden = true;
|
|
report.callerAffinity = {
|
|
...callerAffinity,
|
|
sharedGroupWithChild: sharedGroup,
|
|
};
|
|
}
|
|
const ownerFixture = join(workspace, "owner.mjs");
|
|
const ownerPidFile = join(workspace, "crash-child.pid");
|
|
const moduleUrl = pathToFileURL(
|
|
join(packageRoot, "dist/windows/WindowsNativeLoader.js"),
|
|
).href;
|
|
await writeFile(
|
|
ownerFixture,
|
|
`import {writeFileSync} from 'node:fs';
|
|
import {requireWindowsNativeAuthority} from ${JSON.stringify(moduleUrl)};
|
|
const native=requireWindowsNativeAuthority();
|
|
const env=Object.entries(process.env).map(([k,v])=>k+'='+v);
|
|
const child=native.call('process_spawn',[process.execPath,${JSON.stringify([process.execPath, "--max-old-space-size=128", fixture, "0", join(workspace, "crash-descendant-")].map(quote).join(" "))},${JSON.stringify(workspace)},env]);
|
|
writeFileSync(${JSON.stringify(ownerPidFile)},String(child.pid));setInterval(()=>{},1000);\n`,
|
|
);
|
|
const owner = spawn(
|
|
process.execPath,
|
|
["--max-old-space-size=128", ownerFixture],
|
|
{ stdio: "ignore" },
|
|
);
|
|
try {
|
|
for (
|
|
let attempt = 0;
|
|
attempt < 200 && !(await exists(ownerPidFile));
|
|
attempt++
|
|
)
|
|
await delay();
|
|
const pid = Number(await readFile(ownerPidFile, "utf8"));
|
|
const closed = new Promise((resolve) => owner.once("close", resolve));
|
|
owner.kill("SIGKILL");
|
|
await closed;
|
|
for (let attempt = 0; attempt < 100 && live(pid); attempt++) await delay();
|
|
assert.equal(live(pid), false);
|
|
report.processes.ownerForcedExitKillsOwnedChild = true;
|
|
} finally {
|
|
if (owner.exitCode === null && owner.signalCode === null) {
|
|
const closed = new Promise((resolve) => owner.once("close", resolve));
|
|
owner.kill("SIGKILL");
|
|
await closed;
|
|
}
|
|
}
|
|
report.ok = true;
|
|
if (process.argv[4] !== undefined)
|
|
await writeFile(process.argv[4], `${JSON.stringify(report)}\n`);
|
|
process.stdout.write(`${JSON.stringify(report)}\n`);
|
|
} catch (cause) {
|
|
console.error(
|
|
"Native conformance failed",
|
|
cause.code ?? cause.name,
|
|
cause.constraint ?? "assertion",
|
|
);
|
|
throw cause;
|
|
} finally {
|
|
await rm(workspace, { recursive: true, force: true }).catch((cause) =>
|
|
console.error("Fixture cleanup failed", cause.code),
|
|
);
|
|
}
|