fix(elf): validate declared file extents before provider admission

This commit is contained in:
morluto
2026-10-24 05:00:13 +08:00
parent aa4572828f
commit 651afa4455
6 changed files with 573 additions and 67 deletions
+13
View File
@@ -968,6 +968,19 @@ dossiers, instructions, recovered data types, measured load mappings, loaded
memory bytes, and observed file offsets. Independent load-image attestation
supports DOS MZ, explicitly selected COM, and explicit raw-binary imports;
PE returns measurements with an unsupported attestation status.
ELF target opening checks the declared program and section header tables and
their file-backed ranges before selecting a provider. A declaration beyond EOF
is refused with its range and the actual file size, including a file cut one
byte short. This also refuses carved or deliberately damaged copies that retain
out-of-file declarations; they are not presented as complete smaller programs.
Files declaring no tables remain valid inputs, subject to the provider's lane.
Inactive headers, zero-length ranges and `SHT_NOBITS` memory-only sections do
not require stored payload bytes. Extended counts come from section zero.
The check streams header records in chunks of at most 64 KiB; it does not decode
section names, copy payloads or impose the metadata decoder's structural budget.
Passing it establishes physical range consistency, not complete function
discovery, loader validity or runtime behavior. Existing ISA/ABI checks remain
separate, including the bounded recordless MIPS lane.
Target admission preserves recognized PE/NE/LE/LX signatures separately from
reader layout support. A Windows header overlapping the first 64 DOS-header
bytes remains an unsupported layout in this release; it is not reported as an
+64 -5
View File
@@ -31,6 +31,7 @@ import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { err, ok, type Result } from "../domain/result.js";
import { readMipsElfMetadata } from "./MipsElfMetadata.js";
import { validateElfDeclaredRanges } from "../domain/elfDeclaredRanges.js";
import {
resolveAppBundleExecutable,
type ResolvedAppBundle,
@@ -852,6 +853,12 @@ const readExecutableMetadata = async (
signatureEvidence,
);
}
if (bytes.length >= 4 && bytes.toString("hex", 0, 4) === "7f454c46")
return readElfMetadata(handle, bytes, {
fileSize,
hostArchitecture,
signal,
});
const thin = await readThinMachoMetadata(handle, bytes, {
fileSize,
hostArchitecture,
@@ -881,11 +888,25 @@ const readExecutableMetadata = async (
if (!framed.ok) return framed;
}
}
if (
!parsed.ok ||
parsed.value.architecture !== "mips" ||
parsed.value.mips.elfClass !== 32
)
return parsed;
};
/** ELF structure and family declarations share one descriptor before provider selection. */
const readElfMetadata = async (
handle: FileHandle,
bytes: Buffer,
options: {
readonly fileSize: number;
readonly hostArchitecture: NodeJS.Architecture;
readonly signal?: AbortSignal | undefined;
},
): Promise<Result<ExecutableMetadata, ExecutableMetadataFailure>> => {
const { fileSize, hostArchitecture, signal } = options;
const parsed = parseExecutableHeader(bytes, hostArchitecture, fileSize);
if (!parsed.ok || parsed.value.format !== "elf") return parsed;
const ranges = await readElfDeclaredRanges(handle, bytes, fileSize, signal);
if (!ranges.ok) return ranges;
if (parsed.value.architecture !== "mips" || parsed.value.mips.elfClass !== 32)
return parsed;
const declarations = await readMipsElfMetadata(
handle,
@@ -909,6 +930,44 @@ const readExecutableMetadata = async (
});
};
/** Borrow the admitted descriptor; every request is at most one 64 KiB table chunk. */
const readElfDeclaredRanges = async (
handle: FileHandle,
prefix: Buffer,
fileSize: number,
signal?: AbortSignal,
): Promise<Result<null, string>> => {
const ranges = validateElfDeclaredRanges(prefix, fileSize);
let next = ranges.next();
let window: Buffer | undefined;
while (!next.done) {
throwIfTargetResolutionCancelled(signal);
const { offset, size } = next.value;
let bytes: Buffer;
if (offset + size <= prefix.length)
bytes = prefix.subarray(offset, offset + size);
else {
window ??= Buffer.allocUnsafe(64 * 1024);
bytes = window.subarray(0, size);
let length = 0;
while (length < size) {
const read = await handle.read(
bytes,
length,
size - length,
offset + length,
);
throwIfTargetResolutionCancelled(signal);
if (read.bytesRead === 0) break;
length += read.bytesRead;
}
bytes = bytes.subarray(0, length);
}
next = ranges.next(bytes);
}
return next.value;
};
type ExecutableMetadataFailure =
| string
| {
+271
View File
@@ -0,0 +1,271 @@
import { readElfFixedHeader, type ElfFixedHeader } from "./elfFixedHeader.js";
import { err, ok, type Result } from "./result.js";
/** Positional source requests; the caller owns the descriptor and cancellation. */
export interface ElfDeclarationRead {
readonly offset: number;
readonly size: number;
}
interface ElfDeclaredTable {
readonly offset: bigint;
readonly count: number;
readonly width: number;
}
export interface ElfTableDeclarations {
readonly header: ElfFixedHeader;
readonly programs: ElfDeclaredTable;
readonly sections: ElfDeclaredTable & { readonly namesIndex: number };
}
const reader = (bytes: Buffer, header: ElfFixedHeader) => {
const little = header.byteOrder === "little";
const u16 = (at: number) =>
little ? bytes.readUInt16LE(at) : bytes.readUInt16BE(at);
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 { u16, u32, word };
};
/** Zero-sized/inactive declarations own no file bytes. Arithmetic stays exact. */
const rangeFailure = (
label: string,
offset: bigint,
size: bigint,
fileSize: number,
): string | undefined =>
size !== 0n && (offset > BigInt(fileSize) || size > BigInt(fileSize) - offset)
? `truncated ELF ${label}: declared file range ${String(offset)}..${String(offset + size)} exceeds the ${String(fileSize)}-byte file`
: undefined;
interface RawTableDeclaration {
readonly offset: bigint;
readonly count: bigint;
readonly width: number;
}
const tableFailure = (
kind: "program" | "section",
table: RawTableDeclaration,
header: ElfFixedHeader,
fileSize: number,
): string | undefined => {
if (table.count === 0n) return undefined;
const wide = header.elfClass === 64;
const minimumWidth = kind === "program" ? (wide ? 56 : 32) : wide ? 64 : 40;
const label = `${kind} header table`;
if (table.width < minimumWidth) return `invalid ELF ${label} entry width`;
if (table.offset < BigInt(wide ? 64 : 52))
return `ELF ${label} overlaps the fixed header`;
return rangeFailure(
label,
table.offset,
table.count * BigInt(table.width),
fileSize,
);
};
interface TableCounts {
programCount: bigint;
sectionCount: bigint;
namesIndex: number;
}
const extendedCounts = (
first: Buffer,
header: ElfFixedHeader,
initial: TableCounts,
): Result<TableCounts, string> => {
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)
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);
if (counts.sectionCount < 0xff00n)
return err("invalid ELF extended section count");
}
if (counts.namesIndex === 0xffff) {
counts.namesIndex = zero.u32(wide ? 40 : 24);
if (counts.namesIndex < 0xff00)
return err("invalid ELF extended section-name index");
}
if (counts.programCount === 0xffffn) {
counts.programCount = BigInt(zero.u32(wide ? 44 : 28));
if (counts.programCount < 0xffffn)
return err("invalid ELF extended program count");
}
return ok(counts);
};
const TABLE_FIELDS = {
32: {
programOffset: 28,
sectionOffset: 32,
programWidth: 42,
programCount: 44,
sectionWidth: 46,
sectionCount: 48,
namesIndex: 50,
sectionHeaderSize: 40,
},
64: {
programOffset: 32,
sectionOffset: 40,
programWidth: 54,
programCount: 56,
sectionWidth: 58,
sectionCount: 60,
namesIndex: 62,
sectionHeaderSize: 64,
},
} as const;
/** Canonical table declarations, including extended counts in inactive section zero. */
export function* readElfTableDeclarations(
bytes: Buffer,
fileSize: number,
): Generator<ElfDeclarationRead, Result<ElfTableDeclarations, string>, Buffer> {
const fixed = readElfFixedHeader(bytes, fileSize);
if (!fixed.ok) return fixed;
const header = fixed.value;
const r = reader(bytes, header);
const fields = TABLE_FIELDS[header.elfClass];
const programOffset = r.word(fields.programOffset);
const sectionOffset = r.word(fields.sectionOffset);
const programWidth = r.u16(fields.programWidth);
const sectionWidth = r.u16(fields.sectionWidth);
let programCount = BigInt(r.u16(fields.programCount));
let sectionCount = BigInt(r.u16(fields.sectionCount));
let namesIndex = r.u16(fields.namesIndex);
if (
sectionOffset === 0n &&
(sectionCount !== 0n || namesIndex !== 0 || programCount === 0xffffn)
)
return err("ELF section table is missing");
if (
sectionOffset !== 0n &&
(sectionCount === 0n || namesIndex === 0xffff || programCount === 0xffffn)
) {
const failure = tableFailure(
"section",
{ offset: sectionOffset, count: 1n, width: sectionWidth },
header,
fileSize,
);
if (failure !== undefined) return err(failure);
const first = yield {
offset: Number(sectionOffset),
size: fields.sectionHeaderSize,
};
const counts = extendedCounts(first, header, {
programCount,
sectionCount,
namesIndex,
});
if (!counts.ok) return counts;
({ programCount, sectionCount, namesIndex } = counts.value);
}
for (const [kind, offset, count, width] of [
["program", programOffset, programCount, programWidth],
["section", sectionOffset, sectionCount, sectionWidth],
] as const) {
const failure = tableFailure(
kind,
{ offset, count, width },
header,
fileSize,
);
if (failure !== undefined) return err(failure);
}
if (sectionCount !== 0n && BigInt(namesIndex) >= sectionCount)
return err(
"ELF section-name index lies outside the declared section table",
);
// A validated table fits the host-sized file, so its record count is exact.
return ok({
header,
programs: {
offset: programOffset,
count: Number(programCount),
width: programWidth,
},
sections: {
offset: sectionOffset,
count: Number(sectionCount),
width: sectionWidth,
namesIndex,
},
});
}
/** Examine only header records; referenced payloads are range-checked, never copied. */
function* inspectTableRanges(
kind: "program" | "section",
table: ElfDeclaredTable,
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);
const size = count * table.width;
const bytes = yield {
offset: Number(table.offset) + index * table.width,
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 type = r.u32(at + (kind === "program" ? 0 : 4));
if (type === 0 || (kind === "section" && type === 8)) continue;
const offset = r.word(
at + (kind === "program" ? (wide ? 8 : 4) : wide ? 24 : 16),
);
const length = r.word(
at + (kind === "program" ? (wide ? 32 : 16) : wide ? 32 : 20),
);
const failure = rangeFailure(
`${kind} ${String(index + item)}`,
offset,
length,
fileSize,
);
if (failure !== undefined) return err(failure);
}
}
return ok(null);
}
/** Physical extent admission, independent of section names, CPU lanes and decoder budgets. */
export function* validateElfDeclaredRanges(
prefix: Buffer,
fileSize: number,
): Generator<ElfDeclarationRead, Result<null, string>, Buffer> {
const declarations = yield* readElfTableDeclarations(prefix, fileSize);
if (!declarations.ok) return declarations;
const { header, programs, sections } = declarations.value;
for (const [kind, table] of [
["program", programs],
["section", sections],
] as const) {
if (table.count === 0) continue;
const inspected = yield* inspectTableRanges(kind, table, header, fileSize);
if (!inspected.ok) return inspected;
}
return ok(null);
}
+15 -54
View File
@@ -1,4 +1,4 @@
import { readElfFixedHeader } from "../../domain/elfFixedHeader.js";
import { readElfTableDeclarations } from "../../domain/elfDeclaredRanges.js";
import {
BinaryFormatFailure,
BinaryReader,
@@ -78,63 +78,26 @@ export function* readElfSectionDirectory(
const prefix = yield request(fileSize, 0n, 16n);
const headerSize = prefix[4] === 1 ? 52 : 64;
const bytes = yield request(fileSize, 0n, BigInt(headerSize));
const parsed = readElfFixedHeader(bytes, fileSize);
const parsed = yield* readElfTableDeclarations(bytes, fileSize);
if (!parsed.ok) return reject(parsed.error);
const { elfClass: bits, byteOrder, machine } = parsed.value;
const { header, programs, sections: sectionTable } = parsed.value;
const { elfClass: bits, byteOrder, machine } = header;
const little = byteOrder === "little";
const wide = bits === 64;
const r = new BinaryReader(bytes, little);
const program_offset = r.word(wide ? 32 : 28, bits);
const sectionOffset = r.word(wide ? 40 : 32, bits);
const program_width = r.u16(wide ? 54 : 42);
const sectionWidth = r.u16(wide ? 58 : 46);
let program_count = r.u16(wide ? 56 : 44);
let count = r.u16(wide ? 60 : 48);
let namesIndex = r.u16(wide ? 62 : 50);
if (
(program_count !== 0 && program_offset < BigInt(headerSize)) ||
(sectionOffset !== 0n && sectionOffset < BigInt(headerSize))
)
reject("ELF header table overlaps the native header");
if (
sectionOffset === 0n &&
(count !== 0 || namesIndex !== 0 || program_count === 0xffff)
)
reject("ELF section table is missing");
if (
sectionOffset !== 0n &&
(count === 0 || namesIndex === 0xffff || program_count === 0xffff)
) {
if (sectionWidth < (wide ? 64 : 40))
reject("ELF extended section header width is invalid");
const first = new BinaryReader(
yield request(fileSize, sectionOffset, BigInt(sectionWidth)),
little,
);
if (first.u32(4) !== 0)
reject(
"ELF initial section for extended numbering must have SHT_NULL type",
);
if (count === 0) {
const declared = first.word(wide ? 32 : 20, bits);
if (declared < 0xff00n) reject("ELF extended section count is invalid");
count = exact(declared);
}
if (namesIndex === 0xffff) {
namesIndex = first.u32(wide ? 40 : 24);
if (namesIndex < 0xff00)
reject("ELF extended section name index is below its reserved range");
}
if (program_count === 0xffff) {
program_count = first.u32(wide ? 44 : 28);
if (program_count < 0xffff)
reject("ELF extended program count is below its reserved range");
}
}
const {
offset: program_offset,
width: program_width,
count: program_count,
} = programs;
const {
offset: sectionOffset,
width: sectionWidth,
count,
namesIndex,
} = sectionTable;
let has_dynamic_segment = false;
if (program_count !== 0) {
if (program_width < (wide ? 56 : 32))
reject("ELF program header width is invalid");
const programs = new BinaryReader(
yield request(
fileSize,
@@ -153,8 +116,6 @@ export function* readElfSectionDirectory(
const sections: NativeSection[] = [];
let section_names_limitation: string | undefined;
if (count !== 0) {
if (sectionWidth < (wide ? 64 : 40) || namesIndex >= count)
reject("ELF section width or name-table index is invalid");
const table = new BinaryReader(
yield request(
fileSize,
@@ -0,0 +1,159 @@
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
import { elf } from "../../../src/domain/binaryTarget.fixture.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const image = (bits: 32 | 64, little: boolean) => {
const bytes = Buffer.alloc(368);
const header = elf(bits === 32 ? 1 : 2, little ? 1 : 2, bits === 32 ? 3 : 62);
header.copy(bytes);
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const u16 = (offset: number, value: number) =>
view.setUint16(offset, value, little);
const u32 = (offset: number, value: number) =>
view.setUint32(offset, value, little);
const word = (offset: number, value: bigint) =>
bits === 32
? u32(offset, Number(value))
: view.setBigUint64(offset, value, little);
const wide = bits === 64;
const program = wide ? 64 : 52;
const programWidth = wide ? 56 : 32;
const sectionWidth = wide ? 64 : 40;
u16(16, 2);
word(wide ? 32 : 28, BigInt(program));
word(wide ? 40 : 32, 128n);
u16(wide ? 54 : 42, programWidth);
u16(wide ? 56 : 44, 1);
u16(wide ? 58 : 46, sectionWidth);
u16(wide ? 60 : 48, 2);
u32(program, 1); // PT_LOAD
word(program + (wide ? 8 : 4), 320n);
word(program + (wide ? 32 : 16), 16n);
word(program + (wide ? 40 : 20), 16n);
const section = 128 + sectionWidth;
u32(section + 4, 1); // SHT_PROGBITS
word(section + (wide ? 24 : 16), 352n);
word(section + (wide ? 32 : 20), 16n);
return {
bytes,
word,
u16,
u32,
program,
section,
programWidth,
sectionWidth,
};
};
const admit = async (bytes: Buffer) => {
const directory = await createTestTempDirectory("rea-elf-declared-ranges-");
const path = join(directory, "image.elf");
await writeFile(path, bytes);
const result = await parseBinaryTarget(path);
const digest = createHash("sha256").update(bytes).digest("hex");
expect(
createHash("sha256")
.update(await readFile(path))
.digest("hex"),
).toBe(digest);
if (result.ok) expect(result.value.sha256).toBe(digest);
return result;
};
for (const bits of [32, 64] as const) {
for (const little of [true, false]) {
it(`refuses declared table and payload truncation before provider admission (ELF${bits} ${little ? "LE" : "BE"})`, async () => {
const fixture = image(bits, little);
expect((await admit(fixture.bytes)).ok).toBe(true);
for (const [length, declaration] of [
[fixture.program, "program header table"],
[fixture.program + fixture.programWidth - 1, "program header table"],
[128 + fixture.sectionWidth * 2 - 1, "section header table"],
[335, "program 0"],
[367, "section 1"],
] as const) {
const result = await admit(fixture.bytes.subarray(0, length));
expect(result).toMatchObject({
ok: false,
error: {
constraint: "file_format_rejected",
reason: expect.stringContaining(`truncated ELF ${declaration}`),
},
});
if (result.ok) throw new Error("Expected a declared range refusal");
expect(result.error.reason).toContain(`${length}-byte file`);
}
});
it(`preserves absent tables and memory-only declarations (ELF${bits} ${little ? "LE" : "BE"})`, async () => {
expect(
(
await admit(
elf(bits === 32 ? 1 : 2, little ? 1 : 2, bits === 32 ? 3 : 62),
)
).ok,
).toBe(true);
const fixture = image(bits, little);
const wide = bits === 64;
const outside = wide ? 0xffffffffffffffffn : 0xffffffffn;
fixture.u32(fixture.section + 4, 8); // SHT_NOBITS owns memory, not file bytes.
fixture.word(fixture.section + (wide ? 24 : 16), outside);
fixture.word(fixture.section + (wide ? 32 : 20), outside);
fixture.word(fixture.program + (wide ? 8 : 4), outside);
fixture.word(fixture.program + (wide ? 32 : 16), 0n);
fixture.word(128 + (wide ? 24 : 16), outside); // Inactive section-zero fields.
fixture.word(128 + (wide ? 32 : 20), outside);
expect((await admit(fixture.bytes)).ok).toBe(true);
});
it(`keeps declared offsets exact when their sum exceeds host precision (ELF${bits} ${little ? "LE" : "BE"})`, async () => {
const fixture = image(bits, little);
const maximum = bits === 64 ? 0xffffffffffffffffn : 0xffffffffn;
fixture.word(fixture.program + (bits === 64 ? 8 : 4), maximum);
const result = await admit(fixture.bytes);
expect(result).toMatchObject({ ok: false });
if (result.ok) throw new Error("Expected an out-of-file range");
expect(result.error.reason).toContain(`${maximum}..${maximum + 16n}`);
});
}
}
it.each(["sections", "programs", "section names"] as const)(
"resolves valid extended %s numbering from bounded section zero",
async (kind) => {
const count =
kind === "programs" ? 0xffff : kind === "section names" ? 0xff01 : 0xff00;
const width = kind === "programs" ? 56 : 64;
const sectionAt = kind === "programs" ? 64 + count * width : 64;
const bytes = Buffer.alloc(
kind === "programs" ? sectionAt + 64 : sectionAt + count * width,
);
elf(2, 1, 62).copy(bytes);
bytes.writeBigUInt64LE(BigInt(sectionAt), 40);
bytes.writeUInt16LE(64, 58);
if (kind !== "programs") {
bytes.writeBigUInt64LE(BigInt(count), sectionAt + 32);
if (kind === "section names") {
bytes.writeUInt16LE(0xffff, 62);
bytes.writeUInt32LE(0xff00, sectionAt + 40);
bytes.writeUInt32LE(3, sectionAt + 0xff00 * width + 4);
}
} else {
bytes.writeBigUInt64LE(64n, 32);
bytes.writeUInt16LE(56, 54);
bytes.writeUInt16LE(0xffff, 56);
bytes.writeUInt16LE(1, 60);
bytes.writeUInt32LE(count, sectionAt + 44);
}
expect((await admit(bytes)).ok).toBe(true);
const truncated = await admit(bytes.subarray(0, bytes.length - 1));
expect(truncated).toMatchObject({ ok: false });
if (truncated.ok) throw new Error("Expected an extended table refusal");
expect(truncated.error.reason).toContain("truncated ELF");
},
);
@@ -52,10 +52,31 @@ const fixture = (little: boolean, count = 2): Buffer => {
}
return bytes;
};
const resolve = async (bytes: Buffer) => {
const writeFixture = async (bytes: Buffer) => {
const directory = await createTestTempDirectory("rea-mips-shared-test-");
const path = join(directory, "shared.elf");
await writeFile(path, bytes);
return path;
};
const inspectSource = async (path: string) => {
const handle = await open(path, "r");
try {
return await readMipsElfMetadata(
handle,
{
elfClass: 32,
byteOrder: "little",
type: 3,
flags: 0x74001005,
},
() => undefined,
);
} finally {
await handle.close();
}
};
const resolve = async (bytes: Buffer) => {
const path = await writeFixture(bytes);
const result = await parseBinaryTarget(path);
assert.ok(result.ok);
assert.equal(result.value.kind, "executable");
@@ -201,16 +222,23 @@ it("does not treat uncaptured ABI declaration headers as an inspected absence",
bytes.writeUInt32LE(bytes.length + 1, 32);
bytes.writeUInt16LE(40, 46);
bytes.writeUInt16LE(1, 48);
const target = await resolve(bytes);
assert.equal(target.mips.abiFlags, null);
assert.equal(target.mips.sharedLoad?.status, "unavailable");
const path = await writeFixture(bytes);
const admitted = await parseBinaryTarget(path);
assert.ok(!admitted.ok);
assert.match(admitted.error.reason, /truncated ELF section header table/u);
// The specialized inspector still retains unknown source facts if used on
// damaged evidence; normal target admission refuses its impossible extent.
const declarations = await inspectSource(path);
assert.ok(declarations.ok);
assert.equal(declarations.value.abiFlags, null);
assert.equal(declarations.value.sharedLoad?.status, "unavailable");
assert.match(
ghidraMipsUnsupportedReason(target) ?? "",
/unavailable headers remain unknown/u,
declarations.value.limitations.join(" "),
/extends beyond the captured file/u,
);
});
const malformed: [string, (bytes: Buffer) => void, RegExp][] = [
const malformed: [string, (bytes: Buffer) => void, RegExp, boolean?][] = [
[
"overlapping load ranges",
(b) => {
@@ -233,11 +261,13 @@ const malformed: [string, (bytes: Buffer) => void, RegExp][] = [
"declared dynamic tail outside file",
(b) => b.writeUInt32LE(b.length, 100),
/beyond the file/u,
true,
],
[
"unavailable load bytes",
(b) => b.writeUInt32LE(b.length + 1, 68),
/PT_LOAD range/u,
true,
],
["missing terminator", (b) => b.writeUInt32LE(1, 280), /DT_NULL/u],
[
@@ -268,9 +298,22 @@ const malformed: [string, (bytes: Buffer) => void, RegExp][] = [
];
it.each(malformed)(
"preserves useful partial header facts for %s",
async (_name, mutate, reason) => {
async (_name, mutate, reason, rejectsExtent) => {
const bytes = fixture(true);
mutate(bytes);
if (rejectsExtent) {
const path = await writeFixture(bytes);
const admitted = await parseBinaryTarget(path);
assert.ok(!admitted.ok);
assert.match(admitted.error.reason, /truncated ELF program \d/u);
const declarations = await inspectSource(path);
assert.ok(declarations.ok);
assert.equal(declarations.value.sharedLoad?.status, "unavailable");
if (declarations.value.sharedLoad?.status !== "unavailable")
throw new Error("Expected retained unknown relocation coverage");
assert.match(declarations.value.sharedLoad.reason, reason);
return;
}
const target = await resolve(bytes);
assert.equal(target.mips.flags, 0x74001005);
assert.equal(target.mips.abiFlags, null);