fix(ghidra): validate seed outcome conservation (#9969)

This commit is contained in:
morluto authored and GitHub committed 2026-11-11 17:24:20 +08:00
1 parent b4654b493d
commit d3eb3f6478
2 files changed
+114 -14

No files matched your search

+78
View File
@@ -17,8 +17,13 @@ import { parseConfig } from "../config/parseConfig.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { silentLogger } from "../logger.js";
import {
GHIDRA_SESSION_CAPABILITIES,
parseGhidraSessionInfo,
} from "./GhidraSessionValues.js";
import {
ghidraSeedFailure,
ghidraSeedReportSchema,
ghidraSeedLimitations,
parseGhidraSeeds,
resolveGhidraAnalysisSeeds,
@@ -252,6 +257,79 @@ describe("Ghidra seed session verification", () => {
unmapped: 0,
};
it.each([100, 0])(
"rejects a handshake whose outcome counters report %i created functions",
(function_created) => {
const expected = {
runId: "d6fcbb66-e829-4ff6-a535-0035aec63139",
providerVersion: "12.1.4",
profileDigest: "a".repeat(64),
targetSha256: "b".repeat(64),
};
const parsed = parseGhidraSessionInfo(
{
name: "REA Ghidra bridge",
run_id: expected.runId,
profile_digest: expected.profileDigest,
provider: { id: "ghidra", version: expected.providerVersion },
read_only: false,
analysis_complete: true,
analysis_timed_out: false,
capabilities: [...GHIDRA_SESSION_CAPABILITIES],
target: {
name: "fixture",
language_id: "x86:LE:64:default",
compiler_spec_id: "gcc",
image_base: "0x0",
default_address_space: "ram",
sha256: expected.targetSha256,
},
analysis_seeds: { ...report, function_created },
},
expected,
);
expect(parsed).toMatchObject({
ok: false,
error: { code: "handshake_identity" },
});
},
);
it.each([
{ function_created: 3, function_failed: 0, code_decoded: 0 },
{ function_created: 0, function_failed: 0, code_decoded: 3 },
{ function_created: 0, function_failed: 0, label_applied: 3 },
])(
"rejects outcomes attributed to more seeds of a kind than committed: %j",
(changed) => {
const misattributed = { ...report, ...changed };
expect(ghidraSeedReportSchema.safeParse(misattributed).success).toBe(
true,
);
expect(
ghidraSeedFailure(commitment, misattributed, "xrefs"),
).toBeDefined();
},
);
it("admits producer outcomes including aggregate unmapped seeds without inventing their kinds", () => {
for (const outcome of [
report,
{
...report,
function_created: 0,
function_failed: 0,
code_decoded: 0,
label_applied: 0,
unmapped: 4,
},
{ ...report, function_created: 0, code_decoded: 0, unmapped: 2 },
]) {
expect(ghidraSeedReportSchema.safeParse(outcome).success).toBe(true);
expect(ghidraSeedFailure(commitment, outcome, "xrefs")).toBeUndefined();
}
});
it("accepts exactly the committed seeds and reports their outcome", () => {
expect(ghidraSeedFailure(commitment, report, "xrefs")).toBeUndefined();
expect(ghidraSeedFailure(undefined, undefined, "xrefs")).toBeUndefined();
+36 -14
View File
@@ -37,19 +37,33 @@ export const ghidraSeedCommitmentSchema = z.strictObject({
export type GhidraSeedCommitment = z.infer<typeof ghidraSeedCommitmentSchema>;
/** What the pre-analysis script applied, as reported by the bridge handshake. */
export const ghidraSeedReportSchema = z.strictObject({
format: z.literal(GHIDRA_SEED_FORMAT),
sha256: digest,
entries: count,
function_created: count,
function_existing: count,
function_failed: count,
code_decoded: count,
code_failed: count,
label_applied: count,
label_failed: count,
unmapped: count,
});
export const ghidraSeedReportSchema = z
.strictObject({
format: z.literal(GHIDRA_SEED_FORMAT),
sha256: digest,
entries: count,
function_created: count,
function_existing: count,
function_failed: count,
code_decoded: count,
code_failed: count,
label_applied: count,
label_failed: count,
unmapped: count,
})
.refine(
(report) =>
report.function_created +
report.function_existing +
report.function_failed +
report.code_decoded +
report.code_failed +
report.label_applied +
report.label_failed +
report.unmapped ===
report.entries,
{ message: "Seed outcomes must account for exactly the reported entries." },
);
export type GhidraSeedReport = z.infer<typeof ghidraSeedReportSchema>;
type SeedKind = "function" | "code" | "label";
@@ -267,7 +281,15 @@ export const ghidraSeedFailure = (
: report === undefined
? "The Ghidra session did not report the committed analysis seeds."
: report.sha256 !== commitment.sha256 ||
report.entries !== totalEntries(commitment)
report.entries !== totalEntries(commitment) ||
report.function_created +
report.function_existing +
report.function_failed >
commitment.entries.function ||
report.code_decoded + report.code_failed >
commitment.entries.code ||
report.label_applied + report.label_failed >
commitment.entries.label
? "The Ghidra session applied analysis seeds that differ from the committed profile."
: undefined;
return reason === undefined