From aec679584a74576f39887f3961ff484dafc24dcd Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 15 Nov 2026 21:05:34 +0800 Subject: [PATCH] refactor(verify): give the switch, Swift and address-context Ghidra workspaces to VerifierFixture (#11121) * refactor(verify): give the switch, Swift and address-context Ghidra workspaces to VerifierFixture * fix(verify): carry the retained Ghidra workspace on each verifier's final failure --- .../verify-real-ghidra-address-context.mjs | 58 ++++++++----------- scripts/verify-real-ghidra-swift.mjs | 57 +++++++++--------- scripts/verify-real-ghidra-switch.mjs | 41 +++++++------ .../verify/architecture-guard-baselines.json | 5 +- 4 files changed, 75 insertions(+), 86 deletions(-) diff --git a/scripts/verify-real-ghidra-address-context.mjs b/scripts/verify-real-ghidra-address-context.mjs index 55c6c1f301..93b2bcfd06 100644 --- a/scripts/verify-real-ghidra-address-context.mjs +++ b/scripts/verify-real-ghidra-address-context.mjs @@ -1,8 +1,7 @@ import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; -import { mkdir, mkdtemp, readFile, readdir, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { mkdir, readFile, readdir } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; @@ -14,6 +13,7 @@ import { requireMcpOutput, } from "./lib/mcp-verifier-results.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; +import { VerifierFixture } from "./lib/verifier-fixture.mjs"; if (process.argv.length !== 2 || process.platform !== "linux") throw new Error("Run this ELF verifier on Linux without arguments."); @@ -22,7 +22,12 @@ if (!process.env.GHIDRA_INSTALL_DIR) const exec = promisify(execFile); const run = createVerifierRun(); const root = fileURLToPath(new URL("..", import.meta.url)); -const workspace = await mkdtemp(join(tmpdir(), "rea-address-context-")); +// Ghidra children use the workspace as TMPDIR; a plain root admits them on every host. +const workspaceFixture = await VerifierFixture.open({ + prefix: "rea-address-context-", + private: false, +}); +const workspace = workspaceFixture.path; const runtime = join(workspace, "runtime"); await mkdir(runtime); const path = join(workspace, "pie.elf"); @@ -302,47 +307,34 @@ try { const cleanupFailures = []; // Await provider-owned cleanup before the SDK can terminate its MCP parent. -if (connected) { - try { - await call("close_binary"); - } catch (cause) { - cleanupFailures.push(cause); - } -} -try { - await client.close(); -} catch (cause) { - cleanupFailures.push(cause); -} -try { - await transport.close(); -} catch (cause) { - cleanupFailures.push(cause); -} -try { +// The workspace is removed only after every cleanup and check confirms. +if (connected) workspaceFixture.releaseAfter(() => call("close_binary")); +workspaceFixture.releaseAfter(() => client.close()); +workspaceFixture.releaseAfter(() => transport.close()); +workspaceFixture.releaseAfter(async () => { assert.deepEqual(await readdir(runtime), []); verifierRun = await completeVerifierRun(run); assert.equal(verifierRun.process_lineage.status, "verified"); assert.deepEqual(verifierRun.process_lineage.descendants, []); +}); +try { + await workspaceFixture.release(); } catch (cause) { cleanupFailures.push(cause); } -if (cleanupFailures.length === 0) { - try { - await rm(workspace, { recursive: true, force: true }); - } catch (cause) { - cleanupFailures.push(cause); - } -} if (cleanupFailures.length > 0) { process.stderr.write( `Retained verifier workspace after cleanup failure: ${workspace}\n`, ); - throw new AggregateError( - primaryFailure === undefined - ? cleanupFailures - : [primaryFailure.cause, ...cleanupFailures], - "Ghidra address-context verifier cleanup failed", + throw Object.assign( + new AggregateError( + primaryFailure === undefined + ? cleanupFailures + : [primaryFailure.cause, ...cleanupFailures], + "Ghidra address-context verifier cleanup failed", + ), + // The caller can retry the retained workspace from the final failure. + workspaceFixture.released ? {} : { cleanupLeases: [workspaceFixture] }, ); } if (primaryFailure !== undefined) throw primaryFailure.cause; diff --git a/scripts/verify-real-ghidra-swift.mjs b/scripts/verify-real-ghidra-swift.mjs index 23ada4c16c..756eb2b352 100644 --- a/scripts/verify-real-ghidra-swift.mjs +++ b/scripts/verify-real-ghidra-swift.mjs @@ -1,19 +1,8 @@ #!/usr/bin/env node -import { - closeVerifierResources, - withVerifierCleanup, -} from "./lib/verifier-cleanup.mjs"; +import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; import assert from "node:assert/strict"; import { execFile } from "node:child_process"; -import { - copyFile, - mkdir, - mkdtemp, - readFile, - readdir, - rm, -} from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { copyFile, mkdir, readFile, readdir } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; @@ -27,6 +16,10 @@ import { } from "../dist/process/ProviderProcess.js"; import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; +import { + VerifierFixture, + withVerifierFixture, +} from "./lib/verifier-fixture.mjs"; const execute = promisify(execFile); const run = createVerifierRun(); @@ -47,7 +40,12 @@ try { { cause }, ); } -const root = await mkdtemp(join(tmpdir(), "rea-ghidra-swift-")); +// Ghidra children use the workspace as TMPDIR; a plain root admits them on every host. +const rootFixture = await VerifierFixture.open({ + prefix: "rea-ghidra-swift-", + private: false, +}); +const root = rootFixture.path; const client = new Client({ name: "ghidra-swift-verifier", version: "1" }); const transport = new StdioClientTransport({ command: process.execPath, @@ -79,7 +77,19 @@ let connected = false; let verifierRun; let literalReport; let decoderCleaned = true; -await withVerifierCleanup(async () => { +// The root is removed only after the session, client and decoder confirm. +rootFixture.releaseAfter(async () => { + if (connected) await call("close_binary", {}); +}); +rootFixture.releaseAfter(() => client.close()); +rootFixture.releaseAfter(() => transport.close()); +rootFixture.releaseAfter(async () => { + assert.equal(decoderCleaned, true, "Owned decoder cleanup remains pending"); + verifierRun = await completeVerifierRun(run); + assert.equal(verifierRun.process_lineage.status, "verified"); + assert.deepEqual(verifierRun.process_lineage.descendants, []); +}); +await withVerifierFixture(rootFixture, async () => { await execute("swiftc", ["-O", "-g", source, "-o", binary], { timeout: 60_000, }); @@ -193,22 +203,7 @@ await withVerifierCleanup(async () => { }, [() => call("close_binary", {})]); } literalReport = await verifyStringLiterals(); -}, [ - async () => { - await closeVerifierResources([ - async () => { - if (connected) await call("close_binary", {}); - }, - () => client.close(), - () => transport.close(), - ]); - assert.equal(decoderCleaned, true, "Owned decoder cleanup remains pending"); - verifierRun = await completeVerifierRun(run); - assert.equal(verifierRun.process_lineage.status, "verified"); - assert.deepEqual(verifierRun.process_lineage.descendants, []); - await rm(root, { recursive: true, force: true }); - }, -]); +}); console.log( JSON.stringify( { diff --git a/scripts/verify-real-ghidra-switch.mjs b/scripts/verify-real-ghidra-switch.mjs index fae601d5cd..6f3427fffc 100644 --- a/scripts/verify-real-ghidra-switch.mjs +++ b/scripts/verify-real-ghidra-switch.mjs @@ -10,13 +10,10 @@ import { createHash } from "node:crypto"; import { copyFile, mkdir, - mkdtemp, readFile, readdir, - rm, writeFile, } from "node:fs/promises"; -import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { parseArgs, promisify } from "node:util"; @@ -33,6 +30,10 @@ import { } from "./verify-real-ghidra-switch-assertions.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; import { verifySwitchEvidenceControls } from "./verify-real-ghidra-switch-controls.mjs"; +import { + VerifierFixture, + withVerifierFixture, +} from "./lib/verifier-fixture.mjs"; // Narrow compiler-shape lane; no cross compiler or game artifact prerequisites. const { values } = parseArgs({ @@ -88,7 +89,12 @@ if (!compilerOnly) { assert.equal(installation.status, "available", JSON.stringify(installation)); } const run = createVerifierRun(); -const workspace = await mkdtemp(join(tmpdir(), "rea-switch-proof-")); +// Ghidra children use the workspace as TMPDIR; a plain root admits them on every host. +const workspaceFixture = await VerifierFixture.open({ + prefix: "rea-switch-proof-", + private: false, +}); +const workspace = workspaceFixture.path; const runtime = join(workspace, "runtime"); await mkdir(runtime); const source = join(workspace, "switch.c"); @@ -96,7 +102,18 @@ await writeFile(source, buildSwitchSource()); const flags = ["-O2", "-g", "-fno-pie", "-no-pie"]; const results = []; let productionControls = null; -await withVerifierCleanup(async () => { +// The workspace is removed only once no owned runtime or process survives. +workspaceFixture.releaseAfter(async () => { + assert.deepEqual( + (await readdir(runtime)).filter((name) => name.startsWith("rea-ghidra-")), + [], + "Owned Ghidra runtime survived close", + ); + const cleanup = await completeVerifierRun(run); + assert.equal(cleanup.process_lineage.status, "verified"); + assert.deepEqual(cleanup.process_lineage.descendants, []); +}); +await withVerifierFixture(workspaceFixture, async () => { for (const compiler of ["gcc", "clang"]) { const original = join(workspace, `${compiler}-optimized`); await exec(commands[compiler], [...flags, source, "-o", original]); @@ -170,19 +187,7 @@ await withVerifierCleanup(async () => { }); } } -}, [ - async () => { - assert.deepEqual( - (await readdir(runtime)).filter((name) => name.startsWith("rea-ghidra-")), - [], - "Owned Ghidra runtime survived close", - ); - const cleanup = await completeVerifierRun(run); - assert.equal(cleanup.process_lineage.status, "verified"); - assert.deepEqual(cleanup.process_lineage.descendants, []); - await rm(workspace, { recursive: true, force: true }); - }, -]); +}); const lifecycle = await completeVerifierRun(run); if (lifecycle.process_lineage.status === "verified") assert.deepEqual(lifecycle.process_lineage.descendants, []); diff --git a/scripts/verify/architecture-guard-baselines.json b/scripts/verify/architecture-guard-baselines.json index 8e639a4332..890be7a11f 100644 --- a/scripts/verify/architecture-guard-baselines.json +++ b/scripts/verify/architecture-guard-baselines.json @@ -359,7 +359,6 @@ "src/server/createServer.ts": 1 }, "hand-caught-configuration-error": { - "src/application/binary/BinarySession.ts": 3, "src/application/McpClientRegistration.ts": 1, "src/application/McpRegistrationCatalog.ts": 1, "src/application/McpRegistrationGit.ts": 1, @@ -367,6 +366,7 @@ "src/application/SupportedClients.ts": 1, "src/application/Uninstall.ts": 2, "src/application/WebScriptExportService.ts": 1, + "src/application/binary/BinarySession.ts": 3, "src/ghidra/GhidraProviderClient.ts": 1, "src/ida/IdaConfiguration.ts": 1 }, @@ -418,12 +418,9 @@ "scripts/verify-pe-load-config.mjs": 1, "scripts/verify-published-package.mjs": 1, "scripts/verify-real-cutter.mjs": 1, - "scripts/verify-real-ghidra-address-context.mjs": 1, "scripts/verify-real-ghidra-checkpoints.mjs": 1, "scripts/verify-real-ghidra-function-bundle.mjs": 1, "scripts/verify-real-ghidra-project-coverage.mjs": 1, - "scripts/verify-real-ghidra-swift.mjs": 1, - "scripts/verify-real-ghidra-switch.mjs": 1, "scripts/verify-real-ghidra.mjs": 1, "scripts/verify-real-hopper-callers.mjs": 1, "scripts/verify-real-hopper-deadlines.mjs": 1,