From 1941d343b86b531354d7dec2d1ce0bba199d3659 Mon Sep 17 00:00:00 2001 From: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com> Date: Wed, 14 Oct 2026 08:23:55 +0800 Subject: [PATCH] fix(ghidra): preserve omitted PDB identity uncertainty --- src/ghidra/GhidraAnalysisProfile.ts | 2 +- src/ghidra/GhidraPeProfile.test.ts | 61 +++++++++++++++++++ src/ghidra/GhidraPeProfile.ts | 4 ++ src/ghidra/GhidraProvider.behavior.test.ts | 13 +++- tests/boundary/mcp/ghidraInventoryMcp.test.ts | 2 +- 5 files changed, 78 insertions(+), 4 deletions(-) diff --git a/src/ghidra/GhidraAnalysisProfile.ts b/src/ghidra/GhidraAnalysisProfile.ts index f731b3c62..9b6d6bf32 100644 --- a/src/ghidra/GhidraAnalysisProfile.ts +++ b/src/ghidra/GhidraAnalysisProfile.ts @@ -218,7 +218,7 @@ export const resolveGhidraAnalysisProfile = async ( ].sort(), import_mode: "ephemeral-source-immutable", ...(target.format === "pe" - ? { pdb_observations: "program-info-v4" } + ? { pdb_observations: "program-info-v5" } : {}), annotation_policy: "atomic-function-entry-metadata-v1", load_image_observations: "source-mappings-entry-context-v2", diff --git a/src/ghidra/GhidraPeProfile.test.ts b/src/ghidra/GhidraPeProfile.test.ts index 4bf441690..b8c869564 100644 --- a/src/ghidra/GhidraPeProfile.test.ts +++ b/src/ghidra/GhidraPeProfile.test.ts @@ -168,6 +168,67 @@ it("reports NB10 declared age without inventing a GUID", () => { expect(caveats[0]).not.toContain("GUID"); }); +it.each([false, null, true])( + "keeps an omitted NB10 GUID unknown with loaded = %s", + (loaded) => { + const pdbPath = "a".repeat(4070) + "\\legacy.pdb"; + const legacy = { + ...target, + codeView: { + pdbPath, + pdbFileName: "legacy.pdb", + guid: null, + age: 2, + }, + }; + const pdb = { + ...absentAttributes, + file: pdbPath, + age: "2", + signature: "01234567", + version: "NB10", + loaded, + }; + expect(ghidraPePdbLimitations(legacy, pdb)).toEqual( + ghidraPdbLimitations(pdb), + ); + const omitted: GhidraSessionInfo["pdb"] = { + ...pdb, + resource_limit: { omitted_attributes: ["guid"], utf16_budget: 4096 }, + }; + const caveats = ghidraPePdbLimitations(legacy, omitted); + expect(caveats).toEqual([ + ...ghidraPdbLimitations(omitted), + expect.stringContaining('PDB "legacy.pdb" (age (decimal) 2)'), + ]); + expect(caveats.at(-1)).toContain("equivalence is not established"); + }, +); + +it.each(["signature", "version"] as const)( + "keeps matching NB10 identity when only %s is omitted", + (attribute) => { + const legacy = { + ...target, + codeView: { + pdbPath: "legacy.pdb", + pdbFileName: "legacy.pdb", + guid: null, + age: 2, + }, + }; + const pdb: GhidraSessionInfo["pdb"] = { + ...absentAttributes, + file: "legacy.pdb", + age: "2", + resource_limit: { omitted_attributes: [attribute], utf16_budget: 4096 }, + }; + expect(ghidraPePdbLimitations(legacy, pdb)).toEqual( + ghidraPdbLimitations(pdb), + ); + }, +); + it("retains missing-report uncertainty without a declared CodeView record", () => { const { codeView: _, ...withoutReference } = target; expect(ghidraPePdbLimitations(withoutReference, undefined)).toEqual( diff --git a/src/ghidra/GhidraPeProfile.ts b/src/ghidra/GhidraPeProfile.ts index 300978c70..7f60edb91 100644 --- a/src/ghidra/GhidraPeProfile.ts +++ b/src/ghidra/GhidraPeProfile.ts @@ -15,6 +15,10 @@ const reportedPdbMatchesDeclaration = ( reported: NonNullable, ): boolean => { if ( + reported.resource_limit?.omitted_attributes.some( + (attribute) => + attribute === "file" || attribute === "guid" || attribute === "age", + ) || (reported.file !== declared.pdbPath && reported.file !== declared.pdbFileName) || reported.age === null || diff --git a/src/ghidra/GhidraProvider.behavior.test.ts b/src/ghidra/GhidraProvider.behavior.test.ts index 99e6a330e..bf97d8a48 100644 --- a/src/ghidra/GhidraProvider.behavior.test.ts +++ b/src/ghidra/GhidraProvider.behavior.test.ts @@ -85,7 +85,12 @@ const createElfClient = async (ghidra: GhidraProvider) => { return ghidra.createClient(target, resolved.value.profile); }; -it.each(["program-info-v1", "program-info-v2", "program-info-v3"])( +it.each([ + "program-info-v1", + "program-info-v2", + "program-info-v3", + "program-info-v4", +])( "retains historical PE PDB Evidence for %s without replaying its old projection", async (pdbObservations) => { const target = peTarget("x86_64"); @@ -99,7 +104,11 @@ it.each(["program-info-v1", "program-info-v2", "program-info-v3"])( }); const operation = "search_procedures"; const parameters = { pattern: "wmain" }; - const limitations = ["Legacy projection collapsed unknown PDB load state."]; + const limitations = [ + pdbObservations === "program-info-v4" + ? "Legacy projection discarded NB10 declaration when the reported GUID was omitted." + : "Legacy projection collapsed unknown PDB load state.", + ]; const cache = new AnalysisSnapshotCache(); cache.record({ target, diff --git a/tests/boundary/mcp/ghidraInventoryMcp.test.ts b/tests/boundary/mcp/ghidraInventoryMcp.test.ts index 92a0fc103..14894ba7d 100644 --- a/tests/boundary/mcp/ghidraInventoryMcp.test.ts +++ b/tests/boundary/mcp/ghidraInventoryMcp.test.ts @@ -315,7 +315,7 @@ it("retains missing PDB coverage in empty PE name-search Evidence and snapshot r const evidence = sessionEvidence(harness.session, reply.structuredContent); expect(evidence.normalized_result).toEqual([]); expect(evidence.analysis_profile?.parameters.pdb_observations).toBe( - "program-info-v4", + "program-info-v5", ); const limitation = evidence.limitations.find((value) => value.includes(pdb.file),