fix(ghidra): preserve unknown PDB application state

Use parsed PDB loaded facts without collapsing omitted/null state to false. Only an explicit false reports unapplied symbols; operation and health Evidence retain the same uncertainty. Controlled bridge validation only.

Fixes #2534
This commit is contained in:
morluto committed 2026-10-14 14:47:26 +08:00
1 parent 778114a1ab
commit bcc884556e
4 files changed
+82 -35

No files matched your search

+3 -2
View File
@@ -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.',
]);
});
+6 -2
View File
@@ -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;
+71 -29
View File
@@ -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", () => {
+2 -2
View File
@@ -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<AnalysisError | undefined> => {
if (target.format === "pe") {
pdbLoaded = info.pdb?.loaded === true;
pdbLoaded = info.pdb?.loaded;
pdbLimitations.splice(
0,
pdbLimitations.length,