fix(elf): validate declared file extents before provider admission
This commit is contained in:
1 parent
aa4572828f
commit
651afa4455
6 files changed
+573
-67
No files matched your search
@@ -968,6 +968,19 @@ dossiers, instructions, recovered data types, measured load mappings, loaded
|
|||||||
memory bytes, and observed file offsets. Independent load-image attestation
|
memory bytes, and observed file offsets. Independent load-image attestation
|
||||||
supports DOS MZ, explicitly selected COM, and explicit raw-binary imports;
|
supports DOS MZ, explicitly selected COM, and explicit raw-binary imports;
|
||||||
PE returns measurements with an unsupported attestation status.
|
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
|
Target admission preserves recognized PE/NE/LE/LX signatures separately from
|
||||||
reader layout support. A Windows header overlapping the first 64 DOS-header
|
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
|
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 type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||||
import { err, ok, type Result } from "../domain/result.js";
|
import { err, ok, type Result } from "../domain/result.js";
|
||||||
import { readMipsElfMetadata } from "./MipsElfMetadata.js";
|
import { readMipsElfMetadata } from "./MipsElfMetadata.js";
|
||||||
|
import { validateElfDeclaredRanges } from "../domain/elfDeclaredRanges.js";
|
||||||
import {
|
import {
|
||||||
resolveAppBundleExecutable,
|
resolveAppBundleExecutable,
|
||||||
type ResolvedAppBundle,
|
type ResolvedAppBundle,
|
||||||
@@ -852,6 +853,12 @@ const readExecutableMetadata = async (
|
|||||||
signatureEvidence,
|
signatureEvidence,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (bytes.length >= 4 && bytes.toString("hex", 0, 4) === "7f454c46")
|
||||||
|
return readElfMetadata(handle, bytes, {
|
||||||
|
fileSize,
|
||||||
|
hostArchitecture,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
const thin = await readThinMachoMetadata(handle, bytes, {
|
const thin = await readThinMachoMetadata(handle, bytes, {
|
||||||
fileSize,
|
fileSize,
|
||||||
hostArchitecture,
|
hostArchitecture,
|
||||||
@@ -881,11 +888,25 @@ const readExecutableMetadata = async (
|
|||||||
if (!framed.ok) return framed;
|
if (!framed.ok) return framed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
return parsed;
|
||||||
!parsed.ok ||
|
};
|
||||||
parsed.value.architecture !== "mips" ||
|
|
||||||
parsed.value.mips.elfClass !== 32
|
/** 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;
|
return parsed;
|
||||||
const declarations = await readMipsElfMetadata(
|
const declarations = await readMipsElfMetadata(
|
||||||
handle,
|
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 =
|
type ExecutableMetadataFailure =
|
||||||
| string
|
| 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 {
|
import {
|
||||||
BinaryFormatFailure,
|
BinaryFormatFailure,
|
||||||
BinaryReader,
|
BinaryReader,
|
||||||
@@ -78,63 +78,26 @@ export function* readElfSectionDirectory(
|
|||||||
const prefix = yield request(fileSize, 0n, 16n);
|
const prefix = yield request(fileSize, 0n, 16n);
|
||||||
const headerSize = prefix[4] === 1 ? 52 : 64;
|
const headerSize = prefix[4] === 1 ? 52 : 64;
|
||||||
const bytes = yield request(fileSize, 0n, BigInt(headerSize));
|
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);
|
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 little = byteOrder === "little";
|
||||||
const wide = bits === 64;
|
const wide = bits === 64;
|
||||||
const r = new BinaryReader(bytes, little);
|
const r = new BinaryReader(bytes, little);
|
||||||
const program_offset = r.word(wide ? 32 : 28, bits);
|
const {
|
||||||
const sectionOffset = r.word(wide ? 40 : 32, bits);
|
offset: program_offset,
|
||||||
const program_width = r.u16(wide ? 54 : 42);
|
width: program_width,
|
||||||
const sectionWidth = r.u16(wide ? 58 : 46);
|
count: program_count,
|
||||||
let program_count = r.u16(wide ? 56 : 44);
|
} = programs;
|
||||||
let count = r.u16(wide ? 60 : 48);
|
const {
|
||||||
let namesIndex = r.u16(wide ? 62 : 50);
|
offset: sectionOffset,
|
||||||
if (
|
width: sectionWidth,
|
||||||
(program_count !== 0 && program_offset < BigInt(headerSize)) ||
|
count,
|
||||||
(sectionOffset !== 0n && sectionOffset < BigInt(headerSize))
|
namesIndex,
|
||||||
)
|
} = sectionTable;
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let has_dynamic_segment = false;
|
let has_dynamic_segment = false;
|
||||||
if (program_count !== 0) {
|
if (program_count !== 0) {
|
||||||
if (program_width < (wide ? 56 : 32))
|
|
||||||
reject("ELF program header width is invalid");
|
|
||||||
const programs = new BinaryReader(
|
const programs = new BinaryReader(
|
||||||
yield request(
|
yield request(
|
||||||
fileSize,
|
fileSize,
|
||||||
@@ -153,8 +116,6 @@ export function* readElfSectionDirectory(
|
|||||||
const sections: NativeSection[] = [];
|
const sections: NativeSection[] = [];
|
||||||
let section_names_limitation: string | undefined;
|
let section_names_limitation: string | undefined;
|
||||||
if (count !== 0) {
|
if (count !== 0) {
|
||||||
if (sectionWidth < (wide ? 64 : 40) || namesIndex >= count)
|
|
||||||
reject("ELF section width or name-table index is invalid");
|
|
||||||
const table = new BinaryReader(
|
const table = new BinaryReader(
|
||||||
yield request(
|
yield request(
|
||||||
fileSize,
|
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;
|
return bytes;
|
||||||
};
|
};
|
||||||
const resolve = async (bytes: Buffer) => {
|
const writeFixture = async (bytes: Buffer) => {
|
||||||
const directory = await createTestTempDirectory("rea-mips-shared-test-");
|
const directory = await createTestTempDirectory("rea-mips-shared-test-");
|
||||||
const path = join(directory, "shared.elf");
|
const path = join(directory, "shared.elf");
|
||||||
await writeFile(path, bytes);
|
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);
|
const result = await parseBinaryTarget(path);
|
||||||
assert.ok(result.ok);
|
assert.ok(result.ok);
|
||||||
assert.equal(result.value.kind, "executable");
|
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.writeUInt32LE(bytes.length + 1, 32);
|
||||||
bytes.writeUInt16LE(40, 46);
|
bytes.writeUInt16LE(40, 46);
|
||||||
bytes.writeUInt16LE(1, 48);
|
bytes.writeUInt16LE(1, 48);
|
||||||
const target = await resolve(bytes);
|
const path = await writeFixture(bytes);
|
||||||
assert.equal(target.mips.abiFlags, null);
|
const admitted = await parseBinaryTarget(path);
|
||||||
assert.equal(target.mips.sharedLoad?.status, "unavailable");
|
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(
|
assert.match(
|
||||||
ghidraMipsUnsupportedReason(target) ?? "",
|
declarations.value.limitations.join(" "),
|
||||||
/unavailable headers remain unknown/u,
|
/extends beyond the captured file/u,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const malformed: [string, (bytes: Buffer) => void, RegExp][] = [
|
const malformed: [string, (bytes: Buffer) => void, RegExp, boolean?][] = [
|
||||||
[
|
[
|
||||||
"overlapping load ranges",
|
"overlapping load ranges",
|
||||||
(b) => {
|
(b) => {
|
||||||
@@ -233,11 +261,13 @@ const malformed: [string, (bytes: Buffer) => void, RegExp][] = [
|
|||||||
"declared dynamic tail outside file",
|
"declared dynamic tail outside file",
|
||||||
(b) => b.writeUInt32LE(b.length, 100),
|
(b) => b.writeUInt32LE(b.length, 100),
|
||||||
/beyond the file/u,
|
/beyond the file/u,
|
||||||
|
true,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"unavailable load bytes",
|
"unavailable load bytes",
|
||||||
(b) => b.writeUInt32LE(b.length + 1, 68),
|
(b) => b.writeUInt32LE(b.length + 1, 68),
|
||||||
/PT_LOAD range/u,
|
/PT_LOAD range/u,
|
||||||
|
true,
|
||||||
],
|
],
|
||||||
["missing terminator", (b) => b.writeUInt32LE(1, 280), /DT_NULL/u],
|
["missing terminator", (b) => b.writeUInt32LE(1, 280), /DT_NULL/u],
|
||||||
[
|
[
|
||||||
@@ -268,9 +298,22 @@ const malformed: [string, (bytes: Buffer) => void, RegExp][] = [
|
|||||||
];
|
];
|
||||||
it.each(malformed)(
|
it.each(malformed)(
|
||||||
"preserves useful partial header facts for %s",
|
"preserves useful partial header facts for %s",
|
||||||
async (_name, mutate, reason) => {
|
async (_name, mutate, reason, rejectsExtent) => {
|
||||||
const bytes = fixture(true);
|
const bytes = fixture(true);
|
||||||
mutate(bytes);
|
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);
|
const target = await resolve(bytes);
|
||||||
assert.equal(target.mips.flags, 0x74001005);
|
assert.equal(target.mips.flags, 0x74001005);
|
||||||
assert.equal(target.mips.abiFlags, null);
|
assert.equal(target.mips.abiFlags, null);
|
||||||
|
|||||||
Reference in new issue
Block a user