diff --git a/src/application/BinaryTargetResolver.ts b/src/application/BinaryTargetResolver.ts index 921546eb95..48399a5b75 100644 --- a/src/application/BinaryTargetResolver.ts +++ b/src/application/BinaryTargetResolver.ts @@ -82,6 +82,7 @@ import { parseExecutableHeader, parsePeRecordFacts, peDebugDirectoryEntry, + peCodeViewDataRanges, peRvaToFileOffset, type ExecutableMetadata, } from "../domain/binaryTarget.js"; @@ -1347,28 +1348,15 @@ const readPeCodeView = async ( 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; + for (const { + fileOffset: pointerToRawData, + declaredSize: sizeOfData, + } of peCodeViewDataRanges( + debugDirectoryBuffer.subarray(0, debugDirRead.bytesRead), + sectionTable, + numberOfSections, + debugDir.sizeOfHeaders, + )) { const maxCodeViewRead = Math.min(sizeOfData, 4096); const codeViewBuffer = Buffer.alloc(maxCodeViewRead); const codeViewRead = await handle.read( diff --git a/src/domain/binaryTarget.ts b/src/domain/binaryTarget.ts index d06c47aa69..b38e777dfe 100644 --- a/src/domain/binaryTarget.ts +++ b/src/domain/binaryTarget.ts @@ -630,6 +630,35 @@ export const peRvaToFileOffset = ( return undefined; }; +/** CodeView RVA admission uses the same bounded prefix as target file reads. */ +const PE_CODEVIEW_MAPPING_BYTES = 4096; + +/** Interpret complete debug entries once; callers own directory and payload read bounds. */ +export function* peCodeViewDataRanges( + directory: Buffer, + sectionTable: Buffer, + numberOfSections: number, + sizeOfHeaders: number, +): Generator<{ readonly fileOffset: number; readonly declaredSize: number }> { + for (let offset = 0; offset + 28 <= directory.length; offset += 28) { + if (directory.readUInt32LE(offset + 12) !== 2) continue; + const declaredSize = directory.readUInt32LE(offset + 16); + if (declaredSize < 16) continue; + const rawPointer = directory.readUInt32LE(offset + 24); + const fileOffset = + rawPointer !== 0 + ? rawPointer + : peRvaToFileOffset( + directory.readUInt32LE(offset + 20), + sectionTable, + numberOfSections, + sizeOfHeaders, + Math.min(declaredSize, PE_CODEVIEW_MAPPING_BYTES), + ); + if (fileOffset !== undefined) yield { fileOffset, declaredSize }; + } +} + /** Parse CodeView metadata from a bounded in-memory PE image buffer. */ export const parsePeCodeViewFromBuffer = ( fileBytes: Buffer, @@ -673,29 +702,16 @@ export const parsePeCodeViewFromBuffer = ( 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) - ) + for (const { + fileOffset: pointerToRawData, + declaredSize: sizeOfData, + } of peCodeViewDataRanges( + fileBytes.subarray(debugFileOffset, debugFileOffset + maxDebugBytes), + sectionTable, + numberOfSections, + debugDir.sizeOfHeaders, + )) { + if (fileBytes.length < pointerToRawData + Math.min(sizeOfData, 24)) continue; const codeViewData = fileBytes.subarray( pointerToRawData, diff --git a/src/domain/peCodeViewDataRanges.test.ts b/src/domain/peCodeViewDataRanges.test.ts new file mode 100644 index 0000000000..2acb43a999 --- /dev/null +++ b/src/domain/peCodeViewDataRanges.test.ts @@ -0,0 +1,55 @@ +import { expect, it } from "vitest"; +import { peCodeViewDataRanges } from "./binaryTarget.js"; + +const entry = (type: number, size: number, rva: number, raw: number) => { + const bytes = Buffer.alloc(28); + bytes.writeUInt32LE(type, 12); + bytes.writeUInt32LE(size, 16); + bytes.writeUInt32LE(rva, 20); + bytes.writeUInt32LE(raw, 24); + return bytes; +}; + +const section = () => { + const bytes = Buffer.alloc(40); + bytes.writeUInt32LE(0x2000, 12); + bytes.writeUInt32LE(4096, 16); + bytes.writeUInt32LE(0x400, 20); + return bytes; +}; + +it("prefers physical pointers and maps only missing pointers through section or header RVAs", () => { + const directory = Buffer.concat([ + entry(2, 32, 0x2000, 0x900), + entry(2, 32, 0x2020, 0), + entry(2, 32, 0x100, 0), + entry(2, 32, 0x8000, 0), + ]); + expect([...peCodeViewDataRanges(directory, section(), 1, 0x400)]).toEqual([ + { fileOffset: 0x900, declaredSize: 32 }, + { fileOffset: 0x420, declaredSize: 32 }, + { fileOffset: 0x100, declaredSize: 32 }, + ]); +}); + +it("ignores other debug types, undersized records and incomplete trailing entries", () => { + const directory = Buffer.concat([ + entry(1, 32, 0, 0x500), + entry(2, 15, 0, 0x600), + entry(2, 16, 0, 0x700), + entry(2, 32, 0, 0x800).subarray(0, 27), + ]); + expect([...peCodeViewDataRanges(directory, Buffer.alloc(0), 0, 0)]).toEqual([ + { fileOffset: 0x700, declaredSize: 16 }, + ]); +}); + +it("maps the bounded payload prefix while retaining its full declared size", () => { + const directory = Buffer.concat([ + entry(2, 8192, 0x2000, 0), + entry(2, 8192, 0x2001, 0), + ]); + expect([...peCodeViewDataRanges(directory, section(), 1, 0x400)]).toEqual([ + { fileOffset: 0x400, declaredSize: 8192 }, + ]); +});