mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 13:26:10 +00:00
* fix(native): recover macOS command deadlines Promote native_command_timeout_ms (default 60000), native_ui_deadline_ms (default 180000), and pwntools_timeout_ms (default 30000) as validated per-call settings. Keep cpuSeconds, byte allowances, LLDB grace, and plist probes fixed, and say those caps have no runtime override. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(native): report the deadline that actually expired Pass the selected native command timeout through xcrun resolution, thread the pwntools deadline into pwndbg failures, and state the selected UI deadline apart from the fixed wait-step cap. SIGXFSZ and EFBIG recovery names the file-size allowance. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(native): honor the selected deadline on helper stages The UI helper compile and capture subprocesses used fixed 60s and 30s exec timeouts, so a longer native_ui_deadline_ms still failed as a generic provider error. Preparation timeouts reported the 60000 ms default instead of the selected native_command_timeout_ms. Memory resource-constraint recovery names memory again, and the Darwin plist CLI expectation matches the byte-counted input allowance. Co-authored-by: morluto <morluto@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com>
337 lines
11 KiB
TypeScript
337 lines
11 KiB
TypeScript
import { readElfFixedHeader } from "../../domain/elfFixedHeader.js";
|
|
import {
|
|
BinaryFormatFailure,
|
|
BinaryReader,
|
|
BinaryResourceFailure,
|
|
declaredFileRange,
|
|
} from "./BinaryContainer.js";
|
|
|
|
/** A borrowed positional source. Drivers own admission, cancellation and file identity. */
|
|
export interface NativeReadRequest {
|
|
readonly offset: number;
|
|
readonly size: number;
|
|
}
|
|
export interface NativeSection {
|
|
readonly index: number;
|
|
readonly name: string | null;
|
|
readonly name_state: "known" | "unavailable" | "truncated";
|
|
readonly header_offset: number;
|
|
readonly offset: bigint;
|
|
readonly size: bigint;
|
|
readonly address: bigint;
|
|
readonly type: number;
|
|
readonly flags: bigint;
|
|
readonly link: number;
|
|
readonly info: number;
|
|
readonly entry_size: bigint;
|
|
readonly file_backed: boolean;
|
|
}
|
|
export interface NativeSectionDirectory {
|
|
readonly format: "elf" | "pe";
|
|
readonly bits: 32 | 64;
|
|
readonly little: boolean;
|
|
readonly machine: number;
|
|
readonly elf_os_abi: number | null;
|
|
readonly flags: number;
|
|
readonly program_offset: bigint;
|
|
readonly program_count: number;
|
|
readonly program_width: number;
|
|
readonly sections: readonly NativeSection[];
|
|
}
|
|
const STRUCTURE_BYTES = 16 * 1024 * 1024;
|
|
const reject = (message: string): never => {
|
|
throw new BinaryFormatFailure("malformed", message);
|
|
};
|
|
const exact = (value: bigint): number => {
|
|
if (value < 0n || value > BigInt(Number.MAX_SAFE_INTEGER))
|
|
reject("Native offset exceeds exact host precision");
|
|
return Number(value);
|
|
};
|
|
const request = (
|
|
fileSize: number,
|
|
offset: bigint,
|
|
size: bigint,
|
|
): NativeReadRequest => {
|
|
if (
|
|
offset < 0n ||
|
|
size < 0n ||
|
|
offset > BigInt(fileSize) ||
|
|
size > BigInt(fileSize) - offset
|
|
)
|
|
reject("Native directory range is truncated or outside the file");
|
|
if (size > BigInt(STRUCTURE_BYTES))
|
|
throw new BinaryResourceFailure(
|
|
"structure",
|
|
STRUCTURE_BYTES,
|
|
"Native directory exceeds the 16777216-byte (16 MiB) structural decoding budget. This release has no runtime override.",
|
|
);
|
|
return { offset: exact(offset), size: exact(size) };
|
|
};
|
|
|
|
/** One ELF interpretation for in-memory images and bounded descriptor-backed inspections. */
|
|
export function* readElfSectionDirectory(
|
|
fileSize: number,
|
|
): Generator<NativeReadRequest, NativeSectionDirectory, Buffer> {
|
|
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);
|
|
if (!parsed.ok) return reject(parsed.error);
|
|
const { elfClass: bits, byteOrder, machine } = parsed.value;
|
|
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");
|
|
}
|
|
}
|
|
if (program_count !== 0) {
|
|
if (program_width < (wide ? 56 : 32))
|
|
reject("ELF program header width is invalid");
|
|
request(
|
|
fileSize,
|
|
program_offset,
|
|
BigInt(program_count) * BigInt(program_width),
|
|
);
|
|
}
|
|
const sections: NativeSection[] = [];
|
|
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,
|
|
sectionOffset,
|
|
BigInt(count) * BigInt(sectionWidth),
|
|
),
|
|
little,
|
|
);
|
|
let names: Buffer | null = null;
|
|
if (namesIndex !== 0) {
|
|
const h = namesIndex * sectionWidth;
|
|
if (table.u32(h + 4) !== 3)
|
|
reject("ELF section name table is not a string table");
|
|
names = yield request(
|
|
fileSize,
|
|
table.word(h + (wide ? 24 : 16), bits),
|
|
table.word(h + (wide ? 32 : 20), bits),
|
|
);
|
|
}
|
|
const decodedNames = new Map<
|
|
number,
|
|
{ name: string | null; state: NativeSection["name_state"] }
|
|
>();
|
|
let nameBytes = 0;
|
|
for (let index = 0; index < count; index++) {
|
|
const h = index * sectionWidth;
|
|
const nameOffset = table.u32(h);
|
|
let name: string | null = null;
|
|
let name_state: NativeSection["name_state"] = "unavailable";
|
|
if (names !== null) {
|
|
if (nameOffset >= names.length)
|
|
reject("ELF section name offset is outside its string table");
|
|
let decoded = decodedNames.get(nameOffset);
|
|
if (decoded === undefined) {
|
|
const window = names.subarray(
|
|
nameOffset,
|
|
Math.min(nameOffset + 65536, names.length),
|
|
);
|
|
const end = window.indexOf(0);
|
|
nameBytes += end < 0 ? window.length : end + 1;
|
|
if (nameBytes > STRUCTURE_BYTES)
|
|
throw new BinaryResourceFailure(
|
|
"structure",
|
|
STRUCTURE_BYTES,
|
|
"Decoded ELF section names exceed the 16 MiB structural budget",
|
|
);
|
|
decoded = {
|
|
name: end < 0 ? null : window.toString("latin1", 0, end),
|
|
state: end < 0 ? "truncated" : "known",
|
|
};
|
|
decodedNames.set(nameOffset, decoded);
|
|
}
|
|
name_state = decoded.state;
|
|
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);
|
|
sections.push({
|
|
index,
|
|
name,
|
|
name_state,
|
|
header_offset: exact(sectionOffset) + h,
|
|
offset,
|
|
size,
|
|
address: table.word(h + (wide ? 16 : 12), bits),
|
|
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),
|
|
file_backed:
|
|
type !== 8 && declaredFileRange(fileSize, offset, size).file_backed,
|
|
});
|
|
}
|
|
}
|
|
return {
|
|
format: "elf",
|
|
elf_os_abi: parsed.value.osAbi,
|
|
bits,
|
|
little,
|
|
machine,
|
|
flags: r.u32(wide ? 48 : 36),
|
|
program_offset,
|
|
program_count,
|
|
program_width,
|
|
sections,
|
|
};
|
|
}
|
|
|
|
/** Shared COFF section fields; content extent excludes file-alignment padding. */
|
|
export const readPeSectionHeader = (bytes: Buffer) => {
|
|
const r = new BinaryReader(bytes, true);
|
|
const name = r.range(0, 8, "PE section name");
|
|
const nul = name.indexOf(0);
|
|
const virtual_size = r.u32(8);
|
|
const raw_size = r.u32(16);
|
|
return {
|
|
name: name.toString("latin1", 0, nul < 0 ? name.length : nul),
|
|
virtual_size,
|
|
virtual_address: r.u32(12),
|
|
raw_size,
|
|
offset: r.u32(20),
|
|
characteristics: r.u32(36),
|
|
size: virtual_size === 0 ? raw_size : Math.min(virtual_size, raw_size),
|
|
};
|
|
};
|
|
|
|
export function* readPeSectionDirectory(
|
|
fileSize: number,
|
|
): Generator<NativeReadRequest, NativeSectionDirectory, Buffer> {
|
|
const dos = new BinaryReader(yield request(fileSize, 0n, 64n), true);
|
|
if (dos.u16(0) !== 0x5a4d) reject("Missing DOS MZ header");
|
|
const pe = dos.u32(60);
|
|
if (pe < 64) reject("PE signature overlaps the DOS header");
|
|
const coff = new BinaryReader(yield request(fileSize, BigInt(pe), 24n), true);
|
|
if (coff.u32(0) !== 0x4550) reject("PE signature is invalid");
|
|
const optionalSize = coff.u16(20);
|
|
const optional = new BinaryReader(
|
|
yield request(fileSize, BigInt(pe + 24), BigInt(optionalSize)),
|
|
true,
|
|
);
|
|
const magic = optional.u16(0);
|
|
const bits = magic === 0x10b ? 32 : magic === 0x20b ? 64 : null;
|
|
if (bits === null)
|
|
throw new BinaryFormatFailure(
|
|
"unsupported",
|
|
"Only PE32/PE32+ images are supported",
|
|
);
|
|
const minimum = bits === 64 ? 112 : 96;
|
|
if (
|
|
optionalSize < minimum ||
|
|
optional.u32(minimum - 4) * 8 > optionalSize - minimum
|
|
)
|
|
reject("PE optional header or data directories are truncated");
|
|
const count = coff.u16(6);
|
|
const start = pe + 24 + optionalSize;
|
|
const table = yield request(fileSize, BigInt(start), BigInt(count) * 40n);
|
|
const sections: NativeSection[] = [];
|
|
for (let index = 0; index < count; index++) {
|
|
const row = readPeSectionHeader(
|
|
table.subarray(index * 40, (index + 1) * 40),
|
|
);
|
|
sections.push({
|
|
index,
|
|
name: row.name,
|
|
name_state: "known",
|
|
header_offset: start + index * 40,
|
|
offset: BigInt(row.offset),
|
|
size: BigInt(row.size),
|
|
address: BigInt(row.virtual_address),
|
|
type: 0,
|
|
flags: BigInt(row.characteristics),
|
|
link: 0,
|
|
info: 0,
|
|
entry_size: 0n,
|
|
file_backed: declaredFileRange(
|
|
fileSize,
|
|
BigInt(row.offset),
|
|
BigInt(row.size),
|
|
).file_backed,
|
|
});
|
|
}
|
|
return {
|
|
format: "pe",
|
|
elf_os_abi: null,
|
|
bits,
|
|
little: true,
|
|
machine: coff.u16(4),
|
|
flags: coff.u16(22),
|
|
program_offset: 0n,
|
|
program_count: 0,
|
|
program_width: 0,
|
|
sections,
|
|
};
|
|
}
|
|
|
|
/** Synchronous driver borrows bytes; file drivers satisfy the identical requests without loading payloads. */
|
|
export const readNativeDirectoryFromBuffer = (
|
|
bytes: Buffer,
|
|
format: "elf" | "pe",
|
|
): NativeSectionDirectory => {
|
|
const steps =
|
|
format === "elf"
|
|
? readElfSectionDirectory(bytes.length)
|
|
: readPeSectionDirectory(bytes.length);
|
|
let next = steps.next();
|
|
while (!next.done) {
|
|
const { offset, size } = next.value;
|
|
next = steps.next(bytes.subarray(offset, offset + size));
|
|
}
|
|
return next.value;
|
|
};
|