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
This commit is contained in:
morluto authored and GitHub committed 2026-11-15 21:05:34 +08:00
1 parent 44d359bd05
commit aec679584a
4 files changed
+75 -86

No files matched your search

+25 -33
View File
@@ -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;
+26 -31
View File
@@ -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(
{
+23 -18
View File
@@ -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, []);
@@ -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,