diff --git a/scripts/verify-adb.mjs b/scripts/verify-adb.mjs index fd4ed01285..8ec96ec536 100644 --- a/scripts/verify-adb.mjs +++ b/scripts/verify-adb.mjs @@ -1,13 +1,16 @@ import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; -import { mkdtemp, readFile, rm, stat } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { readFile, stat } from "node:fs/promises"; import { join, posix } from "node:path"; import { promisify } from "node:util"; import { AdbDeviceAnalysisService } from "../dist/application/adb/AdbDeviceAnalysisService.js"; import { createAdbDeviceAnalysisProvider } from "../dist/composition/adb.js"; import { closeVerifierResources } from "./lib/verifier-cleanup.mjs"; +import { + VerifierFixture, + withVerifierFixture, +} from "./lib/verifier-fixture.mjs"; const execFileAsync = promisify(execFile); const options = parseArguments(process.argv.slice(2)); @@ -279,8 +282,12 @@ try { } if (pullCandidate !== null) { - const directory = await mkdtemp(join(tmpdir(), "rea-adb-verify-")); - try { + const directoryFixture = await VerifierFixture.open({ + prefix: "rea-adb-verify-", + private: false, + }); + const directory = directoryFixture.path; + await withVerifierFixture(directoryFixture, async () => { const pull = await run("pull_adb_package", { serial, package: pullCandidate, @@ -309,15 +316,17 @@ try { ); } } - } finally { - await rm(directory, { recursive: true, force: true }); - } + }); } // File transfer: push one lane-owned file, verify both digests, pull it // back, then clean up the device copy through the same fixed argv surface. - const transferRoot = await mkdtemp(join(tmpdir(), "rea-adb-transfer-")); - try { + const transferRootFixture = await VerifierFixture.open({ + prefix: "rea-adb-transfer-", + private: false, + }); + const transferRoot = transferRootFixture.path; + await withVerifierFixture(transferRootFixture, async () => { const payload = join(transferRoot, "rea-verify.txt"); const payloadBytes = "rea adb transfer verification\n"; await import("node:fs/promises").then((fs) => @@ -331,8 +340,12 @@ try { devicePath: deviceTarget, outputDirectory: transferRoot, }); - const screenRoot = await mkdtemp(join(tmpdir(), "rea-adb-screen-")); - try { + const screenRootFixture = await VerifierFixture.open({ + prefix: "rea-adb-screen-", + private: false, + }); + const screenRoot = screenRootFixture.path; + await withVerifierFixture(screenRootFixture, async () => { const screen = await run("capture_adb_screen", { serial, output_directory: screenRoot, @@ -343,9 +356,7 @@ try { screen.width !== null && screen.height !== null && screen.bytes > 0, `${screen.width}x${screen.height}, ${screen.bytes} bytes`, ); - } finally { - await rm(screenRoot, { recursive: true, force: true }); - } + }); if (options.pull !== null) { const details = await run("inspect_adb_package", { serial, @@ -358,9 +369,7 @@ try { `versionCode=${String(details.version_code)}`, ); } - } finally { - await rm(transferRoot, { recursive: true, force: true }); - } + }); } } catch (cause) { verifierFailed = true; diff --git a/scripts/verify-binary-hardening.mjs b/scripts/verify-binary-hardening.mjs index fcd25374e1..5e16dac4cf 100644 --- a/scripts/verify-binary-hardening.mjs +++ b/scripts/verify-binary-hardening.mjs @@ -1,11 +1,11 @@ /** Compare binary hardening rows with host toolchains when they exist. */ import assert from "node:assert/strict"; -import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; import { importVerifierSource } from "./lib/verifier-source-loader.mjs"; import { createVerifierCommands } from "./lib/verify-commands.mjs"; +import { VerifierFixture, onceConfirmed } from "./lib/verifier-fixture.mjs"; const { readElfHardening } = await importVerifierSource( "../src/native/hardening/ElfHardening.ts", @@ -99,10 +99,11 @@ const versionLine = async (command, args = ["--version"]) => { const statesOf = (rows) => Object.fromEntries(rows.map((row) => [row.property, row.state])); -// The root holds every compiled image. Remove it only after the owned -// commands have settled, and keep it when their cleanup failed. +// The root holds every compiled image. The fixture removes it only after the +// owned commands have settled, and keeps it when their cleanup failed. +const closeCommands = onceConfirmed(() => commands.close()); +let rootFixture; let root; -let commandsClosed = false; await withVerifierCleanup(async () => { const report = { host: `${process.platform} ${process.arch}`, @@ -144,20 +145,21 @@ await withVerifierCleanup(async () => { } `; - root = await mkdtemp(join(tmpdir(), "rea-binary-hardening-")); + rootFixture = await VerifierFixture.open({ + prefix: "rea-binary-hardening-", + private: false, + }); + rootFixture.releaseAfter(closeCommands); + root = rootFixture.path; const cFile = join(root, "sample.c"); await writeFile(cFile, source); await verifyElf(root, cFile, report); await verifyMacho(root, cFile, report); console.log(JSON.stringify(report, null, 2)); }, [ + closeCommands, async () => { - await commands.close(); - commandsClosed = true; - }, - async () => { - if (commandsClosed && root !== undefined) - await rm(root, { recursive: true, force: true }); + await rootFixture?.release(); }, async () => { process.off("SIGINT", interrupt); diff --git a/scripts/verify-elf-unwind-ranges.mjs b/scripts/verify-elf-unwind-ranges.mjs index 121bf31155..2d0ffd9a08 100644 --- a/scripts/verify-elf-unwind-ranges.mjs +++ b/scripts/verify-elf-unwind-ranges.mjs @@ -4,11 +4,11 @@ * C++ compiler or no readelf. */ import assert from "node:assert/strict"; -import { mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { writeFile } from "node:fs/promises"; import { join, resolve } from "node:path"; import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; import { createVerifierCommands } from "./lib/verify-commands.mjs"; +import { VerifierFixture, onceConfirmed } from "./lib/verifier-fixture.mjs"; /** * Every probe, compiler driver and inspection runs as an owned process tree, @@ -178,10 +178,11 @@ const inspectedStarts = async (path) => { } }; -// The root holds every compiled image. Remove it only after the owned -// commands have settled, and keep it when their cleanup failed. +// The root holds every compiled image. The fixture removes it only after the +// owned commands have settled, and keeps it when their cleanup failed. +const closeCommands = onceConfirmed(() => commands.close()); +let rootFixture; let root; -let commandsClosed = false; let unavailable = false; await withVerifierCleanup(async () => { const compiler = await firstAvailable(["zig", "clang++", "g++"]); @@ -203,7 +204,12 @@ await withVerifierCleanup(async () => { unavailable = true; return; } - root = await mkdtemp(join(tmpdir(), "rea-elf-unwind-")); + rootFixture = await VerifierFixture.open({ + prefix: "rea-elf-unwind-", + private: false, + }); + rootFixture.releaseAfter(closeCommands); + root = rootFixture.path; const source = join(root, "ranges.cpp"); const unstripped = join(root, "ranges.unstripped"); const stripped = join(root, "ranges.stripped"); @@ -227,13 +233,9 @@ await withVerifierCleanup(async () => { }), ); }, [ + closeCommands, async () => { - await commands.close(); - commandsClosed = true; - }, - async () => { - if (commandsClosed && root !== undefined) - await rm(root, { recursive: true, force: true }); + await rootFixture?.release(); }, async () => { process.off("SIGINT", interrupt); diff --git a/scripts/verify-flutter.mjs b/scripts/verify-flutter.mjs index 0425711fda..10104f14d8 100644 --- a/scripts/verify-flutter.mjs +++ b/scripts/verify-flutter.mjs @@ -1,19 +1,19 @@ import assert from "node:assert/strict"; import { execFile } from "node:child_process"; -import { mkdtemp, readdir, readFile, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join, resolve } from "node:path"; +import { readdir, readFile } from "node:fs/promises"; +import { resolve } from "node:path"; import { promisify } from "node:util"; import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import { toolContract } from "../dist/contracts/toolContracts.js"; import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js"; import { requireMcpOutput } from "./lib/mcp-verifier-results.mjs"; -import { - closeVerifierResources, - withVerifierCleanup, -} from "./lib/verifier-cleanup.mjs"; +import { closeVerifierResources } from "./lib/verifier-cleanup.mjs"; import { hashVerifierFile } from "./lib/verifier-file-resources.mjs"; +import { + VerifierFixture, + withVerifierFixture, +} from "./lib/verifier-fixture.mjs"; const options = parseArguments(process.argv.slice(2)); if (options.help) { @@ -33,7 +33,11 @@ const expectations = ? null : JSON.parse(await readFile(options.expectations, "utf8")); // This is a shared test parent, not a leased provider-owned Windows runtime. -const root = await mkdtemp(join(tmpdir(), "rea-flutter-verifier-")); +const rootFixture = await VerifierFixture.open({ + prefix: "rea-flutter-verifier-", + private: false, +}); +const root = rootFixture.path; const resources = new ArtifactResourceScope(); const entrypoint = resolve("scripts/rea.mjs"); const environment = Object.fromEntries( @@ -52,169 +56,165 @@ const transport = new StdioClientTransport({ stderr: "pipe", }); const execute = promisify(execFile); -await withVerifierCleanup( - async () => { - await client.connect(transport); - const apk = resolve(options.apk); - const identity = await hashVerifierFile(apk, resources); - const identified = await run("identify_flutter_build", { path: apk }); - assert.deepEqual(identified.target, { path: apk, ...identity }); - assert.equal(identified.flutter_detected, true); - assert.equal(identified.coverage, "complete"); - const abis = identified.abis.filter((abi) => abi.libapp.present); - assert.ok(abis.length > 0); - const hashes = new Set(); +// Server shutdown retries retained provider cleanup and settles before the +// fixture removes the root; snapshots left under it fail the run and keep it. +rootFixture.releaseAfter(async () => { + await closeVerifierResources([ + () => client.close(), + () => transport.close(), + () => resources.close(), + ]); + assert.deepEqual( + await readdir(root), + [], + `Provider snapshots remain under ${root}`, + ); +}); +await withVerifierFixture(rootFixture, async () => { + await client.connect(transport); + const apk = resolve(options.apk); + const identity = await hashVerifierFile(apk, resources); + const identified = await run("identify_flutter_build", { path: apk }); + assert.deepEqual(identified.target, { path: apk, ...identity }); + assert.equal(identified.flutter_detected, true); + assert.equal(identified.coverage, "complete"); + const abis = identified.abis.filter((abi) => abi.libapp.present); + assert.ok(abis.length > 0); + const hashes = new Set(); + for (const abi of abis) { + assert.equal(abi.libapp.elf_header_valid, true); + assert.equal(abi.libapp.snapshot_hash_sources, 2); + assert.ok(abi.libapp.snapshot_hash); + hashes.add(abi.libapp.snapshot_hash); + assert.equal(abi.libflutter.present, true); + assert.match(abi.libflutter.build_id ?? "", /^(?:[0-9a-f]{2}){4,64}$/u); + assert.deepEqual(abi.libflutter.build_id_candidates, [ + abi.libflutter.build_id, + ]); + } + assert.equal(hashes.size, 1, "Every ABI must agree on the snapshot hash"); + if (expectations !== null) { + assert.equal(identity.sha256, expectations.target_sha256); + assert.deepEqual([...hashes], [expectations.snapshot_hash]); + assert.deepEqual( + abis.map((abi) => abi.abi).sort(), + Object.keys(expectations.engines).sort(), + ); for (const abi of abis) { - assert.equal(abi.libapp.elf_header_valid, true); - assert.equal(abi.libapp.snapshot_hash_sources, 2); - assert.ok(abi.libapp.snapshot_hash); - hashes.add(abi.libapp.snapshot_hash); - assert.equal(abi.libflutter.present, true); - assert.match(abi.libflutter.build_id ?? "", /^(?:[0-9a-f]{2}){4,64}$/u); - assert.deepEqual(abi.libflutter.build_id_candidates, [ - abi.libflutter.build_id, - ]); - } - assert.equal(hashes.size, 1, "Every ABI must agree on the snapshot hash"); - if (expectations !== null) { - assert.equal(identity.sha256, expectations.target_sha256); - assert.deepEqual([...hashes], [expectations.snapshot_hash]); assert.deepEqual( - abis.map((abi) => abi.abi).sort(), - Object.keys(expectations.engines).sort(), + { + dart_version: abi.libflutter.dart_version, + toolchain_lines: abi.libflutter.toolchain_lines, + }, + expectations.engines[abi.abi], ); - for (const abi of abis) { - assert.deepEqual( - { - dart_version: abi.libflutter.dart_version, - toolchain_lines: abi.libflutter.toolchain_lines, - }, - expectations.engines[abi.abi], - ); - assert.equal(abi.libflutter.dart_version_capped, false); - assert.equal(abi.libflutter.toolchain_capped, false); - } + assert.equal(abi.libflutter.dart_version_capped, false); + assert.equal(abi.libflutter.toolchain_capped, false); + } + process.stdout.write( + "[ok] Exact per-ABI engine labels match digest-bound expectations\n", + ); + } else { + process.stdout.write( + "[unverified] Exact engine labels and their absence need --expectations JSON\n", + ); + } + // The same checks apply to the default selection and each remaining ABI. + const preferred = await run("inspect_dart_aot", { path: apk }); + assert.ok( + abis.some((abi) => abi.abi === preferred.abi), + "Default AOT selection must be one of the identified ABIs", + ); + for (const abi of abis) { + const aot = + abi.abi === preferred.abi + ? preferred + : await run("inspect_dart_aot", { path: apk, abi: abi.abi }); + assert.equal(aot.abi, abi.abi); + assert.equal( + aot.libapp.coverage, + "complete", + `Snapshot decoding must be complete on ${abi.abi}`, + ); + assert.equal(aot.coverage, aot.string_pool.coverage); + if (aot.coverage !== "complete") { process.stdout.write( - "[ok] Exact per-ABI engine labels match digest-bound expectations\n", + `[unverified] ${abi.abi}: full string-pool projection (returned pool is bounded and partial)\n`, + ); + } + assert.deepEqual(aot.target, identified.target); + assert.equal(aot.libapp.sha256, abi.libapp.sha256); + assert.equal(aot.libapp.bytes, abi.libapp.bytes); + assert.equal(aot.libapp.snapshot_hash_sources, 2); + assert.equal(aot.libapp.snapshot_hash, abi.libapp.snapshot_hash); + assert.deepEqual( + aot.libapp.sections.map((section) => section.name).sort(), + [ + "_kDartVmSnapshotData", + "_kDartVmSnapshotInstructions", + "_kDartIsolateSnapshotData", + "_kDartIsolateSnapshotInstructions", + ].sort(), + ); + for (const section of aot.libapp.sections) { + assert.ok(section.offset + section.size <= aot.libapp.bytes); + if (!section.name.endsWith("Data")) continue; + assert.equal(section.magic_valid, true); + assert.equal( + section.kind, + 3, + "This verifier requires an AOT app snapshot", + ); + assert.ok( + section.header_length >= 48 && + section.header_length + 4 <= section.size, + `Snapshot extent must fit ${section.name} on ${abi.abi}`, + ); + } + assert.ok(aot.string_pool.package_uri_count_at_least > 0); + assert.ok(aot.string_pool.dart_uri_count_at_least > 0); + if (expectations?.string_pools !== undefined) { + const expected = expectations.string_pools[abi.abi]; + assert.ok( + expected, + `Pinned string-pool counts are required on ${abi.abi}`, + ); + assert.equal( + aot.string_pool.package_uri_count_at_least, + expected.package_uri_count_at_least, + ); + assert.equal( + aot.string_pool.dart_uri_count_at_least, + expected.dart_uri_count_at_least, + ); + process.stdout.write( + `[ok] ${abi.abi}: exact pinned package and Dart URI counts\n`, ); } else { process.stdout.write( - "[unverified] Exact engine labels and their absence need --expectations JSON\n", + `[unverified] ${abi.abi}: exact string-pool counts need --expectations JSON\n`, ); } - // The same checks apply to the default selection and each remaining ABI. - const preferred = await run("inspect_dart_aot", { path: apk }); - assert.ok( - abis.some((abi) => abi.abi === preferred.abi), - "Default AOT selection must be one of the identified ABIs", + process.stdout.write( + `[ok] ${abi.abi}: four symbols, bounded AOT headers, agreeing hashes and string pool\n`, ); - for (const abi of abis) { - const aot = - abi.abi === preferred.abi - ? preferred - : await run("inspect_dart_aot", { path: apk, abi: abi.abi }); - assert.equal(aot.abi, abi.abi); - assert.equal( - aot.libapp.coverage, - "complete", - `Snapshot decoding must be complete on ${abi.abi}`, - ); - assert.equal(aot.coverage, aot.string_pool.coverage); - if (aot.coverage !== "complete") { - process.stdout.write( - `[unverified] ${abi.abi}: full string-pool projection (returned pool is bounded and partial)\n`, - ); - } - assert.deepEqual(aot.target, identified.target); - assert.equal(aot.libapp.sha256, abi.libapp.sha256); - assert.equal(aot.libapp.bytes, abi.libapp.bytes); - assert.equal(aot.libapp.snapshot_hash_sources, 2); - assert.equal(aot.libapp.snapshot_hash, abi.libapp.snapshot_hash); - assert.deepEqual( - aot.libapp.sections.map((section) => section.name).sort(), - [ - "_kDartVmSnapshotData", - "_kDartVmSnapshotInstructions", - "_kDartIsolateSnapshotData", - "_kDartIsolateSnapshotInstructions", - ].sort(), - ); - for (const section of aot.libapp.sections) { - assert.ok(section.offset + section.size <= aot.libapp.bytes); - if (!section.name.endsWith("Data")) continue; - assert.equal(section.magic_valid, true); - assert.equal( - section.kind, - 3, - "This verifier requires an AOT app snapshot", - ); - assert.ok( - section.header_length >= 48 && - section.header_length + 4 <= section.size, - `Snapshot extent must fit ${section.name} on ${abi.abi}`, - ); - } - assert.ok(aot.string_pool.package_uri_count_at_least > 0); - assert.ok(aot.string_pool.dart_uri_count_at_least > 0); - if (expectations?.string_pools !== undefined) { - const expected = expectations.string_pools[abi.abi]; - assert.ok( - expected, - `Pinned string-pool counts are required on ${abi.abi}`, - ); - assert.equal( - aot.string_pool.package_uri_count_at_least, - expected.package_uri_count_at_least, - ); - assert.equal( - aot.string_pool.dart_uri_count_at_least, - expected.dart_uri_count_at_least, - ); - process.stdout.write( - `[ok] ${abi.abi}: exact pinned package and Dart URI counts\n`, - ); - } else { - process.stdout.write( - `[unverified] ${abi.abi}: exact string-pool counts need --expectations JSON\n`, - ); - } - process.stdout.write( - `[ok] ${abi.abi}: four symbols, bounded AOT headers, agreeing hashes and string pool\n`, - ); - } - if (options.plainApk !== null) { - const plain = resolve(options.plainApk); - const negative = await run("identify_flutter_build", { path: plain }); - assert.deepEqual(negative.target, { - path: plain, - ...(await hashVerifierFile(plain, resources)), - }); - assert.equal(negative.flutter_detected, false); - assert.equal(negative.coverage, "complete"); - } - assert.deepEqual( - await hashVerifierFile(apk, resources), - identity, - "Verification must preserve its input", - ); - }, - // Server shutdown retries retained provider cleanup and settles before success. - [ - async () => { - await closeVerifierResources([ - () => client.close(), - () => transport.close(), - () => resources.close(), - ]); - assert.deepEqual( - await readdir(root), - [], - `Provider snapshots remain under ${root}`, - ); - await rm(root, { recursive: true, force: true }); - }, - ], -); + } + if (options.plainApk !== null) { + const plain = resolve(options.plainApk); + const negative = await run("identify_flutter_build", { path: plain }); + assert.deepEqual(negative.target, { + path: plain, + ...(await hashVerifierFile(plain, resources)), + }); + assert.equal(negative.flutter_detected, false); + assert.equal(negative.coverage, "complete"); + } + assert.deepEqual( + await hashVerifierFile(apk, resources), + identity, + "Verification must preserve its input", + ); +}); process.stdout.write( "all asserted Flutter CLI/MCP checks passed; unverified facets are reported above\n", ); diff --git a/scripts/verify-jeb.mjs b/scripts/verify-jeb.mjs index 6abd439a5d..35bb1b9e1b 100644 --- a/scripts/verify-jeb.mjs +++ b/scripts/verify-jeb.mjs @@ -1,12 +1,12 @@ import { execFile } from "node:child_process"; -import { mkdtemp, writeFile, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { promisify } from "node:util"; import { JebAnalysisService } from "../dist/application/jeb/JebAnalysisService.js"; import { createJebAnalysisProvider } from "../dist/composition/jeb.js"; import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; +import { VerifierFixture } from "./lib/verifier-fixture.mjs"; const run = promisify(execFile); @@ -26,10 +26,15 @@ if (options.help) { const provider = createJebAnalysisProvider(process.env); const service = new JebAnalysisService(provider); +let fixture; let fixtureDirectory; try { const report = await withVerifierCleanup(async () => { - fixtureDirectory = await mkdtemp(join(tmpdir(), "rea-jeb-")); + fixture = await VerifierFixture.open({ + prefix: "rea-jeb-", + private: false, + }); + fixtureDirectory = fixture.path; const client = await service.execute("inspect_jeb_client", {}); if (!client.ok) throw new Error(client.error.message); const engine = client.value.normalized_result.engine; @@ -89,18 +94,19 @@ try { }; }, [ async () => { + // The fixture is removed only after the provider confirms it closed. await provider.close(); - if (fixtureDirectory !== undefined) { - await rm(fixtureDirectory, { recursive: true, force: true }); - fixtureDirectory = undefined; - } + await fixture?.release(); }, ]); process.stdout.write(`${JSON.stringify(report)}\n`); } catch (cause) { - if (fixtureDirectory !== undefined) + if (fixture !== undefined && !fixture.released) process.stderr.write(`JEB fixture retained at ${fixtureDirectory}\n`); - const causes = cause instanceof AggregateError ? cause.errors : [cause]; + // Fixture and cleanup failures nest AggregateErrors; report each leaf reason. + const leaves = (reason) => + reason instanceof AggregateError ? reason.errors.flatMap(leaves) : [reason]; + const causes = leaves(cause); process.stderr.write( `JEB real-provider verification failed: ${causes.map((reason) => (reason instanceof Error ? reason.message : String(reason))).join("; ")}\n`, ); diff --git a/scripts/verify-native-calls.mjs b/scripts/verify-native-calls.mjs index c6c826cd47..a7c89f6c62 100644 --- a/scripts/verify-native-calls.mjs +++ b/scripts/verify-native-calls.mjs @@ -1,8 +1,7 @@ import { requireMcpToolError } from "./lib/mcp-verifier-results.mjs"; import assert from "node:assert/strict"; import { execFile } from "node:child_process"; -import { mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { writeFile } from "node:fs/promises"; import { basename, join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; @@ -14,6 +13,10 @@ import { artifactMcpCall, withArtifactMcp, } from "./lib/artifact-e2e.mjs"; +import { + VerifierFixture, + withVerifierFixture, +} from "./lib/verifier-fixture.mjs"; const exec = promisify(execFile); @@ -264,8 +267,12 @@ const checkHardenedRuntime = async (directory, binary) => { assert.equal(traced.process.exit_status, 0); }; -const directory = await mkdtemp(join(tmpdir(), "rea-native-calls-")); -try { +const directoryFixture = await VerifierFixture.open({ + prefix: "rea-native-calls-", + private: false, +}); +const directory = directoryFixture.path; +await withVerifierFixture(directoryFixture, async () => { const binary = join(directory, "ReaCalls"); await exec("/usr/bin/xcrun", [ "clang", @@ -346,6 +353,4 @@ try { }, }), ); -} finally { - await rm(directory, { recursive: true, force: true }); -} +}); diff --git a/scripts/verify-native-pascal-calls.mjs b/scripts/verify-native-pascal-calls.mjs index d750200397..d594698cde 100644 --- a/scripts/verify-native-pascal-calls.mjs +++ b/scripts/verify-native-pascal-calls.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { readFile, rm } from "node:fs/promises"; +import { readFile } from "node:fs/promises"; import { isAbsolute, join } from "node:path"; import { fileURLToPath } from "node:url"; import { createPackageVerifierCommands } from "./lib/verify-package-commands.mjs"; @@ -10,7 +10,7 @@ import { artifactMcpResult, withArtifactMcp, } from "./lib/artifact-e2e.mjs"; -import { createCanonicalTemporaryDirectory } from "./lib/canonical-temporary-directory.mjs"; +import { VerifierFixture } from "./lib/verifier-fixture.mjs"; const cancellation = new AbortController(); let shutdownSignal; @@ -47,15 +47,14 @@ const callerLines = new Set( return matches[0]; }), ); -const allocatedDirectory = - await createCanonicalTemporaryDirectory("rea-pascal-calls-"); -let directory = allocatedDirectory.path; +const directoryFixture = await VerifierFixture.open({ + prefix: "rea-pascal-calls-", + private: false, +}); +let directory = directoryFixture.path; +// The package command owner releases the fixture after its commands close. const commands = await createPackageVerifierCommands({ - workspace: { - path: allocatedDirectory.allocatedPath, - close: () => - rm(allocatedDirectory.allocatedPath, { recursive: true, force: true }), - }, + workspace: { path: directory, close: () => directoryFixture.release() }, }); const execute = (command, arguments_, { timeout }) => commands.exec(command, arguments_, { diff --git a/scripts/verify-native-ui.mjs b/scripts/verify-native-ui.mjs index 252beaea05..2f606974be 100644 --- a/scripts/verify-native-ui.mjs +++ b/scripts/verify-native-ui.mjs @@ -1,7 +1,6 @@ import { execFile, spawn } from "node:child_process"; import assert from "node:assert/strict"; -import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { mkdir, writeFile, readFile } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; @@ -10,12 +9,17 @@ import { observeNativeUi } from "../dist/native/NativeUiObservation.js"; import { NATIVE_UI_HELPER_MAX_BUFFER } from "../dist/native/NativeUiOutputBudget.js"; import { artifactMcpResult, withArtifactMcp } from "./lib/artifact-e2e.mjs"; import { observeProcessStartIdentity } from "../dist/process/ProcessOwnershipObservation.js"; +import { VerifierFixture } from "./lib/verifier-fixture.mjs"; if (process.platform !== "darwin") throw new Error( "Native UI verification requires macOS, an interactive desktop and Xcode command-line tools", ); -const root = await mkdtemp(join(tmpdir(), "rea-ui-fixture-")); +const rootFixture = await VerifierFixture.open({ + prefix: "rea-ui-fixture-", + private: false, +}); +const root = rootFixture.path; let child; let childExit; let fixturePid; @@ -294,7 +298,7 @@ try { } const fixtureExited = child === undefined || (await openExitedWithFixture(child, childExit)); - if (fixtureExited) await rm(root, { recursive: true, force: true }); + if (fixtureExited) await rootFixture.release(); else { // The app may have been reparented away from `open`; stopping `open` // does not prove the fixture exited, so its root stays in place. diff --git a/scripts/verify-node-sea.mjs b/scripts/verify-node-sea.mjs index d936728c40..9f3bece843 100644 --- a/scripts/verify-node-sea.mjs +++ b/scripts/verify-node-sea.mjs @@ -1,16 +1,23 @@ import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; -import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { readFile, writeFile } from "node:fs/promises"; import { join, resolve } from "node:path"; +import { + VerifierFixture, + withVerifierFixture, +} from "./lib/verifier-fixture.mjs"; // Requires a running Docker daemon, network access for the pinned fixture // producer, and `npm run build:cached`. Never executes the injected application. -const root = await mkdtemp(join(tmpdir(), "rea-node-sea-real-")); +const rootFixture = await VerifierFixture.open({ + prefix: "rea-node-sea-real-", + private: false, +}); +const root = rootFixture.path; const image = "node:24.10.0-bookworm"; const script = 'throw new Error("SEA verification must not execute this application");\n'; -try { +await withVerifierFixture(rootFixture, async () => { await writeFile(join(root, "main.js"), script); await writeFile(join(root, "asset.txt"), "real SEA asset\n"); await writeFile( @@ -76,6 +83,4 @@ try { process.stdout.write( `${JSON.stringify({ producer: image, metadata, verified: ["ELF inventory", "code cache", "fuse", "script extraction", "asset extraction"] }, null, 2)}\n`, ); -} finally { - await rm(root, { recursive: true, force: true }); -} +}); diff --git a/scripts/verify-pe-load-config.mjs b/scripts/verify-pe-load-config.mjs index 6a052c03b4..89315d4118 100644 --- a/scripts/verify-pe-load-config.mjs +++ b/scripts/verify-pe-load-config.mjs @@ -1,13 +1,20 @@ /** Build real PE TLS samples with Zig, then inspect through the public CLI. */ import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; -import { mkdtemp, writeFile, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { writeFile } from "node:fs/promises"; import { join, resolve } from "node:path"; +import { + VerifierFixture, + withVerifierFixture, +} from "./lib/verifier-fixture.mjs"; const zig = process.env.REA_ZIG ?? "zig"; -const root = await mkdtemp(join(tmpdir(), "rea-pe-load-config-")); -try { +const rootFixture = await VerifierFixture.open({ + prefix: "rea-pe-load-config-", + private: false, +}); +const root = rootFixture.path; +await withVerifierFixture(rootFixture, async () => { const source = join(root, "tls.c"); await writeFile( source, @@ -77,6 +84,4 @@ void entry(void) {} }), ); } -} finally { - await rm(root, { recursive: true, force: true }); -} +}); diff --git a/scripts/verify-real-cutter.mjs b/scripts/verify-real-cutter.mjs index c5be50c7a0..f25602415e 100644 --- a/scripts/verify-real-cutter.mjs +++ b/scripts/verify-real-cutter.mjs @@ -8,18 +8,11 @@ import assert from "node:assert/strict"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { createPackageVerifierCommands } from "./lib/verify-package-commands.mjs"; import { createHash, randomUUID } from "node:crypto"; -import { - access, - constants, - mkdtemp, - readFile, - rm, - writeFile, -} from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { access, constants, readFile, writeFile } from "node:fs/promises"; import { join, resolve } from "node:path"; import { createServer } from "../dist/server/createServer.js"; +import { VerifierFixture } from "./lib/verifier-fixture.mjs"; const cli = resolve("scripts/rea.mjs"); const runCli = async (...arguments_) => { @@ -58,12 +51,14 @@ if (process.env.EXPECTED_PLATFORM !== undefined) { "Cutter runner did not match its declared platform.", ); } -const projectRoot = await mkdtemp(join(tmpdir(), "rea-cutter-smoke-")); +const projectFixture = await VerifierFixture.open({ + prefix: "rea-cutter-smoke-", + private: false, +}); +const projectRoot = projectFixture.path; +// The package command owner releases the fixture after its commands close. const commands = await createPackageVerifierCommands({ - workspace: { - path: projectRoot, - close: () => rm(projectRoot, { recursive: true, force: true }), - }, + workspace: { path: projectRoot, close: () => projectFixture.release() }, }); const projectPath = join(projectRoot, "smoke.rzdb"); await withVerifierCleanup(async () => { diff --git a/scripts/verify/architecture-guard-baselines.json b/scripts/verify/architecture-guard-baselines.json index 91171d5842..0453980171 100644 --- a/scripts/verify/architecture-guard-baselines.json +++ b/scripts/verify/architecture-guard-baselines.json @@ -375,17 +375,6 @@ "src/sqlite/SqliteDatabaseProvider.ts": 1 }, "verifier-fixture-root": { - "scripts/verify-adb.mjs": 3, - "scripts/verify-binary-hardening.mjs": 1, - "scripts/verify-elf-unwind-ranges.mjs": 1, - "scripts/verify-flutter.mjs": 1, - "scripts/verify-jeb.mjs": 1, - "scripts/verify-native-calls.mjs": 1, - "scripts/verify-native-pascal-calls.mjs": 1, - "scripts/verify-native-ui.mjs": 1, - "scripts/verify-node-sea.mjs": 1, - "scripts/verify-pe-load-config.mjs": 1, - "scripts/verify-real-cutter.mjs": 1, "scripts/verify-test-temp-hygiene.mjs": 1, "scripts/verify-windows-native.mjs": 1 }, diff --git a/tests/boundary/process/adbVerifierCleanup.test.ts b/tests/boundary/process/adbVerifierCleanup.test.ts index 2b1a16e182..3a58efda94 100644 --- a/tests/boundary/process/adbVerifierCleanup.test.ts +++ b/tests/boundary/process/adbVerifierCleanup.test.ts @@ -37,12 +37,19 @@ const runVerifier = async ( join(root, "scripts", "verify-adb.mjs"), await readFile(new URL("../../../scripts/verify-adb.mjs", import.meta.url)), ); - await writeFile( - join(root, "scripts", "lib", "verifier-cleanup.mjs"), - await readFile( - new URL("../../../scripts/lib/verifier-cleanup.mjs", import.meta.url), - ), - ); + // The verifier's cleanup and fixture owners, with the fixture's imports. + for (const helper of [ + "verifier-cleanup.mjs", + "verifier-fixture.mjs", + "canonical-temporary-directory.mjs", + "verifier-source-loader.mjs", + ]) + await writeFile( + join(root, "scripts", "lib", helper), + await readFile( + new URL(`../../../scripts/lib/${helper}`, import.meta.url), + ), + ); // The simulated device directory stands in for /data/local/tmp; the fake // adb client records its argv and removes only the named probe. const device = join(root, "device");