fix: unify executable format ownership across artifact readers

Centralize ELF section and Godot trailer declarations while preserving caller admission policies. Route six PE entrypoints through canonical DOS ownership so relocation data cannot establish a PE header, and update NativeAOT interpretation identity and verifier dependency coverage.
This commit is contained in:
morluto committed 2026-10-26 16:32:49 +08:00
1 parent 9341b116ef
commit 7adfcc4467
28 files changed
+752 -226

No files matched your search

+1
View File
@@ -128,6 +128,7 @@ export const SCOPES = {
"tests/boundary/process/releaseArtifactCanary.test.ts",
"src/windows/**",
"src/domain/windowsNativeArtifact*.ts",
"src/domain/dosMz*.ts",
"src/domain/peCoffHeader*.ts",
"src/domain/result.ts",
"scripts/lib/verifier-source-loader.mjs",
+23 -21
View File
@@ -1,3 +1,4 @@
import { readElfSectionHeader } from "../domain/elfSectionHeader.js";
import { readElfProgramHeader } from "../domain/elfProgramHeader.js";
import { readElfRawTableDeclarations } from "../domain/elfRawTableDeclarations.js";
import type { FileHandle } from "node:fs/promises";
@@ -158,27 +159,34 @@ export const readMipsElfMetadata = async (
for (let i = 0; i < count; i += 1) {
checkCancelled();
const position = i * table.entrySize;
const parsedProgram = !table.optional
? readElfProgramHeader(
const parsedRecord = table.optional
? readElfSectionHeader(
contents.value.subarray(position, position + table.entrySize),
32,
little,
)
: undefined;
if (parsedProgram !== undefined && !parsedProgram.ok)
return err(parsedProgram.error.message);
const program = parsedProgram?.value;
const type = program?.type ?? u32(contents.value, position + 4);
: readElfProgramHeader(
contents.value.subarray(position, position + table.entrySize),
32,
little,
);
if (!parsedRecord.ok) return err(parsedRecord.error.message);
const declaration = parsedRecord.value;
const { type } = declaration;
const declaredOffset = Number(declaration.offset);
const declaredSize = Number(
"fileSize" in declaration ? declaration.fileSize : declaration.size,
);
if (table.optional && type === SHT_GNU_ATTRIBUTES) {
if (gnuAttributes !== null)
return err("duplicate MIPS GNU attribute sections");
const size = u32(contents.value, position + 20);
const size = declaredSize;
if (size === 0 || size > MIPS_GNU_ATTRIBUTES_MAX_BYTES)
return err(
`MIPS GNU attribute section size ${size} is outside REA's inspected bound of ${MIPS_GNU_ATTRIBUTES_MAX_BYTES} bytes`,
);
const section = await read(
u32(contents.value, position + 16),
declaredOffset,
size,
"GNU attribute section",
);
@@ -190,26 +198,20 @@ export const readMipsElfMetadata = async (
}
if (
metadata.type === 3 &&
table.optional &&
"entrySize" in declaration &&
(type === 4 || type === 9 || type === 19)
)
relocationSections.push({
type,
offset: u32(contents.value, position + 16),
size: u32(contents.value, position + 20),
entrySize: u32(contents.value, position + 36),
offset: declaredOffset,
size: declaredSize,
entrySize: Number(declaration.entrySize),
});
if (type !== table.recordType) continue;
if (found) return err(`duplicate MIPS ABI records in ${table.label}`);
found = true;
const offset =
program !== undefined
? Number(program.offset)
: u32(contents.value, position + 16);
const size =
program !== undefined
? Number(program.fileSize)
: u32(contents.value, position + 20);
const offset = declaredOffset;
const size = declaredSize;
if (size !== 24)
return err("unsupported MIPS ABI record size; expected 24 bytes");
const candidate = await read(offset, 24, "ABI record");
+31 -7
View File
@@ -1,7 +1,9 @@
import { observeMzWindowsHeader } from "../domain/dosMz.js";
import { readPeCoffHeader } from "../domain/peCoffHeader.js";
import {
hasGodotPckHeader,
readGodotPckHeaderFacts,
readGodotPckTrailerFacts,
} from "../domain/godotPckFormat.js";
import type { FileHandle } from "node:fs/promises";
import { createHash } from "node:crypto";
@@ -189,7 +191,15 @@ export const inspectDesktopFile = async (
};
if (prefix.length === 64 && prefix.readUInt16LE(0) === 0x5a4d)
await observe(() =>
inspectPeFile(prefix, row, read, fact, unityManagedDirectory, signal),
inspectPeFile(
prefix,
size,
row,
read,
fact,
unityManagedDirectory,
signal,
),
);
if (
(native || (prefix.length >= 2 && prefix.readUInt16LE(0) === 0x5a4d)) &&
@@ -203,15 +213,29 @@ type RecordDesktopFact = (observation: DesktopFact) => void;
const inspectPeFile = async (
prefix: Buffer,
size: number,
row: DesktopFile,
read: DesktopByteRead,
fact: RecordDesktopFact,
unityManagedDirectory: string | null,
signal?: AbortSignal,
): Promise<void> => {
const peOffset = prefix.readUInt32LE(0x3c);
const observed = observeMzWindowsHeader(prefix, size);
if (observed.state === "candidate") {
row.issues.push("MZ Windows new-header candidate is unresolved");
return;
}
if (observed.state !== "signature-needed" && observed.state !== "declared")
return;
const peOffset = observed.offset;
const coff = await read(peOffset, 24);
if (coff.toString("latin1", 0, 4) !== "PE\0\0") return;
const confirmed = observeMzWindowsHeader(prefix, size, {
offset: peOffset,
bytes: coff,
});
if (confirmed.state === "candidate")
row.issues.push("MZ Windows new-header candidate is unresolved");
if (confirmed.state !== "declared" || confirmed.signature !== "PE") return;
const header = readPeCoffHeader(coff.subarray(4));
if (!header.ok) {
row.issues.push(header.error.message);
@@ -327,14 +351,14 @@ const inspectPckTrailer = async (
fact: RecordDesktopFact,
): Promise<void> => {
const trailer = await read(size - 12, 12);
if (trailer.toString("latin1", 8, 12) === "GDPC") {
const packSize = trailer.readBigUInt64LE(0);
if (packSize < 20n || packSize > BigInt(size - 12))
const declaration = readGodotPckTrailerFacts(trailer, size);
if (declaration.state === "declared") {
const { start: offset, packBytes } = declaration;
if (offset === null || packBytes < 20n)
throw new ArtifactReaderFailure(
"format",
"Embedded PCK trailer declares an invalid pack extent",
);
const offset = size - 12 - Number(packSize);
const bytes = await read(offset, 20);
if (!hasGodotPckHeader(bytes))
throw new ArtifactReaderFailure(
+10 -12
View File
@@ -1,5 +1,8 @@
import type { FileHandle } from "node:fs/promises";
import { GODOT_PCK_MAGIC } from "../domain/godotPckFormat.js";
import {
hasGodotPckHeader,
readGodotPckTrailerFacts,
} from "../domain/godotPckFormat.js";
import { ArtifactReaderFailure } from "./ArtifactReader.js";
import { assertArtifactReadActive } from "./ArtifactFileOperation.js";
@@ -62,20 +65,15 @@ export const locateGodotPck = async (
prefix !== undefined && prefix.length >= 4
? Buffer.from(prefix.buffer, prefix.byteOffset, 4)
: await readPckBytes(handle, 0, 4, size, signal);
if (magic.readUInt32LE(0) === GODOT_PCK_MAGIC)
return { start: 0, end: size, embedded: false };
if (hasGodotPckHeader(magic)) return { start: 0, end: size, embedded: false };
if (size < 12) return undefined;
const trailer = await readPckBytes(handle, size - 12, 12, size, signal);
if (trailer.readUInt32LE(8) !== GODOT_PCK_MAGIC) return undefined;
const end = size - 12;
const bytes = trailer.readBigUInt64LE(0);
const declaration = readGodotPckTrailerFacts(trailer, size);
// The terminal marker is only a candidate until its range resolves to a PCK header.
if (bytes < 4n || bytes > BigInt(end)) return undefined;
const start = end - Number(bytes);
if (
(await readPckBytes(handle, start, 4, end, signal)).readUInt32LE(0) !==
GODOT_PCK_MAGIC
)
if (declaration.state === "absent") return undefined;
const { start, end, packBytes } = declaration;
if (start === null || packBytes < 4n) return undefined;
if (!hasGodotPckHeader(await readPckBytes(handle, start, 4, end, signal)))
return undefined;
return { start, end, embedded: true };
};
+23 -12
View File
@@ -1,3 +1,4 @@
import { readElfSectionHeader } from "../../domain/elfSectionHeader.js";
import { readElfProgramHeader } from "../../domain/elfProgramHeader.js";
import { readElfRawTableDeclarations } from "../../domain/elfRawTableDeclarations.js";
import { createHash } from "node:crypto";
@@ -402,18 +403,28 @@ const locateElfNotes = async (
if (sections === undefined) opaque = opaque || shnum > 0;
else
for (const entry of sections) {
if (readU32(entry, 4, little) !== SHT_NOTE) continue;
const note = elf64
? {
offset: readU64(entry, 24, little),
length: readU64(entry, 32, little),
align: readU64(entry, 48, little),
}
: {
offset: readU32(entry, 16, little),
length: readU32(entry, 20, little),
align: readU32(entry, 32, little),
};
const parsedSection = readElfSectionHeader(
entry,
header.value.elfClass,
little,
);
if (!parsedSection.ok) continue;
const section = parsedSection.value;
if (section.type !== SHT_NOTE) continue;
const note = {
offset:
section.offset <= BigInt(Number.MAX_SAFE_INTEGER)
? Number(section.offset)
: undefined,
length:
section.size <= BigInt(Number.MAX_SAFE_INTEGER)
? Number(section.size)
: undefined,
align:
section.alignment <= BigInt(Number.MAX_SAFE_INTEGER)
? Number(section.alignment)
: undefined,
};
const scanned = await scanElfNoteTable(
carrier,
note,
+24 -29
View File
@@ -1,3 +1,4 @@
import { readElfSectionHeader } from "./elfSectionHeader.js";
import { readElfProgramHeader } from "./elfProgramHeader.js";
import { readElfRawTableDeclarations } from "./elfRawTableDeclarations.js";
import { readElfFixedHeader, type ElfFixedHeader } from "./elfFixedHeader.js";
@@ -21,19 +22,6 @@ export interface ElfTableDeclarations {
readonly sections: ElfDeclaredTable & { readonly namesIndex: number };
}
const reader = (bytes: Buffer, header: ElfFixedHeader) => {
const little = header.byteOrder === "little";
const u32 = (at: number) =>
little ? bytes.readUInt32LE(at) : bytes.readUInt32BE(at);
const word = (at: number) =>
header.elfClass === 32
? BigInt(u32(at))
: little
? bytes.readBigUInt64LE(at)
: bytes.readBigUInt64BE(at);
return { u32, word };
};
/** Zero-sized/inactive declarations own no file bytes. Arithmetic stays exact. */
const rangeFailure = (
label: string,
@@ -86,24 +74,30 @@ const extendedCounts = (
const wide = header.elfClass === 64;
if (first.length !== (wide ? 64 : 40))
return err("truncated ELF extended section-zero header");
const zero = reader(first, header);
if (zero.u32(4) !== 0)
const parsed = readElfSectionHeader(
first,
header.elfClass,
header.byteOrder === "little",
);
if (!parsed.ok) return err(parsed.error.message);
const zero = parsed.value;
if (zero.type !== 0)
return err(
"ELF extended numbering requires an inactive SHT_NULL section zero",
);
const counts = { ...initial };
if (counts.sectionCount === 0n) {
counts.sectionCount = zero.word(wide ? 32 : 20);
counts.sectionCount = zero.size;
if (counts.sectionCount < 0xff00n)
return err("invalid ELF extended section count");
}
if (counts.namesIndex === 0xffff) {
counts.namesIndex = zero.u32(wide ? 40 : 24);
counts.namesIndex = zero.link;
if (counts.namesIndex < 0xff00)
return err("invalid ELF extended section-name index");
}
if (counts.programCount === 0xffffn) {
counts.programCount = BigInt(zero.u32(wide ? 44 : 28));
counts.programCount = BigInt(zero.info);
if (counts.programCount < 0xffffn)
return err("invalid ELF extended program count");
}
@@ -196,7 +190,6 @@ function* inspectTableRanges(
header: ElfFixedHeader,
fileSize: number,
): Generator<ElfDeclarationRead, Result<null, string>, Buffer> {
const wide = header.elfClass === 64;
const perRead = Math.max(1, Math.floor((64 * 1024) / table.width));
for (let index = 0; index < table.count; index += perRead) {
const count = Math.min(perRead, table.count - index);
@@ -206,24 +199,26 @@ function* inspectTableRanges(
size,
};
if (bytes.length !== size) return err(`truncated ELF ${kind} header table`);
const r = reader(bytes, header);
for (let item = 0; item < count; item++) {
const at = item * table.width;
const program =
const parsed =
kind === "program"
? readElfProgramHeader(
bytes.subarray(at, at + table.width),
header.elfClass,
header.byteOrder === "little",
)
: undefined;
if (program !== undefined && !program.ok)
return err(program.error.message);
const facts = program?.value;
const type = facts?.type ?? r.u32(at + 4);
if (type === 0 || (kind === "section" && type === 8)) continue;
const offset = facts?.offset ?? r.word(at + (wide ? 24 : 16));
const length = facts?.fileSize ?? r.word(at + (wide ? 32 : 20));
: readElfSectionHeader(
bytes.subarray(at, at + table.width),
header.elfClass,
header.byteOrder === "little",
);
if (!parsed.ok) return err(parsed.error.message);
const facts = parsed.value;
if (facts.type === 0 || (kind === "section" && facts.type === 8))
continue;
const offset = facts.offset;
const length = "fileSize" in facts ? facts.fileSize : facts.size;
const failure = rangeFailure(
`${kind} ${String(index + item)}`,
offset,
+91
View File
@@ -0,0 +1,91 @@
import { expect, it } from "vitest";
import { readElfSectionHeader } from "./elfSectionHeader.js";
const formats = [
{ bits: 32, littleEndian: false },
{ bits: 32, littleEndian: true },
{ bits: 64, littleEndian: false },
{ bits: 64, littleEndian: true },
] as const;
it.each(formats)(
"preserves exact ELF$bits section and section-zero fields (littleEndian=$littleEndian)",
({ bits, littleEndian }) => {
const wide = bits === 64;
const length = wide ? 64 : 40;
const backing = Buffer.alloc(length + 19, 0xa5);
const bytes = backing.subarray(7, 7 + length);
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const values = wide
? [
0xffffffffffffffffn,
0xfedcba9876543210n,
0x8000000000000001n,
0x123456789abcdef0n,
0x8888888888888888n,
0x7777777777777777n,
]
: [
0xffffffffn,
0xfedcba98n,
0x80000001n,
0x12345678n,
0x88888888n,
0x77777777n,
];
view.setUint32(0, 0xfedcba98, littleEndian);
view.setUint32(4, 0x89abcdef, littleEndian);
view.setUint32(wide ? 40 : 24, 0xffffffff, littleEndian);
view.setUint32(wide ? 44 : 28, 0x80000001, littleEndian);
const offsets = wide ? [8, 16, 24, 32, 48, 56] : [8, 12, 16, 20, 32, 36];
values.forEach((value, index) => {
if (wide) view.setBigUint64(offsets[index]!, value, littleEndian);
else view.setUint32(offsets[index]!, Number(value), littleEndian);
});
const before = Buffer.from(backing);
const parsed = readElfSectionHeader(
backing.subarray(7),
bits,
littleEndian,
);
expect(backing.equals(before)).toBe(true);
backing.fill(0);
expect(parsed).toEqual({
ok: true,
value: {
nameOffset: 0xfedcba98,
type: 0x89abcdef,
flags: values[0],
address: values[1],
offset: values[2],
size: values[3],
link: 0xffffffff,
info: 0x80000001,
alignment: values[4],
entrySize: values[5],
},
});
},
);
it.each(formats)(
"refuses each incomplete ELF$bits section (littleEndian=$littleEndian)",
({ bits, littleEndian }) => {
const length = bits === 64 ? 64 : 40;
const backing = Buffer.alloc(length + 7);
for (let observed = 0; observed < length; observed++)
expect(
readElfSectionHeader(
backing.subarray(7, 7 + observed),
bits,
littleEndian,
),
).toEqual({
ok: false,
error: {
kind: "malformed",
message: "ELF section header is truncated",
},
});
},
);
+32
View File
@@ -0,0 +1,32 @@
import { err, ok } from "./result.js";
/** Fixed ELF32/ELF64 section declarations; consumers own names, numbering and mapping admission. */
export const readElfSectionHeader = (
bytes: Uint8Array,
bits: 32 | 64,
littleEndian: boolean,
) => {
const wide = bits === 64;
if (bytes.byteLength < (wide ? 64 : 40))
return err({
kind: "malformed" as const,
message: "ELF section header is truncated",
});
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const word = (at: number): bigint =>
wide
? view.getBigUint64(at, littleEndian)
: BigInt(view.getUint32(at, littleEndian));
return ok({
nameOffset: view.getUint32(0, littleEndian),
type: view.getUint32(4, littleEndian),
flags: word(8),
address: word(wide ? 16 : 12),
offset: word(wide ? 24 : 16),
size: word(wide ? 32 : 20),
link: view.getUint32(wide ? 40 : 24, littleEndian),
info: view.getUint32(wide ? 44 : 28, littleEndian),
alignment: word(wide ? 48 : 32),
entrySize: word(wide ? 56 : 36),
});
};
+31
View File
@@ -29,3 +29,34 @@ export const readGodotPckHeaderFacts = (
enginePatch: view.getUint32(16, true),
};
};
/** A terminal marker declares an extent; the located header still needs confirmation. */
export type GodotPckTrailerFacts =
| { readonly state: "absent" }
| {
readonly state: "declared";
readonly packBytes: bigint;
readonly start: number | null;
readonly end: number;
};
/** Decode an admitted complete 12-byte trailer and its host-sized source extent. */
export const readGodotPckTrailerFacts = (
trailer: Uint8Array,
fileSize: number,
): GodotPckTrailerFacts => {
if (!hasGodotPckHeader(trailer.subarray(8, 12))) return { state: "absent" };
const view = new DataView(
trailer.buffer,
trailer.byteOffset,
trailer.byteLength,
);
const packBytes = view.getBigUint64(0, true);
const end = fileSize - 12;
return {
state: "declared",
packBytes,
start: packBytes > BigInt(end) ? null : end - Number(packBytes),
end,
};
};
+1 -1
View File
@@ -241,7 +241,7 @@ describe("read-only NativeAOT PE parser", () => {
describe("NativeAOT PE signature admission", () => {
it.each([
["MZ", "Not a PE image"],
["PE", "Invalid PE signature"],
["PE", "MZ header does not declare a PE image"],
])(
"rejects a high-bit lookalike of the %s signature",
(signature, reason) => {
+6 -8
View File
@@ -1,3 +1,4 @@
import { observeMzWindowsHeader } from "../dosMz.js";
import { createHash } from "node:crypto";
import { readPeOptionalHeaderFixedFields } from "../peOptionalHeader.js";
import { readPeCoffHeader } from "../peCoffHeader.js";
@@ -16,7 +17,7 @@ export const NATIVE_AOT_MAX_WORK_UNITS = 64 * 1024 * 1024;
export const NATIVE_AOT_MAX_WORKING_MEMORY_BYTES = 64 * 1024 * 1024;
/** Version of the built-in read-only PE metadata interpretation in profiles. */
export const NATIVE_AOT_PROFILE_CONTRACT_REVISION =
"rtr-9.1-x64-pe-read-only-v7";
"rtr-9.1-x64-pe-read-only-v8";
export type NativeAotParserLimits = {
readonly sourceBytes: number;
@@ -490,11 +491,12 @@ class PeImage {
this.#bytes = bytes;
if (bytes.length < 0x40 || bytes.toString("latin1", 0, 2) !== "MZ")
throw new Error("Not a PE image");
const peOffset = this.#u32(0x3c);
const observed = observeMzWindowsHeader(bytes);
if (observed.state !== "declared" || observed.signature !== "PE")
throw new Error("MZ header does not declare a PE image");
const peOffset = observed.offset;
if (peOffset < 0x40) throw new Error("PE header overlaps the DOS header");
this.#range(peOffset, 24);
if (bytes.toString("latin1", peOffset, peOffset + 4) !== "PE\0\0")
throw new Error("Invalid PE signature");
const coff = readPeCoffHeader(bytes.subarray(peOffset + 4, peOffset + 24));
if (!coff.ok) throw new Error(coff.error.message);
this.#machine = coff.value.machine;
@@ -661,10 +663,6 @@ class PeImage {
)
throw new Error("PE field leaves the captured file");
}
#u32(offset: number): number {
this.#range(offset, 4);
return this.#bytes.readUInt32LE(offset);
}
}
const hex = (value: bigint): string => `0x${value.toString(16)}`;
+10 -6
View File
@@ -1,3 +1,4 @@
import { observeMzWindowsHeader } from "./dosMz.js";
import { createHash } from "node:crypto";
import { readPeCoffHeader } from "./peCoffHeader.js";
import { err, ok, type Result } from "./result.js";
@@ -90,12 +91,15 @@ const validateWindowsX64Artifact = (
"Windows native artifact is not a PE image",
),
);
const offset = bytes.readUInt32LE(0x3c);
if (
offset > bytes.length - 26 ||
bytes.toString("latin1", offset, offset + 4) !== "PE\0\0" ||
bytes.readUInt16LE(offset + 24) !== 0x20b
)
const observed = observeMzWindowsHeader(bytes);
if (observed.state !== "declared" || observed.signature !== "PE")
return err(
new WindowsNativeArtifactError(
"Windows native artifact is not a Windows x64 PE32+ image",
),
);
const offset = observed.offset;
if (offset > bytes.length - 26 || bytes.readUInt16LE(offset + 24) !== 0x20b)
return err(
new WindowsNativeArtifactError(
"Windows native artifact is not a Windows x64 PE32+ image",
+9 -5
View File
@@ -1,3 +1,4 @@
import { observeMzWindowsHeader } from "../domain/dosMz.js";
import {
readPeOptionalHeaderFixedFields,
readPeOptionalHeaderDirectory,
@@ -295,15 +296,18 @@ const readCliHeader = (
};
const readCoffHeader = (bytes: Buffer): CoffHeader => {
const peOffset = bytes.readUInt32LE(0x3c);
requireRange(bytes, peOffset, 24, "pe.coff-header");
if (bytes.readUInt32LE(peOffset) !== 0x0000_4550)
const observed = observeMzWindowsHeader(bytes);
if (observed.state === "candidate" || observed.state === "signature-needed")
requireRange(bytes, observed.offset, 24, "pe.coff-header");
if (observed.state !== "declared" || observed.signature !== "PE")
throw managedFailure(
"invalid-directory",
"pe.signature",
"PE signature is invalid",
peOffset,
"MZ header does not declare a PE image",
0x3c,
);
const peOffset = observed.offset;
requireRange(bytes, peOffset, 24, "pe.coff-header");
const coff = peOffset + 4;
const parsed = readPeCoffHeader(bytes.subarray(coff, coff + 20));
if (!parsed.ok)
+9 -4
View File
@@ -1,3 +1,4 @@
import { observeMzWindowsHeader } from "../domain/dosMz.js";
import { readPeCoffHeader } from "../domain/peCoffHeader.js";
import { readElfFixedHeader } from "../domain/elfFixedHeader.js";
import { decodePeMachine } from "../domain/peMachine.js";
@@ -484,20 +485,24 @@ const identifyPe = async (
const header = await readAt(0, 64);
if (header.length < 64 || header[0] !== 0x4d || header[1] !== 0x5a)
return undefined;
const offset = Buffer.from(
const prefix = Buffer.from(
header.buffer,
header.byteOffset,
header.byteLength,
).readUInt32LE(0x3c);
);
const observed = observeMzWindowsHeader(prefix, size);
if (observed.state !== "signature-needed" && observed.state !== "declared")
return undefined;
const offset = observed.offset;
if (
!Number.isSafeInteger(offset) ||
offset < 64 ||
offset > ELECTRON_FUSE_LIMITS.maximumPeHeaderOffset ||
offset > size - 4
)
return undefined;
const bytes = Buffer.from(await readAt(offset, 24));
if (bytes.length < 4 || bytes.readUInt32LE(0) !== 0x00004550)
const confirmed = observeMzWindowsHeader(prefix, size, { offset, bytes });
if (confirmed.state !== "declared" || confirmed.signature !== "PE")
return undefined;
// A signature establishes the existing scan boundary; only a complete COFF
// header establishes its machine. Managed OS overrides need separate proof.
+19
View File
@@ -566,3 +566,22 @@ it("keeps precision admission limited to consumed ELF program fields and types",
failures: [{ reason: "ELF segment 0 is not file-backed" }],
});
});
it("keeps section precision checks on consumed declarations and mapping fields", async () => {
const bytes = buildElf64Image([{ name: "selected", value: 64, size: 4 }]);
const expected = await readElfSymbols(bytes, ["selected"]);
const table = Number(bytes.readBigUInt64LE(40));
// An unrelated null record may carry uninterpreted declaration words.
for (const relative of [8, 16, 24, 32, 48, 56])
bytes.writeBigUInt64LE(0xffffffffffffffffn, table + relative);
// Alignment is not consumed for this symbol-table mapping.
bytes.writeBigUInt64LE(0xffffffffffffffffn, table + 64 + 48);
expect(await readElfSymbols(bytes, ["selected"])).toEqual(expected);
bytes.writeBigUInt64LE(0xffffffffffffffffn, table + 64 + 8);
expect(await readElfSymbols(bytes, ["selected"])).toEqual({
symbols: [],
failures: [
{ reason: "Dynamic symbol table has an unrepresentable section header" },
],
});
});
+34 -30
View File
@@ -1,3 +1,4 @@
import { readElfSectionHeader } from "../domain/elfSectionHeader.js";
import { readElfProgramHeader } from "../domain/elfProgramHeader.js";
import { readElfSymbol } from "../domain/elfSymbol.js";
import { readElfRawTableDeclarations } from "../domain/elfRawTableDeclarations.js";
@@ -244,15 +245,13 @@ export const readElfSymbols = async (
* offset must be the single file interpretation of its address range.
*/
const sectionMappingFailure = (
header: number,
section: { readonly flags: bigint; readonly address: bigint },
offset: number,
size: number,
): string | undefined => {
const flags = is64 ? reader.u64(header + 8) : reader.u32(header + 8);
const address = is64
? reader.u64(header + 0x10)
: reader.u32(header + 0x0c);
if (flags === null || address === null)
const flags = Number(section.flags);
const address = Number(section.address);
if (!Number.isSafeInteger(flags) || !Number.isSafeInteger(address))
return "has an unrepresentable section header";
if ((flags & SHF_ALLOC) === 0) return undefined;
const mapped = toOffset(address, size);
@@ -268,48 +267,53 @@ export const readElfSymbols = async (
signal?.throwIfAborted();
}
const header = e_shoff + index * e_shentsize;
if (reader.u32(header + 4) !== SHT_DYNSYM) continue;
const offset = is64
? reader.u64(header + 0x18)
: reader.u32(header + 0x10);
const size = is64 ? reader.u64(header + 0x20) : reader.u32(header + 0x14);
const width = is64
? reader.u64(header + 0x38)
: reader.u32(header + 0x24);
const link = reader.u32(header + (is64 ? 0x28 : 0x18));
const parsedSection = readElfSectionHeader(
bytes.subarray(header, header + e_shentsize),
is64 ? 64 : 32,
true,
);
if (!parsedSection.ok) return failure(parsedSection.error.message);
const section = parsedSection.value;
if (section.type !== SHT_DYNSYM) continue;
const offset = Number(section.offset);
const size = Number(section.size);
const link = section.link;
if (
offset === null ||
size === null ||
width !== entrySize ||
!Number.isSafeInteger(offset) ||
!Number.isSafeInteger(size) ||
section.entrySize !== BigInt(entrySize) ||
offset > bytes.length ||
size > bytes.length - offset ||
size % entrySize !== 0
)
return failure("Dynamic symbol table is malformed");
if (link === null || link >= e_shnum)
if (link >= e_shnum)
return failure("Dynamic symbol string section link is invalid");
const stringHeader = e_shoff + link * e_shentsize;
const stringOffset = is64
? reader.u64(stringHeader + 0x18)
: reader.u32(stringHeader + 0x10);
const stringSize = is64
? reader.u64(stringHeader + 0x20)
: reader.u32(stringHeader + 0x14);
const parsedStrings = readElfSectionHeader(
bytes.subarray(stringHeader, stringHeader + e_shentsize),
is64 ? 64 : 32,
true,
);
if (!parsedStrings.ok) return failure(parsedStrings.error.message);
const strings = parsedStrings.value;
const stringOffset = Number(strings.offset);
const stringSize = Number(strings.size);
if (
reader.u32(stringHeader + 4) !== 3 ||
stringOffset === null ||
stringSize === null ||
strings.type !== 3 ||
!Number.isSafeInteger(stringOffset) ||
!Number.isSafeInteger(stringSize) ||
stringOffset > bytes.length ||
stringSize > bytes.length - stringOffset
)
return failure(
"Dynamic symbol strings are out of bounds or not a string table",
);
const symbolMapping = sectionMappingFailure(header, offset, size);
const symbolMapping = sectionMappingFailure(section, offset, size);
if (symbolMapping !== undefined)
return failure(`Dynamic symbol table ${symbolMapping}`);
const stringMapping = sectionMappingFailure(
stringHeader,
strings,
stringOffset,
stringSize,
);
+15 -33
View File
@@ -1,3 +1,4 @@
import { readElfSectionHeader } from "../domain/elfSectionHeader.js";
import { readElfProgramHeader } from "../domain/elfProgramHeader.js";
import { MAX_FLUTTER_BUILD_ID_BYTES } from "../domain/flutter/flutterBuildAnalysis.js";
import type { FileHandle } from "node:fs/promises";
@@ -98,44 +99,30 @@ export const readElfBuildIds = async (
signal?.throwIfAborted();
}
const base = index * table.width;
const parsedProgram = table.program
const parsedRecord = table.program
? readElfProgramHeader(
bytes.subarray(base, base + table.width),
h.is64 ? 64 : 32,
true,
)
: undefined;
if (parsedProgram !== undefined && !parsedProgram.ok)
return failure(parsedProgram.error.message);
const program = parsedProgram?.value;
if (
(program?.type ?? bytes.readUInt32LE(base + 4)) !==
(table.program ? 4 : 7)
)
continue;
const offset =
program !== undefined
? Number(program.offset)
: h.is64
? Number(bytes.readBigUInt64LE(base + 0x18))
: bytes.readUInt32LE(base + 0x10);
const size =
program !== undefined
? Number(program.fileSize)
: h.is64
? Number(bytes.readBigUInt64LE(base + 0x20))
: bytes.readUInt32LE(base + 0x14);
: readElfSectionHeader(
bytes.subarray(base, base + table.width),
h.is64 ? 64 : 32,
true,
);
if (!parsedRecord.ok) return failure(parsedRecord.error.message);
const record = parsedRecord.value;
if (record.type !== (table.program ? 4 : 7)) continue;
const offset = Number(record.offset);
const size = Number("fileSize" in record ? record.fileSize : record.size);
if (
!Number.isSafeInteger(offset) ||
!Number.isSafeInteger(size) ||
size > totalBytes - offset
)
return failure("ELF note range is not file-backed");
if (!table.program) {
const flags = h.is64
? bytes.readBigUInt64LE(base + 8)
: BigInt(bytes.readUInt32LE(base + 8));
if ((flags & 0x800n) !== 0n) {
if ("size" in record) {
if ((record.flags & 0x800n) !== 0n) {
if (!compressedNotes) {
failures.push({
reason: "Compressed ELF note sections are unsupported",
@@ -146,12 +133,7 @@ export const readElfBuildIds = async (
}
}
if (size === 0) continue;
const alignment = elfNoteAlignment(
program?.alignment ??
(h.is64
? bytes.readBigUInt64LE(base + 48)
: bytes.readUInt32LE(base + 32)),
);
const alignment = elfNoteAlignment(record.alignment);
const key = `${offset}:${size}:${alignment}`;
if (ranges.has(key)) continue;
if (ranges.size === MAX_NOTE_RANGES) {
+1 -1
View File
@@ -665,7 +665,7 @@ describe("Ghidra platform support", () => {
executable_role: target.executableRole,
managed: target.managed,
native_aot_metadata: {
contract_revision: "rtr-9.1-x64-pe-read-only-v7",
contract_revision: "rtr-9.1-x64-pe-read-only-v8",
source_bytes: 128 * 1024 * 1024,
work_units: 64 * 1024 * 1024,
working_memory_bytes: 64 * 1024 * 1024,
+9
View File
@@ -637,3 +637,12 @@ it("does not apply note precision admission to unrelated program records", () =>
"ELF offset or size exceeds a safe integer.",
);
});
it("keeps compressed-note refusal ahead of alignment precision admission", () => {
const bytes = compressedElfNoteSectionFixture();
const sectionTable = Number(bytes.readBigUInt64LE(40));
bytes.writeBigUInt64LE(0xffffffffffffffffn, sectionTable + 64 + 48);
expect(() => parse(bytes)).toThrow(
"Compressed ELF note sections are unsupported",
);
});
+40 -33
View File
@@ -1,3 +1,4 @@
import { readElfSectionHeader } from "../../domain/elfSectionHeader.js";
import { readElfProgramHeader } from "../../domain/elfProgramHeader.js";
import { readElfRawTableDeclarations } from "../../domain/elfRawTableDeclarations.js";
import { createHash } from "node:crypto";
@@ -97,7 +98,6 @@ class ElfReader {
constructor(
readonly bytes: Buffer,
readonly little: boolean,
readonly bits: 32 | 64,
) {}
u32(offset: number): number {
@@ -107,22 +107,6 @@ class ElfReader {
: this.bytes.readUInt32BE(offset);
}
wordBits(offset: number): bigint {
const width = this.bits === 64 ? 8 : 4;
this.require(offset, width);
if (this.bits === 32) return BigInt(this.u32(offset));
return this.little
? this.bytes.readBigUInt64LE(offset)
: this.bytes.readBigUInt64BE(offset);
}
word(offset: number): number {
const value = this.wordBits(offset);
if (value > BigInt(Number.MAX_SAFE_INTEGER))
fail("ELF offset or size exceeds a safe integer.");
return Number(value);
}
require(offset: number, length: number): void {
if (
!Number.isSafeInteger(offset) ||
@@ -316,11 +300,7 @@ export const parseElfNotes = (
const fixed = readElfFixedHeader(bytes);
if (!fixed.ok) return fail(fixed.error);
const header = fixed.value;
const reader = new ElfReader(
bytes,
header.byteOrder === "little",
header.elfClass,
);
const reader = new ElfReader(bytes, header.byteOrder === "little");
const wide = header.elfClass === 64;
const type = header.type;
const raw = readElfRawTableDeclarations(bytes, header);
@@ -372,12 +352,25 @@ export const parseElfNotes = (
if (shstrndx >= shnum)
return fail("ELF section name index is out of range.");
const namesHeader = shoff + shstrndx * shentsize;
if (reader.u32(namesHeader + 4) !== SHT_STRTAB)
const parsedNames = readElfSectionHeader(
bytes.subarray(namesHeader, namesHeader + shentsize),
header.elfClass,
reader.little,
);
if (!parsedNames.ok) return fail(parsedNames.error.message);
const namesSection = parsedNames.value;
if (namesSection.type !== SHT_STRTAB)
return fail("ELF section name table is not a string table.");
const strOff = reader.word(namesHeader + (wide ? 24 : 16));
const strSize = reader.word(namesHeader + (wide ? 32 : 20));
const rawNames = reader.slice(strOff, strSize);
const flags = reader.wordBits(namesHeader + 8);
if (
namesSection.offset > BigInt(Number.MAX_SAFE_INTEGER) ||
namesSection.size > BigInt(Number.MAX_SAFE_INTEGER)
)
return fail("ELF offset or size exceeds a safe integer.");
const rawNames = reader.slice(
Number(namesSection.offset),
Number(namesSection.size),
);
const flags = namesSection.flags;
const decoded = decodeElfSectionNameTable(
rawNames,
flags,
@@ -454,17 +447,31 @@ export const parseElfNotes = (
return fail("ELF section header table overlaps the native header.");
for (let index = 0; index < shnum; index += 1) {
const at = shoff + index * shentsize;
if (reader.u32(at + 4) !== SHT_NOTE) continue;
const nameOffset = reader.u32(at);
const fileOffset = reader.word(at + (wide ? 24 : 16));
const fileSize = reader.word(at + (wide ? 32 : 20));
const parsedSection = readElfSectionHeader(
bytes.subarray(at, at + shentsize),
header.elfClass,
reader.little,
);
if (!parsedSection.ok) return fail(parsedSection.error.message);
const section = parsedSection.value;
if (section.type !== SHT_NOTE) continue;
const nameOffset = section.nameOffset;
if (
section.offset > BigInt(Number.MAX_SAFE_INTEGER) ||
section.size > BigInt(Number.MAX_SAFE_INTEGER)
)
return fail("ELF offset or size exceeds a safe integer.");
const fileOffset = Number(section.offset);
const fileSize = Number(section.size);
reader.require(fileOffset, fileSize);
if ((reader.wordBits(at + 8) & SHF_COMPRESSED) !== 0n)
if ((section.flags & SHF_COMPRESSED) !== 0n)
throw new BinaryFormatFailure(
"unsupported",
"Compressed ELF note sections are unsupported; descriptor file offsets require uncompressed note storage.",
);
const alignment = noteAlignment(reader.word(at + (wide ? 48 : 32)));
if (section.alignment > BigInt(Number.MAX_SAFE_INTEGER))
return fail("ELF offset or size exceeds a safe integer.");
const alignment = noteAlignment(Number(section.alignment));
const sectionName =
shstrtab === null
? { name: null, limitation: null }
+30 -21
View File
@@ -1,3 +1,4 @@
import { readElfSectionHeader } from "../../domain/elfSectionHeader.js";
import { readElfProgramHeader } from "../../domain/elfProgramHeader.js";
import { readPeCoffHeader } from "../../domain/peCoffHeader.js";
import { readPeSectionHeader } from "../../domain/peSectionHeader.js";
@@ -102,7 +103,6 @@ export function* readElfSectionDirectory(
const { header, programs, sections: sectionTable } = parsed.value;
const { elfClass: bits, byteOrder, machine } = header;
const little = byteOrder === "little";
const wide = bits === 64;
const {
offset: program_offset,
width: program_width,
@@ -153,27 +153,31 @@ export function* readElfSectionDirectory(
const sections: NativeSection[] = [];
let section_names_limitation: string | undefined;
if (count !== 0) {
const table = new BinaryReader(
yield request(
fileSize,
sectionOffset,
BigInt(count) * BigInt(sectionWidth),
),
little,
const table = yield request(
fileSize,
sectionOffset,
BigInt(count) * BigInt(sectionWidth),
);
let names: Buffer | null = null;
if (namesIndex !== 0) {
const h = namesIndex * sectionWidth;
if (table.u32(h + 4) !== 3)
const parsedNames = readElfSectionHeader(
table.subarray(h, h + sectionWidth),
bits,
little,
);
if (!parsedNames.ok) return reject(parsedNames.error.message);
const namesSection = parsedNames.value;
if (namesSection.type !== 3)
reject("ELF section name table is not a string table");
const rawNames = yield request(
fileSize,
table.word(h + (wide ? 24 : 16), bits),
table.word(h + (wide ? 32 : 20), bits),
namesSection.offset,
namesSection.size,
);
const decoded = decodeElfSectionNameTable(
rawNames,
table.word(h + 8, bits),
namesSection.flags,
bits,
little,
Math.max(0, STRUCTURE_BYTES - rawNames.length),
@@ -199,7 +203,14 @@ export function* readElfSectionDirectory(
let nameBytes = 0;
for (let index = 0; index < count; index++) {
const h = index * sectionWidth;
const nameOffset = table.u32(h);
const parsedSection = readElfSectionHeader(
table.subarray(h, h + sectionWidth),
bits,
little,
);
if (!parsedSection.ok) return reject(parsedSection.error.message);
const section = parsedSection.value;
const nameOffset = section.nameOffset;
let name: string | null = null;
let name_state: NativeSection["name_state"] = "unavailable";
if (names !== null) {
@@ -234,9 +245,7 @@ export function* readElfSectionDirectory(
name = decoded.name;
}
}
const type = table.u32(h + 4);
const offset = table.word(h + (wide ? 24 : 16), bits);
const size = table.word(h + (wide ? 32 : 20), bits);
const { type, offset, size } = section;
sections.push({
index,
name,
@@ -244,12 +253,12 @@ export function* readElfSectionDirectory(
header_offset: exact(sectionOffset) + h,
offset,
size,
address: table.word(h + (wide ? 16 : 12), bits),
address: section.address,
type,
flags: table.word(h + 8, bits),
link: table.u32(h + (wide ? 40 : 24)),
info: table.u32(h + (wide ? 44 : 28)),
entry_size: table.word(h + (wide ? 56 : 36), bits),
flags: section.flags,
link: section.link,
info: section.info,
entry_size: section.entrySize,
file_backed:
type !== 8 && declaredFileRange(fileSize, offset, size).file_backed,
});
+9 -2
View File
@@ -1,3 +1,4 @@
import { observeMzWindowsHeader } from "../../domain/dosMz.js";
import { readPeCoffHeader } from "../../domain/peCoffHeader.js";
import { readPeSectionHeader } from "../../domain/peSectionHeader.js";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
@@ -82,9 +83,15 @@ const intersectsResource = (
export const readPeHeaders = (bytes: Buffer, fileSize = bytes.length) => {
requirePeRange(bytes, 0, 64);
if (bytes.readUInt16LE(0) !== 0x5a4d) peFailure("Expected an MZ PE image.");
const pe = bytes.readUInt32LE(60);
const observed = observeMzWindowsHeader(bytes, fileSize);
if (observed.state === "candidate" || observed.state === "signature-needed")
requirePeRange(bytes, observed.offset, 24);
if (observed.state !== "declared" || observed.signature !== "PE")
return peFailure(
"MZ header does not declare a PE image in the inspected bytes.",
);
const pe = observed.offset;
requirePeRange(bytes, pe, 24);
if (bytes.readUInt32LE(pe) !== 0x4550) peFailure("Expected a PE signature.");
const coff = readPeCoffHeader(bytes.subarray(pe + 4, pe + 24));
if (!coff.ok) return peFailure(coff.error.message);
const {
@@ -61,3 +61,22 @@ it("keeps snapshot rebinding independent of unused ELF section and header-size a
validateRecordedCrashStructure(report, bytes, report.decoder_diagnostics),
).not.toThrow();
});
it("binds PN_XNUM through raw section-zero info without full ELF numbering admission", () => {
const report = recordedCrashSchema.parse(recordedCrashFixture());
const original = recordedCrashFixtureBytes(report);
const bytes = Buffer.concat([original, Buffer.alloc(64)]);
bytes.writeUInt16LE(0xffff, 56);
bytes.writeBigUInt64LE(BigInt(original.length), 40);
bytes.writeUInt16LE(64, 58);
// Rebinding only needs the reported count; full ELF admission separately requires SHT_NULL and reserved thresholds.
bytes.writeUInt32LE(1, original.length + 4);
bytes.writeUInt32LE(report.segments.length, original.length + 44);
expect(() =>
validateRecordedCrashStructure(report, bytes, report.decoder_diagnostics),
).not.toThrow();
bytes.writeUInt32LE(report.segments.length + 1, original.length + 44);
expect(() =>
validateRecordedCrashStructure(report, bytes, report.decoder_diagnostics),
).toThrow(/original ELF\/note headers/);
});
@@ -1,3 +1,4 @@
import { readElfSectionHeader } from "../../domain/elfSectionHeader.js";
import { readElfProgramHeader } from "../../domain/elfProgramHeader.js";
import { readElfRawTableDeclarations } from "../../domain/elfRawTableDeclarations.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
@@ -117,7 +118,9 @@ export function validateRecordedCrashStructure(
let count = programs.count;
if (count === 0xffff) {
if (sections.width < 64) return fail();
count = bytesAt(sections.offset, 64).readUInt32LE(44);
const zero = readElfSectionHeader(bytesAt(sections.offset, 64), 64, true);
if (!zero.ok) return fail();
count = zero.value.info;
}
if (count !== report.segments.length || (count > 0 && entryBytes < 56))
fail();
@@ -743,6 +743,7 @@ it.each([
{ code: true, runtime: true, package: true },
],
["src/domain/peCoffHeader.ts", { code: true, runtime: true, package: true }],
["src/domain/dosMz.ts", { code: true, runtime: true, package: true }],
[
"scripts/lib/verifier-source-loader.mjs",
{ code: true, runtime: true, package: true },
@@ -29,6 +29,7 @@ import {
fatImage,
machoImage,
} from "../../../src/artifacts/apple/MachoImage.fixture.js";
import { locateGodotPck } from "../../../src/artifacts/GodotPckLocation.js";
import { DirectoryArtifactReader } from "../../../src/artifacts/DirectoryArtifactReader.js";
import { ArtifactReaderFailure } from "../../../src/artifacts/ArtifactReader.js";
import { DesktopApplicationReader } from "../../../src/artifacts/DesktopApplicationReader.js";
@@ -216,6 +217,55 @@ it("reads sparse embedded PCK ranges without hashing or retaining a large execut
});
});
it.each([4, 19, 20])(
"keeps PCK location separate from desktop header admission for %i-byte packs",
async (packBytes) => {
const root = await createTestTempDirectory("rea-desktop-pck-minimum-");
const path = join(root, "game.exe");
const carrier = Buffer.alloc(64);
carrier.set([0x7f, 0x45, 0x4c, 0x46]);
const pack = Buffer.alloc(packBytes);
pack.write("GDPC");
const trailer = Buffer.alloc(12);
trailer.writeBigUInt64LE(BigInt(packBytes));
trailer.write("GDPC", 8);
const bytes = Buffer.concat([carrier, pack, trailer]);
await writeFile(path, bytes);
const handle = await open(path, "r");
try {
expect(await locateGodotPck(handle, bytes.length)).toEqual({
start: carrier.length,
end: carrier.length + packBytes,
embedded: true,
});
} finally {
await handle.close();
}
const { report } = await inspect(root);
const file = report.files.find((row) => row.path === "game.exe");
expect(file).toBeDefined();
if (packBytes < 20) {
expect(file?.issues).toContain(
"Embedded PCK trailer declares an invalid pack extent",
);
expect(file?.facts.some((fact) => fact.kind === "godot-pck-header")).toBe(
false,
);
} else {
expect(file?.issues).toEqual([]);
expect(file?.facts).toContainEqual({
kind: "godot-pck-header",
offset: carrier.length,
value: {
pack_version: 0,
engine_version: [0, 0, 0],
placement: "embedded-trailer",
},
});
}
},
);
it("does not promote filenames, arbitrary pak files or symlink targets into byte facts", async () => {
const root = await createTestTempDirectory("rea-desktop-unknown-");
await put(root, "GameAssembly.dll", "x");
@@ -21,11 +21,13 @@ for (const mode of ["preinstall source", "packed source-free"]) {
"scripts/lib/verifier-source-loader.mjs",
"src/domain/windowsNativeArtifact.ts",
"src/domain/peCoffHeader.ts",
"src/domain/dosMz.ts",
"src/domain/result.ts",
]
: [
"dist/domain/windowsNativeArtifact.js",
"dist/domain/peCoffHeader.js",
"dist/domain/dosMz.js",
"dist/domain/result.js",
];
for (const path of [
@@ -0,0 +1,218 @@
import { open, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { observeMzWindowsHeader } from "../../../src/domain/dosMz.js";
import { createWindowsNativeManifest } from "../../../src/domain/windowsNativeArtifact.js";
import {
NATIVE_AOT_MAX_REPORT_BYTES,
parseNativeAotPe,
} from "../../../src/domain/native/nativeAotPe.js";
import { inspectDesktopFile } from "../../../src/artifacts/DesktopFileInspection.js";
import type { DesktopFile } from "../../../src/domain/desktopApplication.js";
import { readManagedPeLayout } from "../../../src/dotnet/ManagedPeReader.js";
import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js";
import { readPeHeaders } from "../../../src/native/pe/PeResourceLayout.js";
import { peResourceFixture } from "../../../src/native/pe/PeResources.fixture.js";
import { ElectronFuseService } from "../../../src/application/electron/ElectronFuseService.js";
import { ElectronFuseProvider } from "../../../src/electron/ElectronFuseProvider.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
nativeAotPeDigest,
nativeAotPeFixture,
} from "../../fixtures/nativeaotPe.js";
import {
OBSIDIAN_FUSE_BYTES,
peHeaderFuseImage,
} from "../../fixtures/electron/fuseImage.js";
/** A valid DOS relocation owns dword60, even when those bytes point at planted PE data. */
const relocationOwnedPointer = (bytes: Buffer): Buffer => {
bytes.writeUInt16LE(bytes.length % 512, 2);
bytes.writeUInt16LE(Math.ceil(bytes.length / 512), 4);
bytes.writeUInt16LE(1, 6);
bytes.writeUInt16LE(4, 8);
bytes.writeUInt16LE(60, 24);
expect(observeMzWindowsHeader(bytes)).toEqual({ state: "absent" });
return bytes;
};
it("keeps relocation-owned DOS bytes out of managed PE layout", () => {
const bytes = relocationOwnedPointer(buildManagedPeFixture());
expect(() => readManagedPeLayout(bytes)).toThrow(/PE/u);
});
it("keeps relocation-owned DOS bytes out of PE resource/certificate header admission", () => {
const bytes = relocationOwnedPointer(peResourceFixture().bytes);
expect(() => readPeHeaders(bytes)).toThrow(/PE/u);
});
it("keeps relocation-owned DOS bytes out of NativeAOT metadata recovery", () => {
const bytes = relocationOwnedPointer(nativeAotPeFixture());
const result = parseNativeAotPe(
bytes,
nativeAotPeDigest(bytes),
NATIVE_AOT_MAX_REPORT_BYTES,
);
expect(result.summary).toMatchObject({
status: "not_applicable",
coverage: { basis: "unsupported-layout" },
});
});
it("refuses relocation-owned DOS bytes as packaged Windows native artifacts", () => {
const bytes = relocationOwnedPointer(nativeAotPeFixture());
expect(createWindowsNativeManifest("fixture", bytes)).toMatchObject({
ok: false,
});
});
it("does not scan fuse wires in a planted PE record owned by DOS relocations", async () => {
const root = await createTestTempDirectory("rea-fuse-dos-relocations-");
const path = join(root, "selected.exe");
await writeFile(
path,
relocationOwnedPointer(
peHeaderFuseImage(0x8664, OBSIDIAN_FUSE_BYTES).bytes,
),
);
const service = new ElectronFuseService(new ElectronFuseProvider());
try {
expect((await service.inspect({ path })).ok).toBe(false);
} finally {
await service.close();
}
});
it("does not emit desktop PE facts from relocation-owned DOS bytes", async () => {
const root = await createTestTempDirectory("rea-desktop-dos-relocations-");
const path = join(root, "selected.exe");
const bytes = relocationOwnedPointer(buildManagedPeFixture());
await writeFile(path, bytes);
const file = await open(path, "r");
const row: DesktopFile = {
file_id: `dsf_${"a".repeat(64)}`,
path: "selected.exe",
local_path: path,
kind: "file",
bytes: bytes.length,
identity: null,
read_status: "examined",
ranges: [],
facts: [],
issues: [],
};
try {
await inspectDesktopFile(file, row, {
bytes: 0,
maximum: 1024 * 1024,
retainedBytes: 0,
maximumRetainedBytes: 1024 * 1024,
});
} finally {
await file.close();
}
expect(
row.facts.filter(
(fact) =>
fact.kind === "native-image-header" || fact.kind === "pe-cli-directory",
),
).toEqual([]);
});
const shortDosHeader = (bytes: Buffer): Buffer => {
bytes.writeUInt16LE(bytes.length % 512, 2);
bytes.writeUInt16LE(Math.ceil(bytes.length / 512), 4);
bytes.writeUInt16LE(0, 6);
bytes.writeUInt16LE(2, 8);
bytes.writeUInt16LE(28, 24);
expect(observeMzWindowsHeader(bytes)).toMatchObject({
state: "declared",
signature: "PE",
});
return bytes;
};
it("preserves recognized PE records after short DOS headers in complete-byte readers", () => {
expect(() =>
readManagedPeLayout(shortDosHeader(buildManagedPeFixture())),
).not.toThrow();
expect(() =>
readPeHeaders(shortDosHeader(peResourceFixture().bytes)),
).not.toThrow();
const bytes = shortDosHeader(nativeAotPeFixture());
expect(
parseNativeAotPe(
bytes,
nativeAotPeDigest(bytes),
NATIVE_AOT_MAX_REPORT_BYTES,
).summary.status,
).toBe("complete");
expect(createWindowsNativeManifest("fixture", bytes)).toMatchObject({
ok: true,
});
});
it("preserves fuse inspection after a short DOS header with a confirmed PE signature", async () => {
const root = await createTestTempDirectory("rea-fuse-short-dos-");
const path = join(root, "selected.exe");
await writeFile(
path,
shortDosHeader(peHeaderFuseImage(0x8664, OBSIDIAN_FUSE_BYTES).bytes),
);
const service = new ElectronFuseService(new ElectronFuseProvider());
try {
const inspected = await service.inspect({ path });
expect(inspected).toMatchObject({
ok: true,
value: { subject: { architecture: "x86_64" } },
});
} finally {
await service.close();
}
});
it("preserves desktop PE observations after a short DOS header", async () => {
const root = await createTestTempDirectory("rea-desktop-short-dos-");
const path = join(root, "selected.exe");
const bytes = shortDosHeader(buildManagedPeFixture());
await writeFile(path, bytes);
const file = await open(path, "r");
const row: DesktopFile = {
file_id: `dsf_${"a".repeat(64)}`,
path: "selected.exe",
local_path: path,
kind: "file",
bytes: bytes.length,
identity: null,
read_status: "examined",
ranges: [],
facts: [],
issues: [],
};
try {
await inspectDesktopFile(file, row, {
bytes: 0,
maximum: 1024 * 1024,
retainedBytes: 0,
maximumRetainedBytes: 1024 * 1024,
});
} finally {
await file.close();
}
expect(row.facts.map((fact) => fact.kind)).toEqual(
expect.arrayContaining(["native-image-header", "pe-cli-directory"]),
);
});
it("keeps a PE signature outside an inspected prefix unresolved until its required range is read", () => {
const bytes = peResourceFixture().bytes;
const prefix = bytes.subarray(0, 64);
expect(observeMzWindowsHeader(prefix, bytes.length)).toMatchObject({
state: "signature-needed",
offset: 128,
});
expect(() => readPeHeaders(bytes)).not.toThrow();
expect(() => readPeHeaders(prefix, bytes.length)).toThrow(
"PE range 128 + 24 leaves the artifact.",
);
});