mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
refactor(pe): centralize fixed COFF header decoding
This commit is contained in:
1 parent
99d4ea9c05
commit
b9ee8fe057
12 files changed
+135
-29
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { readPeCoffHeader } from "../domain/peCoffHeader.js";
|
||||
import {
|
||||
regularFileCancellation,
|
||||
RegularFileCleanupFailure,
|
||||
@@ -1285,9 +1286,11 @@ const readPeMetadata = async (
|
||||
return err("invalid Windows new-header signature in MZ image");
|
||||
if (["NE", "LE", "LX"].includes(windowsHeader.signature))
|
||||
return err(`unsupported ${windowsHeader.signature} executable in MZ image`);
|
||||
const optionalHeaderSize = fileHeader.readUInt16LE(20);
|
||||
const coff = readPeCoffHeader(fileHeader.subarray(4));
|
||||
if (!coff.ok) return err("invalid or truncated PE header");
|
||||
const optionalHeaderSize = coff.value.optionalHeaderSize;
|
||||
if (optionalHeaderSize > 4096) return err("invalid PE optional header size");
|
||||
const numberOfSections = fileHeader.readUInt16LE(6);
|
||||
const numberOfSections = coff.value.sectionCount;
|
||||
const record = Buffer.alloc(24 + optionalHeaderSize);
|
||||
const recordRead = await handle.read(record, 0, record.length, offset);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readPeCoffHeader } from "../domain/peCoffHeader.js";
|
||||
import {
|
||||
hasGodotPckHeader,
|
||||
readGodotPckHeaderFacts,
|
||||
@@ -208,7 +209,12 @@ const inspectPeFile = async (
|
||||
const peOffset = prefix.readUInt32LE(0x3c);
|
||||
const coff = await read(peOffset, 24);
|
||||
if (coff.toString("latin1", 0, 4) !== "PE\0\0") return;
|
||||
const optionalSize = coff.readUInt16LE(20);
|
||||
const header = readPeCoffHeader(coff.subarray(4));
|
||||
if (!header.ok) {
|
||||
row.issues.push(header.error.message);
|
||||
return;
|
||||
}
|
||||
const optionalSize = header.value.optionalHeaderSize;
|
||||
if (optionalSize > 4096)
|
||||
throw new ArtifactReaderFailure(
|
||||
"format",
|
||||
@@ -224,7 +230,7 @@ const inspectPeFile = async (
|
||||
fact({
|
||||
kind: "native-image-header",
|
||||
offset: peOffset,
|
||||
value: { format: "pe", machine: coff.readUInt16LE(4) },
|
||||
value: { format: "pe", machine: header.value.machine },
|
||||
});
|
||||
if (!parsed.value.managed) return;
|
||||
const directories = optional.readUInt16LE(0) === 0x20b ? 112 : 96;
|
||||
@@ -236,7 +242,7 @@ const inspectPeFile = async (
|
||||
value: { rva: cliRva, bytes: cliSize },
|
||||
});
|
||||
if (unityManagedDirectory === null) return;
|
||||
const sectionCount = coff.readUInt16LE(6);
|
||||
const sectionCount = header.value.sectionCount;
|
||||
const sections = await read(peOffset + 24 + optionalSize, sectionCount * 40);
|
||||
const mapped = (rva: number, bytes: number) =>
|
||||
peRvaToFileOffset(
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
UniversalMachOSlice,
|
||||
} from "./binaryTargetTypes.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import { readPeCoffHeader } from "./peCoffHeader.js";
|
||||
import { readPeSectionHeader } from "./peSectionHeader.js";
|
||||
import { readElfFixedHeader } from "./elfFixedHeader.js";
|
||||
import {
|
||||
@@ -489,13 +490,18 @@ export const parsePeRecord = (
|
||||
record.toString("binary", 0, 4) !== "PE\u0000\u0000"
|
||||
)
|
||||
return err("invalid or truncated PE header");
|
||||
const machineType = record.readUInt16LE(4);
|
||||
const coff = readPeCoffHeader(record.subarray(4, 24));
|
||||
if (!coff.ok) return err("invalid or truncated PE header");
|
||||
const {
|
||||
machine: machineType,
|
||||
optionalHeaderSize,
|
||||
characteristics,
|
||||
} = coff.value;
|
||||
const machine = decodePeMachine(machineType);
|
||||
if (machine === undefined)
|
||||
return err(
|
||||
`unsupported PE architecture (COFF machine 0x${machineType.toString(16).padStart(4, "0")})`,
|
||||
);
|
||||
const optionalHeaderSize = record.readUInt16LE(20);
|
||||
if (optionalHeaderSize > 4096 || record.length < 24 + optionalHeaderSize)
|
||||
return err("invalid or truncated PE optional header");
|
||||
const optionalHeader = record.subarray(24, 24 + optionalHeaderSize);
|
||||
@@ -503,7 +509,6 @@ export const parsePeRecord = (
|
||||
if (!facts.ok) return err(facts.error.message);
|
||||
if (facts.value.machine === null) return err("unsupported PE architecture");
|
||||
const { architecture } = facts.value.machine;
|
||||
const characteristics = record.readUInt16LE(22);
|
||||
return ok({
|
||||
format: "pe",
|
||||
peMachine: machineType,
|
||||
@@ -623,8 +628,11 @@ export const parsePeCodeViewFromBuffer = (
|
||||
peOffset: number,
|
||||
): PeCodeViewRecord | undefined => {
|
||||
if (fileBytes.length < peOffset + 24) return undefined;
|
||||
const numberOfSections = fileBytes.readUInt16LE(peOffset + 6);
|
||||
const optionalHeaderSize = fileBytes.readUInt16LE(peOffset + 20);
|
||||
const coff = readPeCoffHeader(
|
||||
fileBytes.subarray(peOffset + 4, peOffset + 24),
|
||||
);
|
||||
if (!coff.ok) return undefined;
|
||||
const { sectionCount: numberOfSections, optionalHeaderSize } = coff.value;
|
||||
if (
|
||||
optionalHeaderSize > 4096 ||
|
||||
fileBytes.length < peOffset + 24 + optionalHeaderSize
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readPeCoffHeader } from "../peCoffHeader.js";
|
||||
import { readPeSectionHeader } from "../peSectionHeader.js";
|
||||
import type { NativeMetadataRecovery } from "./nativeMetadataRecovery.js";
|
||||
|
||||
@@ -493,13 +494,15 @@ class PeImage {
|
||||
this.#range(peOffset, 24);
|
||||
if (bytes.toString("latin1", peOffset, peOffset + 4) !== "PE\0\0")
|
||||
throw new Error("Invalid PE signature");
|
||||
this.#machine = this.#u16(peOffset + 4);
|
||||
const count = this.#u16(peOffset + 6);
|
||||
const coff = readPeCoffHeader(bytes.subarray(peOffset + 4, peOffset + 24));
|
||||
if (!coff.ok) throw new Error(coff.error.message);
|
||||
this.#machine = coff.value.machine;
|
||||
const { sectionCount: count, optionalHeaderSize: optionalSize } =
|
||||
coff.value;
|
||||
// Microsoft documents a 96-section Windows image-loader limit. This
|
||||
// parser accepts executable PE images, not arbitrary COFF object files.
|
||||
if (count > 96)
|
||||
throw new Error("PE image exceeds the Windows 96-section limit");
|
||||
const optionalSize = this.#u16(peOffset + 20);
|
||||
const optional = peOffset + 24;
|
||||
this.#range(optional, optionalSize);
|
||||
// Mapping fields through SizeOfHeaders must belong to this header,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { readPeCoffHeader } from "./peCoffHeader.js";
|
||||
|
||||
it("retains raw COFF declarations from a borrowed record without admission policy", () => {
|
||||
const source = Buffer.alloc(39, 0xa5);
|
||||
const record = source.subarray(7, 27);
|
||||
record.writeUInt16LE(0xffff, 0);
|
||||
record.writeUInt16LE(0x1234, 2);
|
||||
record.writeUInt16LE(0xfedc, 16);
|
||||
record.writeUInt16LE(0x89ab, 18);
|
||||
const before = Buffer.from(source);
|
||||
expect(readPeCoffHeader(source.subarray(7))).toEqual({
|
||||
ok: true,
|
||||
value: {
|
||||
machine: 0xffff,
|
||||
sectionCount: 0x1234,
|
||||
optionalHeaderSize: 0xfedc,
|
||||
characteristics: 0x89ab,
|
||||
},
|
||||
});
|
||||
expect(source.equals(before)).toBe(true);
|
||||
});
|
||||
|
||||
it.each(Array.from({ length: 20 }, (_, length) => length))(
|
||||
"reports a truncated %i-byte COFF record without reading outside it",
|
||||
(length) => {
|
||||
const source = Buffer.alloc(40, 0xff);
|
||||
expect(readPeCoffHeader(source.subarray(7, 7 + length))).toEqual({
|
||||
ok: false,
|
||||
error: { kind: "malformed", message: "PE COFF header is truncated" },
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { err, ok } from "./result.js";
|
||||
|
||||
/** Fixed 20-byte COFF record, excluding the PE signature; callers own admission. */
|
||||
export const readPeCoffHeader = (bytes: Buffer) => {
|
||||
if (bytes.length < 20)
|
||||
return err({
|
||||
kind: "malformed" as const,
|
||||
message: "PE COFF header is truncated",
|
||||
});
|
||||
return ok({
|
||||
machine: bytes.readUInt16LE(0),
|
||||
sectionCount: bytes.readUInt16LE(2),
|
||||
optionalHeaderSize: bytes.readUInt16LE(16),
|
||||
characteristics: bytes.readUInt16LE(18),
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
|
||||
import { readPeCoffHeader } from "../domain/peCoffHeader.js";
|
||||
import { decodePeMachine } from "../domain/peMachine.js";
|
||||
import {
|
||||
managedFailure,
|
||||
@@ -299,8 +300,20 @@ const readCoffHeader = (bytes: Buffer): CoffHeader => {
|
||||
peOffset,
|
||||
);
|
||||
const coff = peOffset + 4;
|
||||
const machine = bytes.readUInt16LE(coff);
|
||||
const sectionCount = bytes.readUInt16LE(coff + 2);
|
||||
const parsed = readPeCoffHeader(bytes.subarray(coff, coff + 20));
|
||||
if (!parsed.ok)
|
||||
throw managedFailure(
|
||||
"invalid-directory",
|
||||
"pe.coff-header",
|
||||
parsed.error.message,
|
||||
peOffset,
|
||||
);
|
||||
const {
|
||||
machine,
|
||||
sectionCount,
|
||||
optionalHeaderSize: optionalSize,
|
||||
characteristics,
|
||||
} = parsed.value;
|
||||
if (sectionCount === 0)
|
||||
throw managedFailure(
|
||||
"invalid-directory",
|
||||
@@ -308,8 +321,6 @@ const readCoffHeader = (bytes: Buffer): CoffHeader => {
|
||||
"PE section count must be positive",
|
||||
coff + 2,
|
||||
);
|
||||
const optionalSize = bytes.readUInt16LE(coff + 16);
|
||||
const characteristics = bytes.readUInt16LE(coff + 18);
|
||||
return { coff, machine, sectionCount, optionalSize, characteristics };
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readPeCoffHeader } from "../domain/peCoffHeader.js";
|
||||
import { readElfFixedHeader } from "../domain/elfFixedHeader.js";
|
||||
import { decodePeMachine } from "../domain/peMachine.js";
|
||||
import type { ResolvedElectronFuseFile } from "./ElectronFusePath.js";
|
||||
@@ -500,8 +501,11 @@ const identifyPe = async (
|
||||
return undefined;
|
||||
// A signature establishes the existing scan boundary; only a complete COFF
|
||||
// header establishes its machine. Managed OS overrides need separate proof.
|
||||
const coff = readPeCoffHeader(bytes.subarray(4));
|
||||
const machine =
|
||||
bytes.length === 24 ? decodePeMachine(bytes.readUInt16LE(4)) : undefined;
|
||||
bytes.length === 24 && coff.ok
|
||||
? decodePeMachine(coff.value.machine)
|
||||
: undefined;
|
||||
return {
|
||||
architecture:
|
||||
machine?.nativeOsOverride === false ? machine.architecture : null,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readPeCoffHeader } from "../../domain/peCoffHeader.js";
|
||||
import { readPeSectionHeader } from "../../domain/peSectionHeader.js";
|
||||
import { readElfTableDeclarations } from "../../domain/elfDeclaredRanges.js";
|
||||
import { readPeOptionalHeaderStructure } from "../../domain/peOptionalHeader.js";
|
||||
@@ -274,7 +275,14 @@ export function* readPeSectionDirectory(
|
||||
if (pe < 64) reject("PE signature overlaps the DOS header");
|
||||
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 parsedCoff = readPeCoffHeader(coff.bytes.subarray(4, 24));
|
||||
if (!parsedCoff.ok) return reject(parsedCoff.error.message);
|
||||
const {
|
||||
sectionCount: count,
|
||||
optionalHeaderSize: optionalSize,
|
||||
machine,
|
||||
characteristics,
|
||||
} = parsedCoff.value;
|
||||
const optional = readPeOptionalHeaderStructure(
|
||||
yield request(fileSize, BigInt(pe + 24), BigInt(optionalSize)),
|
||||
);
|
||||
@@ -293,7 +301,6 @@ export function* readPeSectionDirectory(
|
||||
}
|
||||
}
|
||||
const { bits } = optional.value;
|
||||
const count = coff.u16(6);
|
||||
const start = pe + 24 + optionalSize;
|
||||
const table = yield request(fileSize, BigInt(start), BigInt(count) * 40n);
|
||||
const sections: NativeSection[] = [];
|
||||
@@ -328,8 +335,8 @@ export function* readPeSectionDirectory(
|
||||
elf_os_abi: null,
|
||||
bits,
|
||||
little: true,
|
||||
machine: coff.u16(4),
|
||||
flags: coff.u16(22),
|
||||
machine,
|
||||
flags: characteristics,
|
||||
programs: [],
|
||||
sections,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readPeCoffHeader } from "../../domain/peCoffHeader.js";
|
||||
import { readPeOptionalHeader } from "../../domain/peMachine.js";
|
||||
import { readPeSectionHeader } from "../../domain/peSectionHeader.js";
|
||||
import {
|
||||
@@ -74,8 +75,14 @@ export const readPeImage = (bytes: Buffer): PeImage => {
|
||||
const reader = new BinaryReader(bytes, true);
|
||||
const pe = peHeaderOffset(reader);
|
||||
const optional = pe + 24;
|
||||
const optionalSize = reader.u16(pe + 20);
|
||||
const machine = reader.u16(pe + 4);
|
||||
const coff = readPeCoffHeader(bytes.subarray(pe + 4, pe + 24));
|
||||
if (!coff.ok)
|
||||
throw new BinaryFormatFailure(coff.error.kind, coff.error.message);
|
||||
const {
|
||||
machine,
|
||||
sectionCount: count,
|
||||
optionalHeaderSize: optionalSize,
|
||||
} = coff.value;
|
||||
const facts = readPeOptionalHeader(
|
||||
reader.range(optional, optionalSize, "PE optional header"),
|
||||
machine,
|
||||
@@ -95,7 +102,6 @@ export const readPeImage = (bytes: Buffer): PeImage => {
|
||||
offset: 0,
|
||||
size: Math.min(sizeOfHeaders, bytes.length),
|
||||
};
|
||||
const count = reader.u16(pe + 6);
|
||||
const start = reader.table(
|
||||
BigInt(optional + optionalSize),
|
||||
count,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readPeCoffHeader } from "../../domain/peCoffHeader.js";
|
||||
import { readPeSectionHeader } from "../../domain/peSectionHeader.js";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import {
|
||||
@@ -84,8 +85,13 @@ export const readPeHeaders = (bytes: Buffer, fileSize = bytes.length) => {
|
||||
const pe = bytes.readUInt32LE(60);
|
||||
requirePeRange(bytes, pe, 24);
|
||||
if (bytes.readUInt32LE(pe) !== 0x4550) peFailure("Expected a PE signature.");
|
||||
const sectionCount = bytes.readUInt16LE(pe + 6);
|
||||
const optionalSize = bytes.readUInt16LE(pe + 20);
|
||||
const coff = readPeCoffHeader(bytes.subarray(pe + 4, pe + 24));
|
||||
if (!coff.ok) return peFailure(coff.error.message);
|
||||
const {
|
||||
sectionCount,
|
||||
optionalHeaderSize: optionalSize,
|
||||
machine,
|
||||
} = coff.value;
|
||||
const optionalHeaderOffset = pe + 24;
|
||||
requirePeRange(bytes, optionalHeaderOffset, optionalSize);
|
||||
if (optionalSize < 64) peFailure("Truncated PE optional header.");
|
||||
@@ -105,7 +111,7 @@ export const readPeHeaders = (bytes: Buffer, fileSize = bytes.length) => {
|
||||
if (headerSize < table + sectionCount * 40 || headerSize > fileSize)
|
||||
peFailure("Invalid PE SizeOfHeaders.");
|
||||
return {
|
||||
machine: bytes.readUInt16LE(pe + 4),
|
||||
machine,
|
||||
format,
|
||||
sectionCount,
|
||||
optionalHeaderOffset,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readPeCoffHeader } from "../domain/peCoffHeader.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
@@ -195,10 +196,12 @@ const assertWindowsX64Artifact = (bytes: Buffer): void => {
|
||||
if (
|
||||
offset > bytes.length - 26 ||
|
||||
bytes.toString("latin1", offset, offset + 4) !== "PE\0\0" ||
|
||||
bytes.readUInt16LE(offset + 4) !== 0x8664 ||
|
||||
bytes.readUInt16LE(offset + 24) !== 0x20b
|
||||
)
|
||||
throw new Error("Windows native artifact is not a Windows x64 PE32+ image");
|
||||
const coff = readPeCoffHeader(bytes.subarray(offset + 4, offset + 24));
|
||||
if (!coff.ok || coff.value.machine !== 0x8664)
|
||||
throw new Error("Windows native artifact is not a Windows x64 PE32+ image");
|
||||
};
|
||||
|
||||
let cached: WindowsNativeLoadResult | undefined;
|
||||
|
||||
Reference in new issue
Block a user