fix(verify): restore owned firmware fixture hashing

Replace the removed hashFirmwareFile export with shared stable verifier hashing.
Keep hashing under the file scope and close MCP/file owners independently before
removing their fixture parent. Preserve the primary failure if cleanup also fails.

The actual firmware entrypoint now reaches its prerequisite gate; descriptor
retry and mixed-cleanup controls pass. Real Linux firmware-engine qualification
was not run here. Refs #4912.
This commit is contained in:
morluto
2026-10-18 07:34:07 +08:00
parent 7d79a6a85b
commit f4456dc196
2 changed files with 48 additions and 6 deletions
+29 -6
View File
@@ -9,7 +9,9 @@ import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { firmwareResultSchemas } from "../../../dist/domain/firmware/firmwareAnalysis.js";
import { parseEvidence } from "../../../dist/domain/evidence.js";
import { hashFirmwareFile } from "../../../dist/firmware/FirmwareFiles.js";
import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceScope.js";
import { hashVerifierFile } from "../../lib/verifier-file-resources.mjs";
import { closeVerifierResources } from "../../lib/verifier-cleanup.mjs";
const execFileAsync = promisify(execFile);
const { join, resolve, dirname } = nodePath;
@@ -79,7 +81,7 @@ const oracle = JSON.parse(
await fs.readFile(join(FIXTURE_ROOT, "oracle.json"), "utf8"),
);
const firmwarePath = join(FIXTURE_ROOT, "firmware.bin");
assert.equal(await hashFirmwareFile(firmwarePath), oracle.sha256);
const fileResources = new ArtifactResourceScope();
const workDir = await fs.mkdtemp(join(tmpdir(), "rea-real-firmware-"));
const baseEnv = Object.fromEntries(
@@ -160,7 +162,10 @@ async function verifyFiles(evidence) {
assert.ok(entry !== undefined, `Missing extracted ${suffix}`);
assert.equal(entry.sha256, expected.sha256);
assert.equal(entry.size, expected.size);
assert.equal(await hashFirmwareFile(entry.path), expected.sha256);
assert.equal(
(await hashVerifierFile(entry.path, fileResources)).sha256,
expected.sha256,
);
assert.equal(entry.original_file_range, null);
assert.equal(entry.runtime_address, null);
}
@@ -371,7 +376,12 @@ async function stageGhidra(mcpFiles) {
// Entry point
// ---------------------------------------------------------------------------
let primaryError;
try {
assert.equal(
(await hashVerifierFile(firmwarePath, fileResources)).sha256,
oracle.sha256,
);
const cli = await stageCli();
await connectMcp();
const mcpFiles = await stageMcp(cli);
@@ -381,11 +391,24 @@ try {
await stageGhidra(mcpFiles);
assert.equal(
await hashFirmwareFile(firmwarePath),
(await hashVerifierFile(firmwarePath, fileResources)).sha256,
oracle.sha256,
"Original firmware bytes must remain unchanged",
);
} catch (cause) {
primaryError = cause;
throw cause;
} finally {
await mcpClient.close();
await fs.rm(workDir, { recursive: true, force: true });
await closeVerifierResources(
[
async () => {
await closeVerifierResources([
() => mcpClient.close(),
() => fileResources.close(),
]);
await fs.rm(workDir, { recursive: true, force: true });
},
],
primaryError,
);
}
@@ -134,6 +134,25 @@ it("Ghidra verifier probes await the shared owned inspection contract", async ()
expect(violations).toEqual([]);
});
it("firmware verifier loads its current hash API before reporting unavailable prerequisites", () => {
const result = spawnSync(
process.execPath,
["scripts/verify/firmware/analysis.mjs"],
{
encoding: "utf8",
timeout: 10_000,
env: { PATH: process.env.PATH ?? "" },
},
);
expect(result.status).toBe(1);
expect(result.stderr).toContain(
process.platform === "linux"
? "requires REA_BINWALK_COMMAND"
: "requires Linux",
);
expect(result.stderr).not.toContain("does not provide an export");
});
it.each(["late-close", "retry-close", "retained-close", "opaque-cancellation"])(
"owns Ghidra verifier prerequisite finalization: %s",
async (mode) => {