diff --git a/scripts/verify-conformance-fixtures.mjs b/scripts/verify-conformance-fixtures.mjs index 7cf09addc0..2151a67db7 100644 --- a/scripts/verify-conformance-fixtures.mjs +++ b/scripts/verify-conformance-fixtures.mjs @@ -3,7 +3,11 @@ import { readFile } from "node:fs/promises"; import { dirname, join, resolve } from "node:path"; import { promisify } from "node:util"; -import { sha256 } from "./lib/conformance-fixtures.mjs"; +import { + generateLargeFixture, + sha256, + sourceDigest, +} from "./lib/conformance-fixtures.mjs"; const exec = promisify(execFile); const manifestPath = resolve( @@ -16,6 +20,23 @@ const root = dirname(manifestPath); for (const fixture of manifest.fixtures) { const artifactPath = join(root, fixture.artifact); + const sources = []; + for (const source of fixture.sources ?? []) { + const content = + source.path === "generated/large.c" + ? generateLargeFixture() + : await readFile(join(process.cwd(), "tests/conformance", source.path)); + if (sha256(content) !== source.sha256) + throw new Error( + `${fixture.name}: source hash mismatch for ${source.path}`, + ); + sources.push({ path: source.path, content }); + } + if ( + fixture.sourceSha256 !== undefined && + sourceDigest(sources) !== fixture.sourceSha256 + ) + throw new Error(`${fixture.name}: source manifest digest mismatch`); const artifact = await readFile(artifactPath); if (sha256(artifact) !== fixture.artifactSha256) throw new Error(`${fixture.name}: artifact hash mismatch`); diff --git a/scripts/verify-real-hopper.mjs b/scripts/verify-real-hopper.mjs index c45a79db80..51cd77f1f8 100644 --- a/scripts/verify-real-hopper.mjs +++ b/scripts/verify-real-hopper.mjs @@ -1,4 +1,6 @@ import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { createReadStream } from "node:fs"; import { readdir, realpath } from "node:fs/promises"; import { promisify } from "node:util"; @@ -8,6 +10,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js"; import { firstProcedureAddress, + requireDistinctTargetHashes, requireAddressArray, requireFunctionDossier, requirePseudocode, @@ -253,6 +256,11 @@ const [targetA, targetB] = await Promise.all([ ]); if (targetA === targetB) throw new Error("Real-Hopper verification requires two distinct targets"); +const [targetHashA, targetHashB] = await Promise.all([ + sha256File(targetA), + sha256File(targetB), +]); +requireDistinctTargetHashes(targetHashA, targetHashB); const serverEnvironment = { ...process.env }; delete serverEnvironment.HOPPER_TARGET_PATH; delete serverEnvironment.HOPPER_SECOND_TARGET_PATH; @@ -377,6 +385,7 @@ try { diagnosticCount, dynamicSession: true, targets: [targetA, targetB], + targetHashes: [targetHashA, targetHashB], switched: true, secondOverview: verifiedSecondOverview, }; @@ -422,3 +431,9 @@ await new Promise((resolve, reject) => { }, ); }); + +async function sha256File(path) { + const hash = createHash("sha256"); + for await (const chunk of createReadStream(path)) hash.update(chunk); + return hash.digest("hex"); +} diff --git a/src/application/RealHopperAssertions.ts b/src/application/RealHopperAssertions.ts index 1e815f865d..45e12d22b1 100644 --- a/src/application/RealHopperAssertions.ts +++ b/src/application/RealHopperAssertions.ts @@ -27,6 +27,23 @@ export const firstProcedureAddress = (input: unknown): string => { return first.address; }; +/** Reject two target paths that resolve to the same binary content. */ +export const requireDistinctTargetHashes = ( + firstHash: unknown, + secondHash: unknown, +): void => { + if ( + typeof firstHash !== "string" || + typeof secondHash !== "string" || + firstHash.length === 0 || + secondHash.length === 0 + ) { + throw new TypeError("Target hashes must be non-empty strings"); + } + if (firstHash === secondHash) + throw new TypeError("Real-Hopper verification requires distinct binaries"); +}; + /** Reject empty and success-shaped embedded decompilation failures. */ export const requirePseudocode = ( input: unknown, diff --git a/tests/realHopperAssertions.test.ts b/tests/realHopperAssertions.test.ts index 0140ac591e..fe6d0424ed 100644 --- a/tests/realHopperAssertions.test.ts +++ b/tests/realHopperAssertions.test.ts @@ -2,12 +2,20 @@ import { describe, expect, it } from "vitest"; import { firstProcedureAddress, + requireDistinctTargetHashes, requireAddressArray, requireFunctionDossier, requirePseudocode, } from "../src/application/RealHopperAssertions.js"; describe("real Hopper semantic assertions", () => { + it("rejects two target paths containing the same binary", () => { + expect(() => requireDistinctTargetHashes("same", "same")).toThrow( + /distinct binaries/u, + ); + expect(() => requireDistinctTargetHashes("", "other")).toThrow(); + expect(() => requireDistinctTargetHashes("first", "second")).not.toThrow(); + }); it("extracts only a real procedure address from a structured page", () => { expect( firstProcedureAddress({