fix(ghidra): retain conflicting PDB identity observations
Compare declared CodeView identity with recognized Program GUID and hexadecimal age formats. Preserve both sources when fields conflict or equivalence cannot be established. Keep known Program identity alongside loaded-state attestation caveats at the projection owner, and revise PE snapshot coverage to program-info-v4. Validation: 246 associated tests across 15 suites, runtime build, typecheck and changed-file lint pass; two Windows-only cases skip on Linux. Five conflict/unknown-format cases reproduce before the comparison fix; their loaded-state variants also expose the original Program projection dropping identity fields.
This commit is contained in:
parent
2e86757063
commit
c121db7a47
|
|
@ -218,7 +218,7 @@ export const resolveGhidraAnalysisProfile = async (
|
|||
].sort(),
|
||||
import_mode: "ephemeral-source-immutable",
|
||||
...(target.format === "pe"
|
||||
? { pdb_observations: "program-info-v3" }
|
||||
? { pdb_observations: "program-info-v4" }
|
||||
: {}),
|
||||
annotation_policy: "atomic-function-entry-metadata-v1",
|
||||
load_image_observations: "source-mappings-entry-context-v2",
|
||||
|
|
|
|||
|
|
@ -70,9 +70,9 @@ it.each([true, false, null])(
|
|||
"uses reported PDB observations once with loaded = %s",
|
||||
(loaded) => {
|
||||
const pdb = {
|
||||
file: "reported.pdb",
|
||||
guid: "reported-guid",
|
||||
age: "a",
|
||||
file: "public.pdb",
|
||||
guid: "{01234567-89AB-CDEF-0123-456789ABCDEF}",
|
||||
age: "0X000A",
|
||||
signature: null,
|
||||
version: "RSDS",
|
||||
loaded,
|
||||
|
|
@ -80,10 +80,43 @@ it.each([true, false, null])(
|
|||
const caveats = ghidraPePdbLimitations(target, pdb);
|
||||
expect(caveats).toEqual(ghidraPdbLimitations(pdb));
|
||||
expect(caveats).toHaveLength(1);
|
||||
expect(caveats.join(" ")).not.toContain("public.pdb");
|
||||
expect(caveats.join(" ")).not.toContain('PDB "public.pdb"');
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ file: "different.pdb", guid: "0123456789abcdef0123456789abcdef", age: "a" },
|
||||
{ file: "public.pdb", guid: "1123456789abcdef0123456789abcdef", age: "a" },
|
||||
{ file: "public.pdb", guid: "0123456789abcdef0123456789abcdef", age: "10" },
|
||||
{ file: "public.pdb", guid: "unrecognized-guid", age: "a" },
|
||||
{ file: "public.pdb", guid: "0123456789abcdef0123456789abcdef", age: "ten" },
|
||||
])("retains non-equivalent declared PDB identity for %j", (identity) => {
|
||||
for (const loaded of [false, null, true]) {
|
||||
const pdb = { ...absentAttributes, ...identity, version: "RSDS", loaded };
|
||||
const caveats = ghidraPePdbLimitations(target, pdb);
|
||||
expect(caveats[0]).toBe(ghidraPdbLimitations(pdb)[0]);
|
||||
expect(caveats[0]).toContain(identity.file);
|
||||
expect(caveats[0]).toContain(identity.guid);
|
||||
expect(caveats[0]).toContain(`age (hex) ${identity.age}`);
|
||||
expect(caveats).toHaveLength(2);
|
||||
expect(caveats[1]).toContain('PDB "public.pdb"');
|
||||
expect(caveats[1]).toContain("01234567-89ab-cdef-0123-456789abcdef");
|
||||
expect(caveats[1]).toContain("age (decimal) 10");
|
||||
expect(caveats[1]).toContain("equivalence is not established");
|
||||
expect(pdb).toMatchObject({ ...identity, loaded });
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
"0123456789ABCDEF0123456789ABCDEF",
|
||||
"01234567-89ab-cdef-0123-456789abcdef",
|
||||
])("avoids duplicate identity for normalized matching GUID %s", (guid) => {
|
||||
const pdb = { ...absentAttributes, file: "public.pdb", guid, age: "a" };
|
||||
expect(ghidraPePdbLimitations(target, pdb)).toEqual(
|
||||
ghidraPdbLimitations(pdb),
|
||||
);
|
||||
});
|
||||
|
||||
it("retains explicit failure to load without asserting all names are generated", () => {
|
||||
const caveats = ghidraPePdbLimitations(target, {
|
||||
...absentAttributes,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import type {
|
||||
BinaryTarget,
|
||||
PeCodeViewRecord,
|
||||
} from "../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
ghidraPdbLimitations,
|
||||
type GhidraSessionInfo,
|
||||
|
|
@ -6,6 +9,35 @@ import {
|
|||
|
||||
const PDB_LOCATOR_INLINE_BYTE_BUDGET = 1024;
|
||||
|
||||
/** Establish equivalence only for the declared fields and recognized formats. */
|
||||
const reportedPdbMatchesDeclaration = (
|
||||
declared: PeCodeViewRecord,
|
||||
reported: NonNullable<GhidraSessionInfo["pdb"]>,
|
||||
): boolean => {
|
||||
if (
|
||||
(reported.file !== declared.pdbPath &&
|
||||
reported.file !== declared.pdbFileName) ||
|
||||
reported.age === null ||
|
||||
!/^(?:0x)?[0-9a-f]+$/iu.test(reported.age) ||
|
||||
Number.parseInt(reported.age, 16) !== declared.age
|
||||
)
|
||||
return false;
|
||||
if (declared.guid === null) return reported.guid === null;
|
||||
if (reported.guid === null) return false;
|
||||
let guid = reported.guid;
|
||||
if (guid.startsWith("{") && guid.endsWith("}")) guid = guid.slice(1, -1);
|
||||
if (
|
||||
!/^(?:[0-9a-f]{32}|[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})$/iu.test(
|
||||
guid,
|
||||
)
|
||||
)
|
||||
return false;
|
||||
return (
|
||||
guid.replaceAll("-", "").toLowerCase() ===
|
||||
declared.guid.replaceAll("-", "").toLowerCase()
|
||||
);
|
||||
};
|
||||
|
||||
/** Measure JSON quotes, escapes and UTF-8 without allocating the encoded string. */
|
||||
const pdbLocatorFitsInlineBudget = (value: string): boolean => {
|
||||
if (value.length * 6 + 2 <= PDB_LOCATOR_INLINE_BYTE_BUDGET) return true;
|
||||
|
|
@ -47,10 +79,7 @@ export const ghidraPePdbLimitations = (
|
|||
const reported = ghidraPdbLimitations(pdb);
|
||||
if (
|
||||
target.codeView === undefined ||
|
||||
(pdb !== undefined &&
|
||||
pdb.file !== null &&
|
||||
pdb.age !== null &&
|
||||
(target.codeView.guid === null || pdb.guid !== null))
|
||||
(pdb !== undefined && reportedPdbMatchesDeclaration(target.codeView, pdb))
|
||||
)
|
||||
return reported;
|
||||
const { pdbFileName, guid, age } = target.codeView;
|
||||
|
|
@ -70,6 +99,11 @@ export const ghidraPePdbLimitations = (
|
|||
: pdb.loaded === true
|
||||
? "Ghidra reports a loaded PDB; this declared identity is not an independent matching-PDB attestation"
|
||||
: "Ghidra reports load state unknown";
|
||||
const declared = `The PE CodeView record references PDB ${locator} (${identity}); ${state}. Original PDB names and types may be missing. Use list_procedures and address-based analysis to inspect recovered functions. REA does not admit a caller-selected PDB companion.`;
|
||||
const comparison =
|
||||
pdb !== undefined &&
|
||||
[pdb.file, pdb.guid, pdb.age].some((value) => value !== null)
|
||||
? " Reported and declared PDB observations are retained separately; equivalence is not established."
|
||||
: "";
|
||||
const declared = `The PE CodeView record references PDB ${locator} (${identity}); ${state}.${comparison} Original PDB names and types may be missing. Use list_procedures and address-based analysis to inspect recovered functions. REA does not admit a caller-selected PDB companion.`;
|
||||
return pdb === undefined ? [declared] : [...reported, declared];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ const createElfClient = async (ghidra: GhidraProvider) => {
|
|||
return ghidra.createClient(target, resolved.value.profile);
|
||||
};
|
||||
|
||||
it.each(["program-info-v1", "program-info-v2"])(
|
||||
it.each(["program-info-v1", "program-info-v2", "program-info-v3"])(
|
||||
"retains historical PE PDB Evidence for %s without replaying its old projection",
|
||||
async (pdbObservations) => {
|
||||
const target = peTarget("x86_64");
|
||||
|
|
|
|||
|
|
@ -121,21 +121,26 @@ export const ghidraPdbLimitations = (
|
|||
limitations.push(
|
||||
`PDB attributes ${pdb.resource_limit.omitted_attributes.join(", ")} were omitted because the handshake's ${pdb.resource_limit.utf16_budget}-UTF-16-unit PDB attribute budget was exhausted. Omitted values remain unknown; they were not truncated or reported as absent.`,
|
||||
);
|
||||
const hasReference = [
|
||||
pdb.file,
|
||||
pdb.guid,
|
||||
pdb.age,
|
||||
pdb.signature,
|
||||
pdb.version,
|
||||
].some((value) => value !== null);
|
||||
const reference = hasReference
|
||||
? `The Ghidra Program references PDB ${pdb.file ?? "(file unknown)"} (GUID ${pdb.guid ?? "unknown"}, age (hex) ${pdb.age ?? "unknown"}, signature ${pdb.signature ?? "unknown"}, version ${pdb.version ?? "unknown"}); `
|
||||
: "";
|
||||
if (pdb.loaded === true)
|
||||
return [
|
||||
...limitations,
|
||||
"Ghidra reports a loaded PDB; this is not an independent PDB identity or complete symbol/type-coverage attestation. Original names and types may still be missing; an empty name search does not prove a procedure is absent. Use list_procedures and address-based analysis to inspect recovered functions.",
|
||||
`${reference}Ghidra reports a loaded PDB; this is not an independent PDB identity or complete symbol/type-coverage attestation. Original names and types may still be missing; an empty name search does not prove a procedure is absent. Use list_procedures and address-based analysis to inspect recovered functions.`,
|
||||
];
|
||||
if (
|
||||
[pdb.file, pdb.guid, pdb.age, pdb.signature, pdb.version].every(
|
||||
(value) => value === null,
|
||||
)
|
||||
)
|
||||
return limitations;
|
||||
if (!hasReference) return limitations;
|
||||
const state = pdb.loaded === false ? "not loaded" : "load state unknown";
|
||||
return [
|
||||
...limitations,
|
||||
`The target references PDB ${pdb.file ?? "(file unknown)"} (GUID ${pdb.guid ?? "unknown"}, age (hex) ${pdb.age ?? "unknown"}, signature ${pdb.signature ?? "unknown"}, version ${pdb.version ?? "unknown"}); Ghidra reports ${state}. Original names and types may be missing; an empty name search does not prove a procedure is absent. Use list_procedures and address-based analysis to inspect recovered functions. REA does not admit a caller-selected PDB companion.`,
|
||||
`${reference}Ghidra reports ${state}. Original names and types may be missing; an empty name search does not prove a procedure is absent. Use list_procedures and address-based analysis to inspect recovered functions. REA does not admit a caller-selected PDB companion.`,
|
||||
];
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue