mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(elf): validate declared file extents before provider admission
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
| {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user