mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 09:52:27 +00:00
* fix(native): retain Mach-O file identity through completion * fix(native): distinguish malformed Mach-O encryption facts * fix(native): preserve Mach-O command and dylib coverage * fix(native): preserve Apple dispatch fixup and declaration facts --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
729 lines
26 KiB
TypeScript
729 lines
26 KiB
TypeScript
import {
|
|
MachoLoadCommandCursor,
|
|
decodeMachoFixedWordFields,
|
|
machoFixedWordFieldNames,
|
|
readMachoLoadCommandTableHeader,
|
|
} from "../domain/apple/machoLoadCommands.js";
|
|
import { readMachoSegmentHeader } from "../domain/apple/machoSegmentHeader.js";
|
|
import { readMachoSectionHeader } from "../domain/apple/machoSectionHeader.js";
|
|
import { createHash } from "node:crypto";
|
|
|
|
const DYLIB_COMMANDS = new Set([
|
|
0xc, 0xd, 0x18, 0x80000018, 0x1f, 0x8000001f, 0x20, 0x23, 0x80000023,
|
|
]);
|
|
const LC_STR_COMMANDS = new Set([
|
|
...DYLIB_COMMANDS,
|
|
0x6,
|
|
0x7,
|
|
0x9,
|
|
0x10,
|
|
0xe,
|
|
0xf,
|
|
0x12,
|
|
0x13,
|
|
0x14,
|
|
0x15,
|
|
0x8000001c,
|
|
0x27,
|
|
0x39,
|
|
]);
|
|
|
|
/** Read exact ranges from an already admitted and identity-verified slice. */
|
|
export type MachoRangeReader = (
|
|
offset: number,
|
|
size: number,
|
|
) => Promise<Uint8Array>;
|
|
|
|
/** Invalid on-disk Mach-O framing or payload at the binary producer boundary. */
|
|
export class MachoBinaryFormatError extends Error {}
|
|
|
|
/** Producer-derived output capacity exceeded while retaining binary strings. */
|
|
export class MachoCommandNamesLimitError extends Error {
|
|
constructor(
|
|
readonly maximum: number,
|
|
readonly unit: "bytes" | "records" | "projected_bytes",
|
|
) {
|
|
super(
|
|
`Mach-O load-command ${unit === "projected_bytes" ? "projected bytes" : unit} exceed the retained output budget`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Reserve fixed projection keys, scalar metadata, arrays and bounded limitation
|
|
// messages even when the input has no load commands.
|
|
const PROJECTED_ENVELOPE_BYTES = 8192;
|
|
// A section becomes both a segment-view object and a set of indexed command
|
|
// fields. Reserve for their keys, numbers, identities and JSON framing before
|
|
// retaining the section. Strings are charged separately for escaping and hex.
|
|
const PROJECTED_COMMAND_BYTES = 2048;
|
|
const PROJECTED_SECTION_BYTES = 2048;
|
|
const PROJECTED_CHILD_BYTES = 256;
|
|
const PROJECTED_STRING_EXPANSION = 24;
|
|
const DEFAULT_PROJECTED_BYTES = 64 * 1024 * 1024;
|
|
|
|
export interface MachoCommandNames {
|
|
readonly sliceSize: number;
|
|
readonly header: {
|
|
readonly wide: boolean;
|
|
readonly littleEndian: boolean;
|
|
readonly cpuType: number;
|
|
readonly cpuSubtype: number;
|
|
readonly fileType: number;
|
|
readonly flags: number;
|
|
};
|
|
readonly commands: readonly {
|
|
readonly index: number;
|
|
readonly command: number;
|
|
readonly offset: number;
|
|
readonly size: number;
|
|
/** Every byte after the command header belongs to a decoded field. */
|
|
readonly fullyDecoded?: boolean;
|
|
/** Whole-command digest when it fits the retained output allowance. */
|
|
readonly rawSha256: string | null;
|
|
readonly fixedFields?: Readonly<Record<string, number>>;
|
|
readonly segmentName?: Uint8Array;
|
|
readonly segment?: {
|
|
readonly address: bigint;
|
|
readonly memorySize: bigint;
|
|
readonly fileOffset: bigint;
|
|
readonly fileSize: bigint;
|
|
readonly maxProtection: number;
|
|
readonly protection: number;
|
|
readonly flags: number;
|
|
};
|
|
readonly sections?: readonly {
|
|
readonly name: Uint8Array;
|
|
readonly segmentName: Uint8Array;
|
|
readonly address: bigint;
|
|
readonly size: bigint;
|
|
readonly fileOffset: number;
|
|
readonly alignmentExponent: number;
|
|
readonly relocationOffset: number;
|
|
readonly relocationCount: number;
|
|
readonly reserved1: number;
|
|
readonly reserved2: number;
|
|
readonly reserved3?: number;
|
|
readonly flags: number;
|
|
}[];
|
|
readonly commandString?: Uint8Array;
|
|
readonly commandStringOffset?: number;
|
|
readonly dylib?: {
|
|
readonly nameOffset: number;
|
|
readonly currentVersion: number;
|
|
readonly compatibilityVersion: number;
|
|
readonly encoding:
|
|
| { readonly kind: "timestamp"; readonly timestamp: number }
|
|
| { readonly kind: "use"; readonly flags: number };
|
|
};
|
|
readonly legacyBuild?: {
|
|
readonly version: number;
|
|
readonly sdk: number;
|
|
};
|
|
readonly linkerOptions?: readonly Uint8Array[];
|
|
readonly note?: {
|
|
readonly owner: Uint8Array;
|
|
readonly offset: bigint;
|
|
readonly size: bigint;
|
|
};
|
|
readonly fileset?: {
|
|
readonly address: bigint;
|
|
readonly fileOffset: bigint;
|
|
readonly entryId: Uint8Array;
|
|
readonly reserved: number;
|
|
};
|
|
readonly prebound?: {
|
|
readonly moduleCount: number;
|
|
readonly linkedModulesOffset: number;
|
|
readonly linkedModules: Uint8Array;
|
|
};
|
|
readonly fvmlib?: {
|
|
readonly minorVersion: number;
|
|
readonly headerAddress: number;
|
|
};
|
|
readonly fvmfile?: { readonly headerAddress: number };
|
|
readonly uuid?: Uint8Array;
|
|
readonly entrypoint?: {
|
|
readonly offset: bigint;
|
|
readonly stackSize: bigint;
|
|
};
|
|
readonly build?: {
|
|
readonly platform: number;
|
|
readonly minimumOs: number;
|
|
readonly sdk: number;
|
|
readonly tools: readonly {
|
|
readonly tool: number;
|
|
readonly version: number;
|
|
}[];
|
|
};
|
|
}[];
|
|
}
|
|
|
|
/**
|
|
* Walk only fixed records and declared strings from a selected thin image.
|
|
* The caller owns identity, cancellation, closing, and the per-string byte cap.
|
|
*/
|
|
export const readMachoCommandNames = async (
|
|
read: MachoRangeReader,
|
|
imageSize: number,
|
|
maximumStringBytes: number,
|
|
maximumRecords: number,
|
|
maximumProjectedBytes = DEFAULT_PROJECTED_BYTES,
|
|
): Promise<MachoCommandNames> => {
|
|
if (
|
|
!Number.isSafeInteger(maximumStringBytes) ||
|
|
maximumStringBytes < 0 ||
|
|
!Number.isSafeInteger(maximumRecords) ||
|
|
maximumRecords < 0 ||
|
|
!Number.isSafeInteger(maximumProjectedBytes) ||
|
|
maximumProjectedBytes < 0
|
|
)
|
|
throw new TypeError("Mach-O read limits must be nonnegative safe integers");
|
|
const exact = async (offset: number, size: number): Promise<Uint8Array> => {
|
|
if (
|
|
!Number.isSafeInteger(offset) ||
|
|
!Number.isSafeInteger(size) ||
|
|
offset < 0 ||
|
|
size < 0 ||
|
|
offset > imageSize - size
|
|
)
|
|
throw new MachoBinaryFormatError("Mach-O range exceeds selected slice");
|
|
const value = await read(offset, size);
|
|
if (value.byteLength !== size)
|
|
throw new MachoBinaryFormatError("Mach-O range is truncated");
|
|
return value;
|
|
};
|
|
const header = await exact(0, Math.min(32, imageSize));
|
|
const parsed = readMachoLoadCommandTableHeader(header, imageSize);
|
|
if (!parsed.ok) throw new MachoBinaryFormatError(parsed.error);
|
|
if (parsed.value === null)
|
|
throw new MachoBinaryFormatError("Selected slice is not Mach-O");
|
|
const table = parsed.value;
|
|
const cursor = new MachoLoadCommandCursor(table);
|
|
if (table.commandCount > maximumRecords)
|
|
throw new MachoCommandNamesLimitError(maximumRecords, "records");
|
|
let projectedBytes = 0;
|
|
const chargeProjection = (count: number, perItem: number): void => {
|
|
if (count > Math.floor((maximumProjectedBytes - projectedBytes) / perItem))
|
|
throw new MachoCommandNamesLimitError(
|
|
maximumProjectedBytes,
|
|
"projected_bytes",
|
|
);
|
|
projectedBytes += count * perItem;
|
|
};
|
|
chargeProjection(1, PROJECTED_ENVELOPE_BYTES);
|
|
chargeProjection(table.commandCount, PROJECTED_COMMAND_BYTES);
|
|
let records = table.commandCount;
|
|
let retainedStringBytes = 0;
|
|
const stringAllowance = (): number =>
|
|
Math.min(
|
|
maximumStringBytes - retainedStringBytes,
|
|
Math.floor(
|
|
(maximumProjectedBytes - projectedBytes) / PROJECTED_STRING_EXPANSION,
|
|
),
|
|
);
|
|
const admitStringBytes = (count: number): void => {
|
|
if (count > maximumStringBytes - retainedStringBytes)
|
|
throw new MachoCommandNamesLimitError(maximumStringBytes, "bytes");
|
|
if (count > stringAllowance())
|
|
throw new MachoCommandNamesLimitError(
|
|
maximumProjectedBytes,
|
|
"projected_bytes",
|
|
);
|
|
};
|
|
const retain = (bytes: Uint8Array): Uint8Array => {
|
|
admitStringBytes(bytes.byteLength);
|
|
chargeProjection(bytes.byteLength, PROJECTED_STRING_EXPANSION);
|
|
retainedStringBytes += bytes.byteLength;
|
|
// Range readers may return Buffer views into a much larger read window.
|
|
// Charge the retained length before allocating an independently owned copy.
|
|
return Uint8Array.from(bytes);
|
|
};
|
|
const readString = async (
|
|
offset: number,
|
|
extent: number,
|
|
label: string,
|
|
): Promise<Uint8Array> => {
|
|
const length = Math.min(extent, stringAllowance() + 1);
|
|
const bytes = await exact(offset, length);
|
|
const end = bytes.indexOf(0);
|
|
if (end < 0) {
|
|
if (length < extent) admitStringBytes(length);
|
|
throw new MachoBinaryFormatError(`${label} is unterminated`);
|
|
}
|
|
return retain(bytes.subarray(0, end));
|
|
};
|
|
const commands: MachoCommandNames["commands"][number][] = [];
|
|
while (cursor.nextOffset !== null) {
|
|
const row = cursor.read(
|
|
await exact(table.headerSize + cursor.nextOffset, 8),
|
|
);
|
|
if (!row.ok) throw new MachoBinaryFormatError(row.error);
|
|
const frame = row.value;
|
|
const at = table.headerSize + frame.offset;
|
|
const fixedNames = machoFixedWordFieldNames(frame.command);
|
|
let fixedFields: Readonly<Record<string, number>> | undefined;
|
|
if (fixedNames !== undefined) {
|
|
const byteLength = fixedNames.length * 4;
|
|
const decoded =
|
|
frame.size < 8 + byteLength
|
|
? null
|
|
: decodeMachoFixedWordFields(
|
|
frame.command,
|
|
await exact(at + 8, byteLength),
|
|
table.littleEndian,
|
|
);
|
|
if (decoded === null)
|
|
throw new MachoBinaryFormatError(
|
|
"Mach-O fixed command fields are truncated",
|
|
);
|
|
fixedFields = decoded;
|
|
}
|
|
const segmentWide =
|
|
frame.command === 0x19 ? true : frame.command === 1 ? false : null;
|
|
let segmentName: Uint8Array | undefined;
|
|
let segmentFacts: MachoCommandNames["commands"][number]["segment"];
|
|
let sections:
|
|
| NonNullable<MachoCommandNames["commands"][number]["sections"]>[number][]
|
|
| undefined;
|
|
if (segmentWide !== null) {
|
|
const segmentHeaderSize = segmentWide ? 72 : 56;
|
|
if (frame.size < segmentHeaderSize)
|
|
throw new MachoBinaryFormatError("Mach-O segment is truncated");
|
|
const segmentBytes = await exact(at, segmentHeaderSize);
|
|
const segment = readMachoSegmentHeader(
|
|
segmentBytes,
|
|
segmentWide,
|
|
table.littleEndian,
|
|
);
|
|
if (!segment.ok) throw new MachoBinaryFormatError(segment.error.message);
|
|
const width = segmentWide ? 80 : 68;
|
|
if (
|
|
segment.value.sectionCount >
|
|
Math.floor((frame.size - segmentHeaderSize) / width)
|
|
)
|
|
throw new MachoBinaryFormatError("Mach-O section table is truncated");
|
|
if (segment.value.sectionCount > maximumRecords - records)
|
|
throw new MachoCommandNamesLimitError(maximumRecords, "records");
|
|
chargeProjection(segment.value.sectionCount, PROJECTED_SECTION_BYTES);
|
|
records += segment.value.sectionCount;
|
|
segmentName = retain(segment.value.nameBytes);
|
|
const segmentView = new DataView(
|
|
segmentBytes.buffer,
|
|
segmentBytes.byteOffset,
|
|
segmentBytes.byteLength,
|
|
);
|
|
segmentFacts = {
|
|
address: segment.value.address,
|
|
memorySize: segment.value.memorySize,
|
|
fileOffset: segment.value.fileOffset,
|
|
fileSize: segment.value.fileSize,
|
|
maxProtection: segment.value.maxProtection,
|
|
protection: segment.value.protection,
|
|
flags: segmentView.getUint32(segmentWide ? 68 : 52, table.littleEndian),
|
|
};
|
|
sections = [];
|
|
for (let i = 0; i < segment.value.sectionCount; i++) {
|
|
const sectionBytes = await exact(
|
|
at + segmentHeaderSize + i * width,
|
|
width,
|
|
);
|
|
const section = readMachoSectionHeader(
|
|
sectionBytes,
|
|
segmentWide,
|
|
table.littleEndian,
|
|
);
|
|
if (!section.ok)
|
|
throw new MachoBinaryFormatError(section.error.message);
|
|
const sectionView = new DataView(
|
|
sectionBytes.buffer,
|
|
sectionBytes.byteOffset,
|
|
sectionBytes.byteLength,
|
|
);
|
|
sections.push({
|
|
name: retain(section.value.nameBytes),
|
|
segmentName: retain(section.value.segmentNameBytes),
|
|
address: section.value.address,
|
|
size: section.value.size,
|
|
fileOffset: section.value.offset,
|
|
alignmentExponent: sectionView.getUint32(
|
|
segmentWide ? 52 : 44,
|
|
table.littleEndian,
|
|
),
|
|
relocationOffset: sectionView.getUint32(
|
|
segmentWide ? 56 : 48,
|
|
table.littleEndian,
|
|
),
|
|
relocationCount: sectionView.getUint32(
|
|
segmentWide ? 60 : 52,
|
|
table.littleEndian,
|
|
),
|
|
reserved1: sectionView.getUint32(
|
|
segmentWide ? 68 : 60,
|
|
table.littleEndian,
|
|
),
|
|
reserved2: sectionView.getUint32(
|
|
segmentWide ? 72 : 64,
|
|
table.littleEndian,
|
|
),
|
|
...(segmentWide
|
|
? { reserved3: sectionView.getUint32(76, table.littleEndian) }
|
|
: {}),
|
|
flags: section.value.flags,
|
|
});
|
|
}
|
|
}
|
|
// Dylib and rpath commands own an lc_str at offset 8. Retain bytes up to
|
|
// the first NUL without allocating the command's full declared extent.
|
|
let commandString: Uint8Array | undefined;
|
|
let commandStringOffset: number | undefined;
|
|
let dylib: MachoCommandNames["commands"][number]["dylib"];
|
|
if (LC_STR_COMMANDS.has(frame.command)) {
|
|
if (frame.size < 12)
|
|
throw new MachoBinaryFormatError(
|
|
"Mach-O command string offset is truncated",
|
|
);
|
|
const data = await exact(at + 8, 4);
|
|
const offset = new DataView(
|
|
data.buffer,
|
|
data.byteOffset,
|
|
data.byteLength,
|
|
).getUint32(0, table.littleEndian);
|
|
const minimum = DYLIB_COMMANDS.has(frame.command)
|
|
? 24
|
|
: frame.command === 0x6 ||
|
|
frame.command === 0x7 ||
|
|
frame.command === 0x10
|
|
? 20
|
|
: frame.command === 0x9
|
|
? 16
|
|
: 12;
|
|
if (offset < minimum || offset >= frame.size)
|
|
throw new MachoBinaryFormatError(
|
|
"Mach-O command string offset is invalid",
|
|
);
|
|
commandString = await readString(
|
|
at + offset,
|
|
frame.size - offset,
|
|
"Mach-O command string",
|
|
);
|
|
commandStringOffset = offset;
|
|
if (DYLIB_COMMANDS.has(frame.command)) {
|
|
const fixed = await exact(at + 12, 12);
|
|
const view = new DataView(
|
|
fixed.buffer,
|
|
fixed.byteOffset,
|
|
fixed.byteLength,
|
|
);
|
|
const marker = view.getUint32(0, table.littleEndian);
|
|
const useEncoding =
|
|
(frame.command === 0xc || frame.command === 0x80000018) &&
|
|
offset === 28 &&
|
|
marker === 0x1a741800;
|
|
if (useEncoding && frame.size < 28)
|
|
throw new MachoBinaryFormatError(
|
|
"Mach-O dylib use command is truncated",
|
|
);
|
|
const flags = useEncoding ? await exact(at + 24, 4) : undefined;
|
|
dylib = {
|
|
nameOffset: offset,
|
|
currentVersion: view.getUint32(4, table.littleEndian),
|
|
compatibilityVersion: view.getUint32(8, table.littleEndian),
|
|
encoding:
|
|
flags === undefined
|
|
? { kind: "timestamp", timestamp: marker }
|
|
: {
|
|
kind: "use",
|
|
flags: new DataView(
|
|
flags.buffer,
|
|
flags.byteOffset,
|
|
flags.byteLength,
|
|
).getUint32(0, table.littleEndian),
|
|
},
|
|
};
|
|
}
|
|
}
|
|
let prebound: MachoCommandNames["commands"][number]["prebound"];
|
|
if (frame.command === 0x10) {
|
|
if (frame.size < 20)
|
|
throw new MachoBinaryFormatError(
|
|
"Mach-O prebound dylib command is truncated",
|
|
);
|
|
const data = await exact(at + 12, 8);
|
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
const moduleCount = view.getUint32(0, table.littleEndian);
|
|
const linkedModulesOffset = view.getUint32(4, table.littleEndian);
|
|
const length = Math.ceil(moduleCount / 8);
|
|
if (linkedModulesOffset < 20 || linkedModulesOffset > frame.size - length)
|
|
throw new MachoBinaryFormatError(
|
|
"Mach-O prebound module bitmap exceeds its command",
|
|
);
|
|
admitStringBytes(length);
|
|
prebound = {
|
|
moduleCount,
|
|
linkedModulesOffset,
|
|
linkedModules: retain(await exact(at + linkedModulesOffset, length)),
|
|
};
|
|
}
|
|
let fvmlib: MachoCommandNames["commands"][number]["fvmlib"];
|
|
if (frame.command === 0x6 || frame.command === 0x7) {
|
|
if (frame.size < 20)
|
|
throw new MachoBinaryFormatError("Mach-O fvmlib command is truncated");
|
|
const data = await exact(at + 12, 8);
|
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
fvmlib = {
|
|
minorVersion: view.getUint32(0, table.littleEndian),
|
|
headerAddress: view.getUint32(4, table.littleEndian),
|
|
};
|
|
}
|
|
let fvmfile: MachoCommandNames["commands"][number]["fvmfile"];
|
|
if (frame.command === 0x9) {
|
|
if (frame.size < 16)
|
|
throw new MachoBinaryFormatError("Mach-O fvmfile command is truncated");
|
|
const data = await exact(at + 12, 4);
|
|
fvmfile = {
|
|
headerAddress: new DataView(
|
|
data.buffer,
|
|
data.byteOffset,
|
|
data.byteLength,
|
|
).getUint32(0, table.littleEndian),
|
|
};
|
|
}
|
|
let linkerOptions: Uint8Array[] | undefined;
|
|
if (frame.command === 0x2d) {
|
|
if (frame.size < 12)
|
|
throw new MachoBinaryFormatError(
|
|
"Mach-O linker option count is truncated",
|
|
);
|
|
const countBytes = await exact(at + 8, 4);
|
|
const count = new DataView(
|
|
countBytes.buffer,
|
|
countBytes.byteOffset,
|
|
countBytes.byteLength,
|
|
).getUint32(0, table.littleEndian);
|
|
if (count > maximumRecords - records)
|
|
throw new MachoCommandNamesLimitError(maximumRecords, "records");
|
|
if (count > frame.size - 12)
|
|
throw new MachoBinaryFormatError(
|
|
"Mach-O linker option table is truncated",
|
|
);
|
|
chargeProjection(count, PROJECTED_CHILD_BYTES);
|
|
records += count;
|
|
linkerOptions = [];
|
|
let offset = 12;
|
|
let current = new Uint8Array(0);
|
|
let currentLength = 0;
|
|
while (linkerOptions.length < count && offset < frame.size) {
|
|
const length = Math.min(frame.size - offset, 4096);
|
|
const bytes = await exact(at + offset, length);
|
|
offset += length;
|
|
for (const byte of bytes) {
|
|
if (byte === 0) {
|
|
linkerOptions.push(retain(current.subarray(0, currentLength)));
|
|
current = new Uint8Array(0);
|
|
currentLength = 0;
|
|
if (linkerOptions.length === count) break;
|
|
} else {
|
|
const remaining = stringAllowance();
|
|
if (currentLength >= remaining) admitStringBytes(currentLength + 1);
|
|
if (currentLength === current.byteLength) {
|
|
const capacity = Math.min(
|
|
remaining,
|
|
Math.max(32, current.byteLength * 2),
|
|
);
|
|
const expanded = new Uint8Array(capacity);
|
|
expanded.set(current);
|
|
current = expanded;
|
|
}
|
|
current[currentLength++] = byte;
|
|
}
|
|
}
|
|
}
|
|
if (linkerOptions.length !== count)
|
|
throw new MachoBinaryFormatError(
|
|
"Mach-O linker option is unterminated",
|
|
);
|
|
}
|
|
let legacyBuild: MachoCommandNames["commands"][number]["legacyBuild"];
|
|
if ([0x24, 0x25, 0x2f, 0x30].includes(frame.command)) {
|
|
if (frame.size < 16)
|
|
throw new MachoBinaryFormatError(
|
|
"Mach-O minimum version command is truncated",
|
|
);
|
|
const bytes = await exact(at + 8, 8);
|
|
const view = new DataView(
|
|
bytes.buffer,
|
|
bytes.byteOffset,
|
|
bytes.byteLength,
|
|
);
|
|
legacyBuild = {
|
|
version: view.getUint32(0, table.littleEndian),
|
|
sdk: view.getUint32(4, table.littleEndian),
|
|
};
|
|
}
|
|
let note: MachoCommandNames["commands"][number]["note"];
|
|
if (frame.command === 0x31) {
|
|
if (frame.size < 40)
|
|
throw new MachoBinaryFormatError("Mach-O note command is truncated");
|
|
const data = await exact(at + 8, 32);
|
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
note = {
|
|
owner: retain(data.subarray(0, 16)),
|
|
offset: view.getBigUint64(16, table.littleEndian),
|
|
size: view.getBigUint64(24, table.littleEndian),
|
|
};
|
|
}
|
|
let fileset: MachoCommandNames["commands"][number]["fileset"];
|
|
if (frame.command === 0x80000035) {
|
|
if (frame.size < 32)
|
|
throw new MachoBinaryFormatError("Mach-O fileset command is truncated");
|
|
const data = await exact(at + 8, 24);
|
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
const offset = view.getUint32(16, table.littleEndian);
|
|
if (offset < 32 || offset >= frame.size)
|
|
throw new MachoBinaryFormatError(
|
|
"Mach-O fileset entry string offset is invalid",
|
|
);
|
|
fileset = {
|
|
address: view.getBigUint64(0, table.littleEndian),
|
|
fileOffset: view.getBigUint64(8, table.littleEndian),
|
|
entryId: await readString(
|
|
at + offset,
|
|
frame.size - offset,
|
|
"Mach-O fileset entry string",
|
|
),
|
|
reserved: view.getUint32(20, table.littleEndian),
|
|
};
|
|
}
|
|
let uuid: Uint8Array | undefined;
|
|
if (frame.command === 0x1b) {
|
|
if (frame.size < 24)
|
|
throw new MachoBinaryFormatError("Mach-O UUID command is truncated");
|
|
uuid = Uint8Array.from(await exact(at + 8, 16));
|
|
}
|
|
let entrypoint: MachoCommandNames["commands"][number]["entrypoint"];
|
|
if (frame.command === 0x80000028) {
|
|
if (frame.size < 24)
|
|
throw new MachoBinaryFormatError(
|
|
"Mach-O entrypoint command is truncated",
|
|
);
|
|
const bytes = await exact(at + 8, 16);
|
|
const view = new DataView(
|
|
bytes.buffer,
|
|
bytes.byteOffset,
|
|
bytes.byteLength,
|
|
);
|
|
entrypoint = {
|
|
offset: view.getBigUint64(0, table.littleEndian),
|
|
stackSize: view.getBigUint64(8, table.littleEndian),
|
|
};
|
|
}
|
|
let build: MachoCommandNames["commands"][number]["build"];
|
|
if (frame.command === 0x32) {
|
|
if (frame.size < 24)
|
|
throw new MachoBinaryFormatError(
|
|
"Mach-O build version command is truncated",
|
|
);
|
|
const header = await exact(at + 8, 16);
|
|
const view = new DataView(
|
|
header.buffer,
|
|
header.byteOffset,
|
|
header.byteLength,
|
|
);
|
|
const toolCount = view.getUint32(12, table.littleEndian);
|
|
if (toolCount > Math.floor((frame.size - 24) / 8))
|
|
throw new MachoBinaryFormatError(
|
|
"Mach-O build tool table is truncated",
|
|
);
|
|
if (toolCount > maximumRecords - records)
|
|
throw new MachoCommandNamesLimitError(maximumRecords, "records");
|
|
chargeProjection(toolCount, PROJECTED_CHILD_BYTES);
|
|
records += toolCount;
|
|
const tools: { tool: number; version: number }[] = [];
|
|
for (let i = 0; i < toolCount; i++) {
|
|
const tool = await exact(at + 24 + i * 8, 8);
|
|
const toolView = new DataView(
|
|
tool.buffer,
|
|
tool.byteOffset,
|
|
tool.byteLength,
|
|
);
|
|
tools.push({
|
|
tool: toolView.getUint32(0, table.littleEndian),
|
|
version: toolView.getUint32(4, table.littleEndian),
|
|
});
|
|
}
|
|
build = {
|
|
platform: view.getUint32(0, table.littleEndian),
|
|
minimumOs: view.getUint32(4, table.littleEndian),
|
|
sdk: view.getUint32(8, table.littleEndian),
|
|
tools,
|
|
};
|
|
}
|
|
const fullyDecoded =
|
|
(fixedNames !== undefined && frame.size === 8 + fixedNames.length * 4) ||
|
|
(uuid !== undefined && frame.size === 24) ||
|
|
(entrypoint !== undefined && frame.size === 24) ||
|
|
(legacyBuild !== undefined && frame.size === 16) ||
|
|
(segmentWide !== null &&
|
|
sections !== undefined &&
|
|
frame.size ===
|
|
(segmentWide ? 72 : 56) +
|
|
sections.length * (segmentWide ? 80 : 68)) ||
|
|
(build !== undefined && frame.size === 24 + build.tools.length * 8);
|
|
let rawSha256: string | null = null;
|
|
if (!fullyDecoded && frame.size <= maximumStringBytes) {
|
|
const digest = createHash("sha256");
|
|
for (let position = 0; position < frame.size;) {
|
|
const length = Math.min(64 * 1024, frame.size - position);
|
|
digest.update(await exact(at + position, length));
|
|
position += length;
|
|
}
|
|
rawSha256 = digest.digest("hex");
|
|
}
|
|
commands.push({
|
|
index: frame.index,
|
|
command: frame.command,
|
|
offset: frame.offset,
|
|
size: frame.size,
|
|
fullyDecoded,
|
|
rawSha256,
|
|
...(fixedFields === undefined ? {} : { fixedFields }),
|
|
...(segmentName === undefined ||
|
|
sections === undefined ||
|
|
segmentFacts === undefined
|
|
? {}
|
|
: { segmentName, segment: segmentFacts, sections }),
|
|
...(commandString === undefined ? {} : { commandString }),
|
|
...(commandStringOffset === undefined ? {} : { commandStringOffset }),
|
|
...(dylib === undefined ? {} : { dylib }),
|
|
...(legacyBuild === undefined ? {} : { legacyBuild }),
|
|
...(linkerOptions === undefined ? {} : { linkerOptions }),
|
|
...(note === undefined ? {} : { note }),
|
|
...(fileset === undefined ? {} : { fileset }),
|
|
...(prebound === undefined ? {} : { prebound }),
|
|
...(fvmlib === undefined ? {} : { fvmlib }),
|
|
...(fvmfile === undefined ? {} : { fvmfile }),
|
|
...(uuid === undefined ? {} : { uuid }),
|
|
...(entrypoint === undefined ? {} : { entrypoint }),
|
|
...(build === undefined ? {} : { build }),
|
|
});
|
|
}
|
|
const end = cursor.finish();
|
|
if (!end.ok) throw new MachoBinaryFormatError(end.error);
|
|
return {
|
|
sliceSize: imageSize,
|
|
header: {
|
|
wide: table.wide,
|
|
littleEndian: table.littleEndian,
|
|
cpuType: table.identity.cpuType,
|
|
cpuSubtype: table.identity.cpuSubtype,
|
|
fileType: table.fileType,
|
|
flags: new DataView(
|
|
header.buffer,
|
|
header.byteOffset,
|
|
header.byteLength,
|
|
).getUint32(24, table.littleEndian),
|
|
},
|
|
commands,
|
|
};
|
|
};
|