diff --git a/src/ghidra/GhidraPeProfile.test.ts b/src/ghidra/GhidraPeProfile.test.ts index 561aad7012..38db29bfc6 100644 --- a/src/ghidra/GhidraPeProfile.test.ts +++ b/src/ghidra/GhidraPeProfile.test.ts @@ -33,6 +33,7 @@ describe("ghidraPeCodeViewLimitations", () => { }, }; expect(ghidraPeCodeViewLimitations(peTarget, true)).toEqual([]); + expect(ghidraPeCodeViewLimitations(peTarget, undefined)).toEqual([]); }); it("returns empty limitations for PE target without codeView", () => { @@ -66,7 +67,7 @@ describe("ghidraPeCodeViewLimitations", () => { age: 1, }, }; - expect(ghidraPeCodeViewLimitations(peTarget)).toEqual([ + expect(ghidraPeCodeViewLimitations(peTarget, false)).toEqual([ 'The target references debug symbols "app.pdb" (GUID 01234567-89ab-cdef-0123-456789abcdef, age 1) that were not applied; procedure names are analysis-generated.', ]); }); @@ -88,7 +89,7 @@ describe("ghidraPeCodeViewLimitations", () => { age: 2, }, }; - expect(ghidraPeCodeViewLimitations(peTarget)).toEqual([ + expect(ghidraPeCodeViewLimitations(peTarget, false)).toEqual([ 'The target references debug symbols "legacy.pdb" (age 2) that were not applied; procedure names are analysis-generated.', ]); }); diff --git a/src/ghidra/GhidraPeProfile.ts b/src/ghidra/GhidraPeProfile.ts index 217b3256f3..46f53040a5 100644 --- a/src/ghidra/GhidraPeProfile.ts +++ b/src/ghidra/GhidraPeProfile.ts @@ -10,9 +10,13 @@ import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; */ export const ghidraPeCodeViewLimitations = ( target: BinaryTarget, - pdbLoaded = false, + pdbLoaded?: boolean | null, ): readonly string[] => { - if (target.format !== "pe" || target.codeView === undefined || pdbLoaded) { + if ( + target.format !== "pe" || + target.codeView === undefined || + pdbLoaded !== false + ) { return []; } const { pdbFileName, guid, age } = target.codeView; diff --git a/src/ghidra/GhidraProvider.behavior.test.ts b/src/ghidra/GhidraProvider.behavior.test.ts index 9d4892fc84..7bc0dd8b2f 100644 --- a/src/ghidra/GhidraProvider.behavior.test.ts +++ b/src/ghidra/GhidraProvider.behavior.test.ts @@ -956,36 +956,78 @@ describe("Ghidra result projection", () => { }); describe("Ghidra PE CodeView debug symbol limitations", () => { - it("projects unapplied companion PDB limitation for PE target with CodeView record", async () => { - const ghidra = provider(installationHost(), () => ({ - start: () => Promise.resolve(ok(sessionInfo())), - callTool: () => Promise.resolve(ok(null)), - close: () => Promise.resolve(ok(null)), - })); - const target = { - ...peTarget("x86_64"), - codeView: { - pdbPath: "C:\\projects\\app\\build\\test.pdb", - pdbFileName: "test.pdb", + it.each([ + { + name: "omitted PDB facts", + loaded: undefined, + expectedLimitation: + "Ghidra did not report PDB attributes; debug-symbol coverage is unknown.", + forbiddenLimitation: "that were not applied", + }, + { + name: "an explicit not-loaded report", + loaded: false, + expectedLimitation: + 'The target references debug symbols "test.pdb" (GUID 01234567-89ab-cdef-0123-456789abcdef, age 1) that were not applied; procedure names are analysis-generated.', + forbiddenLimitation: "coverage is unknown", + }, + { + name: "an explicit loaded report", + loaded: true, + expectedLimitation: "Ghidra reports a loaded PDB", + forbiddenLimitation: "that were not applied", + }, + ])( + "preserves CodeView coverage state for $name in session Evidence", + async ({ loaded, expectedLimitation, forbiddenLimitation }) => { + const pdb = { + file: "C:\\projects\\app\\build\\test.pdb", guid: "01234567-89ab-cdef-0123-456789abcdef", - age: 1, - }, - }; - const profile = await ghidra.resolveAnalysisProfile(target); - if (!profile.ok || profile.value.profile === null) - throw new Error("expected PE profile"); - const client = ghidra.createClient(target, profile.value.profile); - const health = await client.execute("health", {}); - expect(health).toMatchObject({ - ok: true, - value: { - limitations: expect.arrayContaining([ - 'The target references debug symbols "test.pdb" (GUID 01234567-89ab-cdef-0123-456789abcdef, age 1) that were not applied; procedure names are analysis-generated.', - ]), - }, - }); - await client.close(); - }); + age: "1", + signature: null, + version: "RSDS", + loaded: loaded ?? null, + }; + const ghidra = provider(installationHost(), () => ({ + start: () => + Promise.resolve( + ok( + loaded === undefined ? sessionInfo() : { ...sessionInfo(), pdb }, + ), + ), + callTool: () => Promise.resolve(ok([])), + close: () => Promise.resolve(ok(null)), + })); + const target = { + ...peTarget("x86_64"), + codeView: { + pdbPath: "C:\\projects\\app\\build\\test.pdb", + pdbFileName: "test.pdb", + guid: "01234567-89ab-cdef-0123-456789abcdef", + age: 1, + }, + }; + const profile = await ghidra.resolveAnalysisProfile(target); + if (!profile.ok || profile.value.profile === null) + throw new Error("expected PE profile"); + const client = ghidra.createClient(target, profile.value.profile); + try { + const search = await client.execute("search_procedures", { + pattern: "add_numbers", + }); + if (!search.ok) throw search.error; + const health = await client.execute("health", {}); + if (!health.ok) throw health.error; + for (const observation of [search.value, health.value]) { + const limitations = observation.limitations.join("\n"); + expect(limitations).toContain(expectedLimitation); + expect(limitations).not.toContain(forbiddenLimitation); + } + } finally { + await client.close(); + } + }, + ); }); describe("Ghidra relationship coverage", () => { diff --git a/src/ghidra/GhidraProviderClient.ts b/src/ghidra/GhidraProviderClient.ts index 80c37c0be2..738c09363c 100644 --- a/src/ghidra/GhidraProviderClient.ts +++ b/src/ghidra/GhidraProviderClient.ts @@ -532,7 +532,7 @@ export const createGhidraProviderClient = (input: { // Replaced by the reported outcome once the session handshake is checked. const seedLimitations: string[] = [...ghidraSeedLimitations(seeds)]; const pdbLimitations: string[] = []; - let pdbLoaded = false; + let pdbLoaded: boolean | null | undefined; // Replaced by the reported storage outcome once the handshake is checked. const softFloatLimitations: string[] = [ ...ghidraMipsSoftFloatLimitations(softFloat), @@ -663,7 +663,7 @@ export const createGhidraProviderClient = (input: { info: GhidraSessionInfo, ): Promise => { if (target.format === "pe") { - pdbLoaded = info.pdb?.loaded === true; + pdbLoaded = info.pdb?.loaded; pdbLimitations.splice( 0, pdbLimitations.length,