fix(ghidra): report unapplied PE CodeView PDB limitation (#2283)

* fix(ghidra): report unapplied PE CodeView PDB limitation (#2259)

When Ghidra analyzes a PE target that references companion debug symbols
in its CodeView (IMAGE_DEBUG_TYPE_CODEVIEW / RSDS or NB10) debug directory,
Ghidra's headless analyzer cannot automatically locate or apply the companion
PDB from isolated runtime directories without companion admission. Procedure
names remain analysis-generated (FUN_<address>).

Parse CodeView debug metadata (RSDS GUID/age and NB10 age) during PE target
resolution, and report an Evidence limitation informing callers and agents
that referenced debug symbols were not applied and procedure names are
analysis-generated.

Fixes #2259

* test(pe): group CodeView header coverage separately

* fix(ghidra): avoid contradictory PDB limitations

* fix(android): make DEX descriptor pattern portable

* fix(ci): isolate Linux Hopper process fixture inventory

* fix(comparison): recheck historical calls-only changes (#2223)

* fix(comparison): preserve incomplete function call evidence

Keep missing, unknown, partial and unresolved call relationships unknown in function comparisons and reconstruction claims. Aggregate IDA caller and callee coverage without overwriting uncertainty.

Validate with 144 focused tests, typecheck/lint, docs checks and test discovery. Real providers were not verified; the full headless IDA session suite requires unavailable Windows native controls.

* fix(comparison): recheck historical calls-only changes

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>

* style: format identity comparison and IDA runner

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: Lucien <164857476+dakjdakd@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-14 02:56:26 +08:00
1 parent 4481f86188
commit 729ca2ab6a
9 files changed
+607 -7

No files matched your search

+95 -3
View File
@@ -40,10 +40,14 @@ import {
} from "../domain/zipPackageFormat.js";
import {
parseCodeViewData,
parseExecutableHeader,
parsePeRecord,
peDebugDirectoryEntry,
peRvaToFileOffset,
type ExecutableMetadata,
} from "../domain/binaryTarget.js";
import type { PeCodeViewRecord } from "../domain/binaryTargetTypes.js";
/** Options for resolving a local binary target and its stable identity. */
export interface BinaryTargetResolverOptions {
@@ -549,10 +553,98 @@ const readPeMetadata = async (
return err(`unsupported ${windowsHeader.signature} executable in MZ image`);
const optionalHeaderSize = fileHeader.readUInt16LE(20);
if (optionalHeaderSize > 4096) return err("invalid PE optional header size");
const numberOfSections = fileHeader.readUInt16LE(6);
const record = Buffer.alloc(24 + optionalHeaderSize);
const recordRead = await handle.read(record, 0, record.length, offset);
throwIfTargetResolutionCancelled(signal);
return recordRead.bytesRead === record.length
? parsePeRecord(record)
: err("invalid or truncated PE header");
if (recordRead.bytesRead !== record.length)
return err("invalid or truncated PE header");
const codeView = await readPeCodeView(
handle,
offset,
numberOfSections,
record.subarray(24),
optionalHeaderSize,
signal,
);
return parsePeRecord(record, codeView);
};
const readPeCodeView = async (
handle: FileHandle,
peOffset: number,
numberOfSections: number,
optionalHeader: Buffer,
optionalHeaderSize: number,
signal?: AbortSignal,
): Promise<PeCodeViewRecord | undefined> => {
throwIfTargetResolutionCancelled(signal);
const debugDir = peDebugDirectoryEntry(optionalHeader);
if (debugDir === undefined || numberOfSections > 96) return undefined;
const sectionTableOffset = peOffset + 24 + optionalHeaderSize;
const sectionTableSize = numberOfSections * 40;
const sectionTable = Buffer.alloc(sectionTableSize);
const sectionRead = await handle.read(
sectionTable,
0,
sectionTableSize,
sectionTableOffset,
);
throwIfTargetResolutionCancelled(signal);
if (sectionRead.bytesRead !== sectionTableSize) return undefined;
const debugFileOffset = peRvaToFileOffset(
debugDir.debugRva,
sectionTable,
numberOfSections,
debugDir.sizeOfHeaders,
Math.min(debugDir.debugSize, 4096),
);
if (debugFileOffset === undefined) return undefined;
const maxDebugSize = Math.min(debugDir.debugSize, 4096);
const debugDirectoryBuffer = Buffer.alloc(maxDebugSize);
const debugDirRead = await handle.read(
debugDirectoryBuffer,
0,
maxDebugSize,
debugFileOffset,
);
throwIfTargetResolutionCancelled(signal);
const numEntries = Math.floor(debugDirRead.bytesRead / 28);
for (let j = 0; j < numEntries; j++) {
const entryOffset = j * 28;
const type = debugDirectoryBuffer.readUInt32LE(entryOffset + 12);
if (type !== 2) continue; // IMAGE_DEBUG_TYPE_CODEVIEW
const sizeOfData = debugDirectoryBuffer.readUInt32LE(entryOffset + 16);
let pointerToRawData = debugDirectoryBuffer.readUInt32LE(entryOffset + 24);
if (pointerToRawData === 0) {
const addressOfRawData = debugDirectoryBuffer.readUInt32LE(
entryOffset + 20,
);
const mapped = peRvaToFileOffset(
addressOfRawData,
sectionTable,
numberOfSections,
debugDir.sizeOfHeaders,
Math.min(sizeOfData, 4096),
);
if (mapped === undefined) continue;
pointerToRawData = mapped;
}
if (sizeOfData < 16) continue;
const maxCodeViewRead = Math.min(sizeOfData, 4096);
const codeViewBuffer = Buffer.alloc(maxCodeViewRead);
const codeViewRead = await handle.read(
codeViewBuffer,
0,
maxCodeViewRead,
pointerToRawData,
);
throwIfTargetResolutionCancelled(signal);
const parsed = parseCodeViewData(
codeViewBuffer.subarray(0, codeViewRead.bytesRead),
sizeOfData,
);
if (parsed !== undefined) return parsed;
}
return undefined;
};
+72 -2
View File
@@ -58,20 +58,67 @@ export const fat = (
return bytes;
};
/** Build a bounded PE image with optional managed metadata. */
export interface PeCodeViewFixture {
readonly type?: "RSDS" | "NB10";
readonly guid?: string;
readonly age?: number;
readonly pdbPath?: string;
}
/** Build a bounded PE image with optional managed metadata or CodeView debug data. */
export const pe = (
machine: number,
offset = 64,
characteristics = 0x0002,
managed = false,
codeView?: PeCodeViewFixture,
): Buffer => {
const optionalHeaderSize =
machine === 0x014c || machine === 0x01c0 ? 224 : 240;
const bytes = Buffer.alloc(Math.max(512, offset + 24 + optionalHeaderSize));
const sectionTableOffset = offset + 24 + optionalHeaderSize;
const sectionCount = codeView !== undefined ? 1 : 0;
const rawDataOffset = 512;
let codeViewData: Buffer | undefined;
if (codeView !== undefined) {
const type = codeView.type ?? "RSDS";
const pdbPath = codeView.pdbPath ?? "app.pdb";
const age = codeView.age ?? 1;
const pathBuf = Buffer.from(`${pdbPath}\0`, "utf8");
if (type === "RSDS") {
codeViewData = Buffer.alloc(24 + pathBuf.length);
codeViewData.write("RSDS", 0, "ascii");
const guid = codeView.guid ?? "01234567-89ab-cdef-0123-456789abcdef";
const parts = guid.split("-");
if (parts.length === 5) {
codeViewData.writeUInt32LE(parseInt(parts[0]!, 16), 4);
codeViewData.writeUInt16LE(parseInt(parts[1]!, 16), 8);
codeViewData.writeUInt16LE(parseInt(parts[2]!, 16), 10);
Buffer.from(parts[3]!, "hex").copy(codeViewData, 12);
Buffer.from(parts[4]!, "hex").copy(codeViewData, 14);
}
codeViewData.writeUInt32LE(age, 20);
pathBuf.copy(codeViewData, 24);
} else {
codeViewData = Buffer.alloc(16 + pathBuf.length);
codeViewData.write("NB10", 0, "ascii");
codeViewData.writeUInt32LE(0, 4);
codeViewData.writeUInt32LE(0, 8);
codeViewData.writeUInt32LE(age, 12);
pathBuf.copy(codeViewData, 16);
}
}
const minSize =
codeViewData !== undefined
? rawDataOffset + 28 + codeViewData.length + 32
: Math.max(512, offset + 24 + optionalHeaderSize);
const bytes = Buffer.alloc(minSize);
bytes.write("MZ", 0, "ascii");
bytes.writeUInt32LE(offset, 0x3c);
bytes.write("PE\0\0", offset, "binary");
bytes.writeUInt16LE(machine, offset + 4);
bytes.writeUInt16LE(sectionCount, offset + 6);
bytes.writeUInt16LE(optionalHeaderSize, offset + 20);
bytes.writeUInt16LE(characteristics, offset + 22);
const optionalHeader = offset + 24;
@@ -83,6 +130,29 @@ export const pe = (
bytes.writeUInt32LE(0x2000, optionalHeader + directoryOffset + 14 * 8);
bytes.writeUInt32LE(72, optionalHeader + directoryOffset + 14 * 8 + 4);
}
if (codeViewData !== undefined) {
const debugDirRva = 0x1000;
const debugDirSize = 28;
bytes.writeUInt32LE(debugDirRva, optionalHeader + directoryOffset + 6 * 8);
bytes.writeUInt32LE(
debugDirSize,
optionalHeader + directoryOffset + 6 * 8 + 4,
);
bytes.write(".rdata\0\0", sectionTableOffset, "ascii");
bytes.writeUInt32LE(0x1000, sectionTableOffset + 8);
bytes.writeUInt32LE(0x1000, sectionTableOffset + 12);
bytes.writeUInt32LE(512, sectionTableOffset + 16);
bytes.writeUInt32LE(rawDataOffset, sectionTableOffset + 20);
bytes.writeUInt32LE(0x40000040, sectionTableOffset + 36);
bytes.writeUInt32LE(2, rawDataOffset + 12);
bytes.writeUInt32LE(codeViewData.length, rawDataOffset + 16);
bytes.writeUInt32LE(debugDirRva + 28, rawDataOffset + 20);
bytes.writeUInt32LE(rawDataOffset + 28, rawDataOffset + 24);
codeViewData.copy(bytes, rawDataOffset + 28);
}
return bytes;
};
+172 -1
View File
@@ -3,6 +3,7 @@ import type {
ElfFamilyMetadata,
HeaderArchitecture,
MipsElfMetadata,
PeCodeViewRecord,
RiscvElfMetadata,
UniversalMachOSlice,
} from "./binaryTargetTypes.js";
@@ -50,6 +51,7 @@ export type ExecutableMetadata =
| "shared-library"
| "non-executable";
readonly managed: boolean;
readonly codeView?: PeCodeViewRecord;
};
/**
@@ -257,7 +259,8 @@ const parseMz = (
);
if (windowsHeader.signature !== "PE")
return err("invalid or truncated Windows executable header in MZ image");
return parsePeRecord(bytes.subarray(offset));
const codeView = parsePeCodeViewFromBuffer(bytes, offset);
return parsePeRecord(bytes.subarray(offset), codeView);
}
const parsed = parseDosMzHeader(bytes, fileSize, signatureAtOffset);
return parsed.ok
@@ -272,6 +275,7 @@ const parseMz = (
/** Parse a bounded PE signature, COFF record, and optional header without I/O. */
export const parsePeRecord = (
record: Buffer,
codeView: PeCodeViewRecord | undefined,
): Result<ExecutableMetadata, string> => {
if (
record.length < 24 ||
@@ -305,9 +309,176 @@ export const parsePeRecord = (
? "application"
: "shared-library",
managed: managed.value,
...(codeView !== undefined ? { codeView } : {}),
});
};
/** Parse CodeView record data (RSDS or NB10) from raw data bytes. */
export const parseCodeViewData = (
buffer: Buffer,
sizeOfData: number,
): PeCodeViewRecord | undefined => {
if (sizeOfData < 16 || buffer.length < 16) return undefined;
const magic = buffer.subarray(0, 4).toString("binary");
const pathOffset = magic === "RSDS" ? 24 : magic === "NB10" ? 16 : undefined;
if (
pathOffset === undefined ||
sizeOfData <= pathOffset ||
buffer.length <= pathOffset
)
return undefined;
const end = buffer.indexOf(0, pathOffset);
if (end <= pathOffset || end >= sizeOfData) return undefined;
const pdbPath = buffer.toString("utf8", pathOffset, end);
const pdbFileName = pdbPath.split(/[/\\]/u).pop() ?? pdbPath;
if (magic === "RSDS" && sizeOfData >= 24 && buffer.length >= 24) {
const part1 = buffer.readUInt32LE(4).toString(16).padStart(8, "0");
const part2 = buffer.readUInt16LE(8).toString(16).padStart(4, "0");
const part3 = buffer.readUInt16LE(10).toString(16).padStart(4, "0");
const part4 = buffer.subarray(12, 14).toString("hex");
const part5 = buffer.subarray(14, 20).toString("hex");
const guid = `${part1}-${part2}-${part3}-${part4}-${part5}`;
const age = buffer.readUInt32LE(20);
return { pdbPath, pdbFileName, guid, age };
}
if (magic === "NB10") {
const age = buffer.readUInt32LE(12);
return { pdbPath, pdbFileName, guid: null, age };
}
return undefined;
};
/** Locate the debug directory RVA and size from a PE optional header. */
export const peDebugDirectoryEntry = (
optionalHeader: Buffer,
):
| {
readonly debugRva: number;
readonly debugSize: number;
readonly sizeOfHeaders: number;
}
| undefined => {
if (optionalHeader.length < 2) return undefined;
const magic = optionalHeader.readUInt16LE(0);
if (magic !== 0x10b && magic !== 0x20b) return undefined;
const directoryOffset = magic === 0x20b ? 112 : 96;
const directoryCountOffset = directoryOffset - 4;
if (optionalHeader.length < directoryCountOffset + 4) return undefined;
const directoryCount = optionalHeader.readUInt32LE(directoryCountOffset);
if (directoryCount <= 6) return undefined;
const debugEntryOffset = directoryOffset + 6 * 8;
if (optionalHeader.length < debugEntryOffset + 8) return undefined;
const debugRva = optionalHeader.readUInt32LE(debugEntryOffset);
const debugSize = optionalHeader.readUInt32LE(debugEntryOffset + 4);
if (debugRva === 0 || debugSize < 28) return undefined;
return {
debugRva,
debugSize,
sizeOfHeaders: optionalHeader.readUInt32LE(60),
};
};
/** Translate an RVA to an absolute file offset using the PE section table. */
export const peRvaToFileOffset = (
rva: number,
sectionTable: Buffer,
numberOfSections: number,
sizeOfHeaders: number,
size = 1,
): number | undefined => {
if (rva < sizeOfHeaders && size <= sizeOfHeaders - rva) return rva;
for (let i = 0; i < numberOfSections; i++) {
const offset = i * 40;
if (sectionTable.length < offset + 40) break;
const virtualAddress = sectionTable.readUInt32LE(offset + 12);
const sizeOfRawData = sectionTable.readUInt32LE(offset + 16);
const pointerToRawData = sectionTable.readUInt32LE(offset + 20);
if (
pointerToRawData > 0 &&
rva >= virtualAddress &&
rva - virtualAddress < sizeOfRawData &&
size <= sizeOfRawData - (rva - virtualAddress)
) {
return pointerToRawData + (rva - virtualAddress);
}
}
return undefined;
};
/** Parse CodeView metadata from a bounded in-memory PE image buffer. */
export const parsePeCodeViewFromBuffer = (
fileBytes: Buffer,
peOffset: number,
): PeCodeViewRecord | undefined => {
if (fileBytes.length < peOffset + 24) return undefined;
const numberOfSections = fileBytes.readUInt16LE(peOffset + 6);
const optionalHeaderSize = fileBytes.readUInt16LE(peOffset + 20);
if (
optionalHeaderSize > 4096 ||
fileBytes.length < peOffset + 24 + optionalHeaderSize
)
return undefined;
const optionalHeader = fileBytes.subarray(
peOffset + 24,
peOffset + 24 + optionalHeaderSize,
);
const debugDir = peDebugDirectoryEntry(optionalHeader);
if (debugDir === undefined) return undefined;
const sectionTableOffset = peOffset + 24 + optionalHeaderSize;
const sectionTableSize = numberOfSections * 40;
if (fileBytes.length < sectionTableOffset + sectionTableSize)
return undefined;
const sectionTable = fileBytes.subarray(
sectionTableOffset,
sectionTableOffset + sectionTableSize,
);
const debugFileOffset = peRvaToFileOffset(
debugDir.debugRva,
sectionTable,
numberOfSections,
debugDir.sizeOfHeaders,
Math.min(debugDir.debugSize, 4096),
);
if (debugFileOffset === undefined || fileBytes.length < debugFileOffset + 28)
return undefined;
const maxDebugBytes = Math.min(
debugDir.debugSize,
fileBytes.length - debugFileOffset,
);
const numEntries = Math.floor(maxDebugBytes / 28);
for (let j = 0; j < numEntries; j++) {
const entryOffset = debugFileOffset + j * 28;
const type = fileBytes.readUInt32LE(entryOffset + 12);
if (type !== 2) continue; // IMAGE_DEBUG_TYPE_CODEVIEW
const sizeOfData = fileBytes.readUInt32LE(entryOffset + 16);
let pointerToRawData = fileBytes.readUInt32LE(entryOffset + 24);
if (pointerToRawData === 0) {
const addressOfRawData = fileBytes.readUInt32LE(entryOffset + 20);
const mapped = peRvaToFileOffset(
addressOfRawData,
sectionTable,
numberOfSections,
debugDir.sizeOfHeaders,
Math.min(sizeOfData, 4096),
);
if (mapped === undefined) continue;
pointerToRawData = mapped;
}
if (
sizeOfData < 16 ||
fileBytes.length < pointerToRawData + Math.min(sizeOfData, 24)
)
continue;
const codeViewData = fileBytes.subarray(
pointerToRawData,
Math.min(fileBytes.length, pointerToRawData + sizeOfData),
);
const parsed = parseCodeViewData(codeViewData, sizeOfData);
if (parsed !== undefined) return parsed;
}
return undefined;
};
const peManagedStatus = (
optionalHeader: Buffer,
architecture: BinaryArchitecture,
+10
View File
@@ -24,6 +24,14 @@ export interface ElfFamilyMetadata {
readonly flags: number;
}
/** Referenced CodeView debug symbols from a PE debug directory. */
export interface PeCodeViewRecord {
readonly pdbPath: string;
readonly pdbFileName: string;
readonly guid: string | null;
readonly age: number;
}
/** Families whose parsed target variant carries `ElfFamilyMetadata`. */
export const ELF_METADATA_ARCHITECTURES = [
"mips",
@@ -170,6 +178,8 @@ export type BinaryTarget =
| "non-executable";
/** Whether the PE declares a non-empty CLI header data-directory entry. */
readonly managed: boolean;
/** Referenced CodeView debug symbols from the PE debug directory, if present. */
readonly codeView?: PeCodeViewRecord;
})
| (BinaryTargetIdentity &
NonExecutableMetadata & {
+95
View File
@@ -0,0 +1,95 @@
import { describe, expect, it } from "vitest";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { ghidraPeCodeViewLimitations } from "./GhidraPeProfile.js";
describe("ghidraPeCodeViewLimitations", () => {
it("returns empty limitations for non-PE targets", () => {
const elfTarget: BinaryTarget = {
path: "/test/file",
sha256: "abc",
kind: "executable",
format: "elf",
architecture: "x86_64",
availableArchitectures: ["x86_64"],
};
expect(ghidraPeCodeViewLimitations(elfTarget)).toEqual([]);
});
it("does not claim CodeView symbols were unapplied when the session loaded a PDB", () => {
const peTarget: BinaryTarget = {
path: "/test/file.exe",
sha256: "abc",
kind: "executable",
architecture: "x86_64",
availableArchitectures: ["x86_64"],
executableRole: "application",
managed: false,
format: "pe",
codeView: {
pdbPath: "C:\\symbols.pdb",
pdbFileName: "symbols.pdb",
guid: "12345678-1234-1234-1234-123456789abc",
age: 1,
},
};
expect(ghidraPeCodeViewLimitations(peTarget, true)).toEqual([]);
});
it("returns empty limitations for PE target without codeView", () => {
const peTarget: BinaryTarget = {
path: "/test/file.exe",
sha256: "def",
kind: "executable",
format: "pe",
architecture: "x86_64",
availableArchitectures: ["x86_64"],
executableRole: "application",
managed: false,
};
expect(ghidraPeCodeViewLimitations(peTarget)).toEqual([]);
});
it("returns limitation with GUID and age for RSDS CodeView records", () => {
const peTarget: BinaryTarget = {
path: "/test/file.exe",
sha256: "def",
kind: "executable",
format: "pe",
architecture: "x86_64",
availableArchitectures: ["x86_64"],
executableRole: "application",
managed: false,
codeView: {
pdbPath: "C:\\build\\app.pdb",
pdbFileName: "app.pdb",
guid: "01234567-89ab-cdef-0123-456789abcdef",
age: 1,
},
};
expect(ghidraPeCodeViewLimitations(peTarget)).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.',
]);
});
it("returns limitation without GUID for NB10 CodeView records", () => {
const peTarget: BinaryTarget = {
path: "/test/file.exe",
sha256: "def",
kind: "executable",
format: "pe",
architecture: "x86",
availableArchitectures: ["x86"],
executableRole: "application",
managed: false,
codeView: {
pdbPath: "D:\\test\\legacy.pdb",
pdbFileName: "legacy.pdb",
guid: null,
age: 2,
},
};
expect(ghidraPeCodeViewLimitations(peTarget)).toEqual([
'The target references debug symbols "legacy.pdb" (age 2) that were not applied; procedure names are analysis-generated.',
]);
});
});
+24
View File
@@ -0,0 +1,24 @@
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
/**
* Report unapplied PE companion PDB symbols when CodeView debug information is present.
*
* Ghidra does not automatically locate or apply sidecar PDBs in REA's isolated
* runtime directories without companion admission. When the PE target header
* references debug symbols, surface this limitation so callers understand that
* procedure names remain analysis-generated.
*/
export const ghidraPeCodeViewLimitations = (
target: BinaryTarget,
pdbLoaded = false,
): readonly string[] => {
if (target.format !== "pe" || target.codeView === undefined || pdbLoaded) {
return [];
}
const { pdbFileName, guid, age } = target.codeView;
return [
guid !== null
? `The target references debug symbols "${pdbFileName}" (GUID ${guid}, age ${age}) that were not applied; procedure names are analysis-generated.`
: `The target references debug symbols "${pdbFileName}" (age ${age}) that were not applied; procedure names are analysis-generated.`,
];
};
@@ -822,6 +822,39 @@ 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",
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();
});
});
describe("Ghidra relationship coverage", () => {
it("marks the full direct call-reference response complete while keeping static caveats", async () => {
const ghidra = provider(installationHost(), () => ({
+6 -1
View File
@@ -68,6 +68,7 @@ import {
isGhidraMipsSoftFloatTarget,
} from "./GhidraMipsProfile.js";
import { ghidraRiscvLimitations } from "./GhidraRiscvProfile.js";
import { ghidraPeCodeViewLimitations } from "./GhidraPeProfile.js";
import { unverifiedGhidraBuildLimitation } from "./GhidraInstallationPolicy.js";
import { GhidraHeadlessLauncher } from "./GhidraLauncher.js";
import { attestGhidraNativeLoadImage } from "./GhidraLoadImageAttest.js";
@@ -530,6 +531,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;
// Replaced by the reported storage outcome once the handshake is checked.
const softFloatLimitations: string[] = [
...ghidraMipsSoftFloatLimitations(softFloat),
@@ -654,12 +656,14 @@ export const createGhidraProviderClient = (input: {
operation: AnalysisOperation,
info: GhidraSessionInfo,
): Promise<AnalysisError | undefined> => {
if (target.format === "pe")
if (target.format === "pe") {
pdbLoaded = info.pdb?.loaded === true;
pdbLimitations.splice(
0,
pdbLimitations.length,
...ghidraPdbLimitations(info.pdb),
);
}
const softFloatReason = ghidraMipsSoftFloatFailure(
softFloat,
info.mips_soft_float_storage,
@@ -712,6 +716,7 @@ export const createGhidraProviderClient = (input: {
...ghidraExtensionLimitations(extensions),
...seedLimitations,
...pdbLimitations,
...ghidraPeCodeViewLimitations(target, pdbLoaded),
...(releaseLimitation === undefined ? [] : [releaseLimitation]),
];
const nativeAotOwner =
@@ -26,6 +26,7 @@ import {
thinMach,
} from "../../../src/domain/binaryTarget.fixture.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import { parseExecutableHeader } from "../../../src/domain/binaryTarget.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
@@ -424,6 +425,65 @@ describe("binary target I/O: explicit kinds and executable headers", () => {
});
});
it("extracts CodeView RSDS debug metadata from a PE target", async () => {
const directory = await createTestTempDirectory("rea-target-");
const path = join(directory, "symbols.exe");
await writeFile(
path,
pe(0x8664, 64, 0x0002, false, {
type: "RSDS",
pdbPath: "C:\\build\\test.pdb",
guid: "12345678-abcd-ef01-2345-6789abcdef01",
age: 3,
}),
);
const result = await parseBinaryTarget(path, {
cwd: directory,
hostArchitecture: "x64",
});
expect(result.ok).toBe(true);
if (!result.ok) throw result.error;
expect(result.value).toMatchObject({
format: "pe",
architecture: "x86_64",
codeView: {
pdbPath: "C:\\build\\test.pdb",
pdbFileName: "test.pdb",
guid: "12345678-abcd-ef01-2345-6789abcdef01",
age: 3,
},
});
});
it("extracts CodeView NB10 debug metadata from a PE target", async () => {
const directory = await createTestTempDirectory("rea-target-");
const path = join(directory, "legacy.exe");
await writeFile(
path,
pe(0x014c, 64, 0x0002, false, {
type: "NB10",
pdbPath: "D:\\old\\legacy.pdb",
age: 1,
}),
);
const result = await parseBinaryTarget(path, {
cwd: directory,
hostArchitecture: "ia32",
});
expect(result.ok).toBe(true);
if (!result.ok) throw result.error;
expect(result.value).toMatchObject({
format: "pe",
architecture: "x86",
codeView: {
pdbPath: "D:\\old\\legacy.pdb",
pdbFileName: "legacy.pdb",
guid: null,
age: 1,
},
});
});
it("refuses a truncated Mach-O header before any provider sees it", async () => {
const directory = await createTestTempDirectory("rea-target-");
const path = join(directory, "truncated");
@@ -884,3 +944,43 @@ it.each([
});
},
);
describe("binary target I/O: PE CodeView header ranges", () => {
it.each(["RSDS", "NB10"] as const)(
"reads bounded %s header debug records and rejects unfinished names",
async (type) => {
const directory = await createTestTempDirectory("rea-target-");
const path = join(directory, "header-symbols.exe");
const bytes = pe(0x8664, 64, 0x0002, false, {
type,
pdbPath: "header.pdb",
});
const optionalHeader = 64 + 24;
bytes.writeUInt32LE(512, optionalHeader + 60);
bytes.copy(bytes, 384, 512);
bytes.writeUInt32LE(384, optionalHeader + 112 + 6 * 8);
bytes.writeUInt32LE(412, 384 + 20);
bytes.writeUInt32LE(0, 384 + 24);
for (const complete of [true, false]) {
if (!complete) {
const dataSize = bytes.readUInt32LE(384 + 16);
bytes[412 + dataSize - 1] = 0x61;
}
await writeFile(path, bytes);
const resolved = await parseBinaryTarget(path, {
hostArchitecture: "x64",
});
const buffered = parseExecutableHeader(bytes, "x64");
if (!resolved.ok) throw resolved.error;
if (!buffered.ok) throw new Error(buffered.error);
for (const value of [resolved.value, buffered.value]) {
if (complete)
expect(value).toMatchObject({
codeView: { pdbPath: "header.pdb" },
});
else expect(value).not.toHaveProperty("codeView");
}
}
},
);
});