mirror of
https://github.com/morluto/rea.git
synced 2026-11-13 10:12:50 +00:00
refactor(pe): share optional-header structural facts
Decode optional-header declarations once and pass detached directory words, locations and SizeOfHeaders to native consumers. Preserve executable machine admission, raw GPU/resource machine reporting, RVA mapping and certificate file-offset semantics. Validation: fresh build/catalog, full static checks, 466 core/provider fixture tests, 122 workflow tests and 14 selected file-backed PE admission tests passed. Public CLI and actual stdio MCP used authored artifacts; installed external providers were not qualified. Added 17 structural cases and two GPU machine-policy controls. Eager directory decoding costs remain unmeasured.
This commit is contained in:
1 parent
9548953b47
commit
599099f656
12 files changed
+394
-119
No files matched your search
@@ -98,7 +98,7 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
||||
architecture: image.architecture,
|
||||
bits: image.bits,
|
||||
};
|
||||
if (directory === undefined || directory.virtual_address === 0)
|
||||
if (directory === undefined || directory.address === 0)
|
||||
return {
|
||||
...result,
|
||||
export_directory: null,
|
||||
@@ -129,7 +129,7 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
||||
return value.toString("latin1");
|
||||
};
|
||||
|
||||
const header = map(directory.virtual_address, PE_EXPORT_DIRECTORY_BYTES);
|
||||
const header = map(directory.address, PE_EXPORT_DIRECTORY_BYTES);
|
||||
const ordinalBase = reader.u32(header + 16);
|
||||
const functionCount = reader.u32(header + 20);
|
||||
const nameCount = reader.u32(header + 24);
|
||||
@@ -180,7 +180,7 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
||||
}
|
||||
}
|
||||
|
||||
const forwarderEnd = directory.virtual_address + directory.size;
|
||||
const forwarderEnd = directory.address + directory.size;
|
||||
const retain = createRetention();
|
||||
const exports: PeExport[] = [];
|
||||
for (let index = 0; index < functionCount; index++) {
|
||||
@@ -189,7 +189,7 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
||||
if (rva === 0) continue;
|
||||
const ordinal = ordinalBase + index;
|
||||
let target: Pick<PeExport, "address" | "forwarder">;
|
||||
if (rva >= directory.virtual_address && rva < forwarderEnd) {
|
||||
if (rva >= directory.address && rva < forwarderEnd) {
|
||||
try {
|
||||
target = { address: null, forwarder: cString(rva) };
|
||||
} catch (cause) {
|
||||
@@ -216,7 +216,7 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
||||
return {
|
||||
...result,
|
||||
export_directory: {
|
||||
rva: directory.virtual_address,
|
||||
rva: directory.address,
|
||||
size: directory.size,
|
||||
dll_name: dllName,
|
||||
ordinal_base: ordinalBase,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { PeDataDirectory } from "../../domain/peOptionalHeader.js";
|
||||
|
||||
/** Distinguish corrupt recognized binaries from containers outside parser coverage. */
|
||||
export class BinaryFormatFailure extends Error {
|
||||
constructor(
|
||||
@@ -120,10 +122,7 @@ interface BinaryContainerFacts {
|
||||
*/
|
||||
readonly pe_header_mapping?: FileMapping;
|
||||
/** PE optional-header data directories, indexed as declared. */
|
||||
readonly pe_data_directories?: readonly {
|
||||
readonly virtual_address: number;
|
||||
readonly size: number;
|
||||
}[];
|
||||
readonly pe_data_directories?: readonly PeDataDirectory[];
|
||||
/** Mach-O load-command observation, when available. */
|
||||
readonly macho_chained_fixups?: boolean;
|
||||
/** Section-name lookup was incomplete; absence by name is not established. */
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { readElfTableDeclarations } from "../../domain/elfDeclaredRanges.js";
|
||||
import { readPeOptionalHeaderStructure } from "../../domain/peOptionalHeader.js";
|
||||
import {
|
||||
BinaryFormatFailure,
|
||||
BinaryReader,
|
||||
@@ -286,23 +287,24 @@ export function* readPeSectionDirectory(
|
||||
const coff = new BinaryReader(yield request(fileSize, BigInt(pe), 24n), true);
|
||||
if (coff.u32(0) !== 0x4550) reject("PE signature is invalid");
|
||||
const optionalSize = coff.u16(20);
|
||||
const optional = new BinaryReader(
|
||||
const optional = readPeOptionalHeaderStructure(
|
||||
yield request(fileSize, BigInt(pe + 24), BigInt(optionalSize)),
|
||||
true,
|
||||
);
|
||||
const magic = optional.u16(0);
|
||||
const bits = magic === 0x10b ? 32 : magic === 0x20b ? 64 : null;
|
||||
if (bits === null)
|
||||
throw new BinaryFormatFailure(
|
||||
"unsupported",
|
||||
"Only PE32/PE32+ images are supported",
|
||||
);
|
||||
const minimum = bits === 64 ? 112 : 96;
|
||||
if (
|
||||
optionalSize < minimum ||
|
||||
optional.u32(minimum - 4) * 8 > optionalSize - minimum
|
||||
)
|
||||
reject("PE optional header or data directories are truncated");
|
||||
if (!optional.ok) {
|
||||
switch (optional.error.reason) {
|
||||
case "truncated-magic":
|
||||
return reject("16-bit field range is truncated or outside the file");
|
||||
case "unsupported-magic":
|
||||
throw new BinaryFormatFailure(
|
||||
"unsupported",
|
||||
"Only PE32/PE32+ images are supported",
|
||||
);
|
||||
case "truncated-fixed-fields":
|
||||
case "truncated-directory-table":
|
||||
return reject("PE optional header or data directories are truncated");
|
||||
}
|
||||
}
|
||||
const { bits } = optional.value;
|
||||
const count = coff.u16(6);
|
||||
const start = pe + 24 + optionalSize;
|
||||
const table = yield request(fileSize, BigInt(start), BigInt(count) * 40n);
|
||||
|
||||
@@ -82,13 +82,13 @@ export const readPeImage = (bytes: Buffer): PeImage => {
|
||||
);
|
||||
if (!facts.ok)
|
||||
throw new BinaryFormatFailure(facts.error.kind, facts.error.message);
|
||||
const { bits, dataDirectories } = facts.value;
|
||||
const { bits, dataDirectories, sizeOfHeaders } = facts.value;
|
||||
const imageBase = reader.word(optional + (bits === 64 ? 24 : 28), bits);
|
||||
const fileAlignment = reader.u32(optional + 36);
|
||||
const headerMapping: FileMapping = {
|
||||
address: imageBase,
|
||||
offset: 0,
|
||||
size: Math.min(reader.u32(optional + 60), bytes.length),
|
||||
size: Math.min(sizeOfHeaders, bytes.length),
|
||||
};
|
||||
const count = reader.u16(pe + 6);
|
||||
const start = reader.table(
|
||||
|
||||
@@ -54,27 +54,13 @@ export const parsePeCertificates = (
|
||||
artifact: PeCertificates["artifact"],
|
||||
input: InspectPeCertificatesInput,
|
||||
): PeCertificates => {
|
||||
const { pe, optional, magic, directoriesAt, directoryCount, headerSize } =
|
||||
const { machine, format, dataDirectories, headerSize, optionalHeaderOffset } =
|
||||
readPeHeaders(bytes);
|
||||
const machine = bytes.readUInt16LE(pe + 4);
|
||||
const format = magic === 0x20b ? "pe32-plus" : "pe32";
|
||||
const securityIndex = 4;
|
||||
if (directoryCount <= securityIndex) {
|
||||
return {
|
||||
artifact,
|
||||
format,
|
||||
machine,
|
||||
table: null,
|
||||
entries: [],
|
||||
coverage: { status: "complete", examined_entries: 0, certificates: 0 },
|
||||
limitations: [...ABSENT_LIMITATIONS],
|
||||
};
|
||||
}
|
||||
const directoryAt = optional + directoriesAt + securityIndex * 8;
|
||||
requireRange(bytes, directoryAt, 8);
|
||||
const tableOffset = bytes.readUInt32LE(directoryAt);
|
||||
const tableSize = bytes.readUInt32LE(directoryAt + 4);
|
||||
if (tableOffset === 0 && tableSize === 0) {
|
||||
const security = dataDirectories[4];
|
||||
if (
|
||||
security === undefined ||
|
||||
(security.address === 0 && security.size === 0)
|
||||
) {
|
||||
return {
|
||||
artifact,
|
||||
format,
|
||||
@@ -85,6 +71,9 @@ export const parsePeCertificates = (
|
||||
limitations: [...ABSENT_LIMITATIONS],
|
||||
};
|
||||
}
|
||||
const tableOffset = security.address;
|
||||
const tableSize = security.size;
|
||||
const directoryAt = optionalHeaderOffset + security.offset;
|
||||
if (tableSize === 0 || tableOffset < headerSize)
|
||||
fail("PE certificate table offset overlaps headers or has zero size.");
|
||||
requireRange(bytes, tableOffset, tableSize);
|
||||
|
||||
@@ -389,12 +389,12 @@ export const parsePeImports = async (
|
||||
const delayDirectory = image.pe_data_directories?.[PE_DELAY_IMPORT_DIRECTORY];
|
||||
const imports: PeImportDll[] = [];
|
||||
let importDirectoryResult: PeImports["import_directory"] = null;
|
||||
if (importDirectory !== undefined && importDirectory.virtual_address !== 0) {
|
||||
if (importDirectory !== undefined && importDirectory.address !== 0) {
|
||||
const spans: { offset: number; bytes: number }[] = [];
|
||||
for (let index = 0; ; index++) {
|
||||
await checkpoint();
|
||||
const descriptorRva =
|
||||
importDirectory.virtual_address + index * IMPORT_DESCRIPTOR_BYTES;
|
||||
importDirectory.address + index * IMPORT_DESCRIPTOR_BYTES;
|
||||
const descriptor = mapDescriptor(
|
||||
descriptorRva,
|
||||
IMPORT_DESCRIPTOR_BYTES,
|
||||
@@ -453,7 +453,7 @@ export const parsePeImports = async (
|
||||
});
|
||||
}
|
||||
importDirectoryResult = {
|
||||
rva: importDirectory.virtual_address,
|
||||
rva: importDirectory.address,
|
||||
size: importDirectory.size,
|
||||
locations: spans,
|
||||
};
|
||||
@@ -463,12 +463,12 @@ export const parsePeImports = async (
|
||||
|
||||
const delayImports: PeDelayImportDll[] = [];
|
||||
let delayDirectoryResult: PeImports["delay_import_directory"] = null;
|
||||
if (delayDirectory !== undefined && delayDirectory.virtual_address !== 0) {
|
||||
if (delayDirectory !== undefined && delayDirectory.address !== 0) {
|
||||
const spans: { offset: number; bytes: number }[] = [];
|
||||
for (let index = 0; ; index++) {
|
||||
await checkpoint();
|
||||
const descriptorRva =
|
||||
delayDirectory.virtual_address + index * DELAY_DESCRIPTOR_BYTES;
|
||||
delayDirectory.address + index * DELAY_DESCRIPTOR_BYTES;
|
||||
const descriptor = mapDescriptor(
|
||||
descriptorRva,
|
||||
DELAY_DESCRIPTOR_BYTES,
|
||||
@@ -534,7 +534,7 @@ export const parsePeImports = async (
|
||||
});
|
||||
}
|
||||
delayDirectoryResult = {
|
||||
rva: delayDirectory.virtual_address,
|
||||
rva: delayDirectory.address,
|
||||
size: delayDirectory.size,
|
||||
locations: spans,
|
||||
};
|
||||
@@ -559,7 +559,7 @@ export const parsePeImports = async (
|
||||
mappingCompatibility: standardMappingCompatibility,
|
||||
truncatedDescriptor: pefileTruncatedDescriptor,
|
||||
bits,
|
||||
directoryRva: importDirectory?.virtual_address ?? 0,
|
||||
directoryRva: importDirectory?.address ?? 0,
|
||||
fileBytes: bytes.length,
|
||||
}),
|
||||
coverage: coverage(
|
||||
|
||||
@@ -164,7 +164,7 @@ export const parsePeLoadConfig = async (
|
||||
const dataDirectory = (index: number): DirectorySpan | null => {
|
||||
const entry = image.pe_data_directories[index];
|
||||
if (entry === undefined) return null;
|
||||
const { virtual_address: rva, size } = entry;
|
||||
const { address: rva, size } = entry;
|
||||
if (rva === 0 && size === 0) return null;
|
||||
if (rva === 0 || size === 0)
|
||||
throw new BinaryFormatFailure(
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import {
|
||||
readPeOptionalHeaderStructure,
|
||||
type PeOptionalHeaderFailure,
|
||||
} from "../../domain/peOptionalHeader.js";
|
||||
|
||||
const OPTIONAL_HEADER_FAILURES: Readonly<
|
||||
Record<PeOptionalHeaderFailure["reason"], string>
|
||||
> = {
|
||||
"truncated-magic": "Truncated PE optional header.",
|
||||
"unsupported-magic": "Unsupported PE optional header magic.",
|
||||
"truncated-fixed-fields": "Truncated PE data directory count.",
|
||||
"truncated-directory-table":
|
||||
"PE directory count exceeds its optional header.",
|
||||
};
|
||||
|
||||
export const peFailure = (message: string): never => {
|
||||
throw new ArtifactReaderFailure("format", message);
|
||||
@@ -71,31 +85,31 @@ export const readPeHeaders = (bytes: Buffer, fileSize = bytes.length) => {
|
||||
if (bytes.readUInt32LE(pe) !== 0x4550) peFailure("Expected a PE signature.");
|
||||
const sectionCount = bytes.readUInt16LE(pe + 6);
|
||||
const optionalSize = bytes.readUInt16LE(pe + 20);
|
||||
const optional = pe + 24;
|
||||
requirePeRange(bytes, optional, optionalSize);
|
||||
const optionalHeaderOffset = pe + 24;
|
||||
requirePeRange(bytes, optionalHeaderOffset, optionalSize);
|
||||
if (optionalSize < 64) peFailure("Truncated PE optional header.");
|
||||
const magic = bytes.readUInt16LE(optional);
|
||||
if (magic !== 0x10b && magic !== 0x20b)
|
||||
peFailure("Unsupported PE optional header magic.");
|
||||
const countAt = magic === 0x10b ? 92 : 108;
|
||||
const directoriesAt = countAt + 4;
|
||||
if (optionalSize < directoriesAt)
|
||||
peFailure("Truncated PE data directory count.");
|
||||
const directoryCount = bytes.readUInt32LE(optional + countAt);
|
||||
if (directoryCount > Math.floor((optionalSize - directoriesAt) / 8))
|
||||
peFailure("PE directory count exceeds its optional header.");
|
||||
const headerSize = bytes.readUInt32LE(optional + 60);
|
||||
const table = optional + optionalSize;
|
||||
const parsed = readPeOptionalHeaderStructure(
|
||||
bytes.subarray(optionalHeaderOffset, optionalHeaderOffset + optionalSize),
|
||||
);
|
||||
if (!parsed.ok)
|
||||
return peFailure(OPTIONAL_HEADER_FAILURES[parsed.error.reason]);
|
||||
const {
|
||||
sizeOfHeaders: headerSize,
|
||||
format,
|
||||
directoryOffset,
|
||||
dataDirectories,
|
||||
} = parsed.value;
|
||||
const table = optionalHeaderOffset + optionalSize;
|
||||
requirePeRange(bytes, table, sectionCount * 40);
|
||||
if (headerSize < table + sectionCount * 40 || headerSize > fileSize)
|
||||
peFailure("Invalid PE SizeOfHeaders.");
|
||||
return {
|
||||
pe,
|
||||
machine: bytes.readUInt16LE(pe + 4),
|
||||
format,
|
||||
sectionCount,
|
||||
optional,
|
||||
magic,
|
||||
directoriesAt,
|
||||
directoryCount,
|
||||
optionalHeaderOffset,
|
||||
directoryOffset,
|
||||
dataDirectories,
|
||||
headerSize,
|
||||
table,
|
||||
};
|
||||
@@ -107,12 +121,12 @@ export const readPeResourceLayout = (
|
||||
fileSize = bytes.length,
|
||||
) => {
|
||||
const {
|
||||
pe,
|
||||
machine,
|
||||
format,
|
||||
sectionCount,
|
||||
optional,
|
||||
magic,
|
||||
directoriesAt,
|
||||
directoryCount,
|
||||
optionalHeaderOffset,
|
||||
directoryOffset,
|
||||
dataDirectories,
|
||||
headerSize,
|
||||
table,
|
||||
} = readPeHeaders(bytes, fileSize);
|
||||
@@ -194,14 +208,16 @@ export const readPeResourceLayout = (
|
||||
requirePeExtent(fileSize, offset, size);
|
||||
return offset;
|
||||
};
|
||||
const directoryAt = optional + directoriesAt + 16;
|
||||
const rva = directoryCount > 2 ? bytes.readUInt32LE(directoryAt) : 0;
|
||||
const size = directoryCount > 2 ? bytes.readUInt32LE(directoryAt + 4) : 0;
|
||||
const resource = dataDirectories[2];
|
||||
const directoryAt =
|
||||
optionalHeaderOffset + (resource?.offset ?? directoryOffset + 2 * 8);
|
||||
const rva = resource?.address ?? 0;
|
||||
const size = resource?.size ?? 0;
|
||||
if ((rva === 0) !== (size === 0))
|
||||
peFailure("Inconsistent PE resource directory RVA/size.");
|
||||
return {
|
||||
format: magic === 0x10b ? ("pe32" as const) : ("pe32-plus" as const),
|
||||
machine: bytes.readUInt16LE(pe + 4),
|
||||
format,
|
||||
machine,
|
||||
directoryAt,
|
||||
rva,
|
||||
size,
|
||||
|
||||
Reference in new issue
Block a user