mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
refactor(macho): centralize segment and section decoding
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { readMachoSegmentHeader } from "../../domain/apple/machoSegmentHeader.js";
|
||||
import { MachoLoadCommandCursor } from "../../domain/apple/machoLoadCommands.js";
|
||||
import {
|
||||
MAX_JVM_CLASS_IDENTITY_BYTES,
|
||||
@@ -504,20 +505,16 @@ const readSegmentSpan = (
|
||||
body: Uint8Array,
|
||||
wide: boolean,
|
||||
): SegmentSpan | undefined => {
|
||||
if (body.byteLength < (wide ? 72 : 56)) return undefined;
|
||||
const view = viewOf(body);
|
||||
const nameBytes = body.subarray(8, 24);
|
||||
const parsed = readMachoSegmentHeader(body, wide, true);
|
||||
if (!parsed.ok) return undefined;
|
||||
const { nameBytes, address: vmaddr, memorySize: vmsize } = parsed.value;
|
||||
const end = nameBytes.indexOf(0);
|
||||
return {
|
||||
name: new TextDecoder("utf-8", { fatal: false }).decode(
|
||||
end < 0 ? nameBytes : nameBytes.subarray(0, end),
|
||||
),
|
||||
vmaddr: wide
|
||||
? view.getBigUint64(24, true)
|
||||
: BigInt(view.getUint32(24, true)),
|
||||
vmsize: wide
|
||||
? view.getBigUint64(32, true)
|
||||
: BigInt(view.getUint32(28, true)),
|
||||
vmaddr,
|
||||
vmsize,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { readMachoSectionHeader } from "./machoSectionHeader.js";
|
||||
|
||||
const formats = [
|
||||
{ wide: false, littleEndian: false, headerSize: 68 },
|
||||
{ wide: false, littleEndian: true, headerSize: 68 },
|
||||
{ wide: true, littleEndian: false, headerSize: 80 },
|
||||
{ wide: true, littleEndian: true, headerSize: 80 },
|
||||
] as const;
|
||||
|
||||
it.each(formats)(
|
||||
"preserves raw section facts (wide=$wide, littleEndian=$littleEndian)",
|
||||
({ wide, littleEndian, headerSize }) => {
|
||||
const backing = Buffer.alloc(headerSize + 19, 0xa5);
|
||||
const bytes = backing.subarray(7, 7 + headerSize);
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const names = Buffer.from([
|
||||
0x5f, 0x80, 0xff, 0, 0xe9, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
|
||||
]);
|
||||
names.copy(bytes, 0);
|
||||
const segmentNames = Buffer.from("__DATA\0trailing!", "latin1");
|
||||
segmentNames.copy(bytes, 16);
|
||||
const address = wide ? 0xfedcba9876543210n : 0xfedcba98n;
|
||||
const size = wide ? 0xffffffffffffffffn : 0xffffffffn;
|
||||
if (wide) {
|
||||
view.setBigUint64(32, address, littleEndian);
|
||||
view.setBigUint64(40, size, littleEndian);
|
||||
} else {
|
||||
view.setUint32(32, Number(address), littleEndian);
|
||||
view.setUint32(36, Number(size), littleEndian);
|
||||
}
|
||||
view.setUint32(wide ? 48 : 40, 0xffffffff, littleEndian);
|
||||
view.setUint32(wide ? 64 : 56, 0x89abcdef, littleEndian);
|
||||
const expectedSegmentName = Buffer.from(bytes.subarray(16, 32));
|
||||
const before = Buffer.from(backing);
|
||||
const parsed = readMachoSectionHeader(
|
||||
backing.subarray(7),
|
||||
wide,
|
||||
littleEndian,
|
||||
);
|
||||
expect(backing.equals(before)).toBe(true);
|
||||
if (!parsed.ok) throw new Error(parsed.error.message);
|
||||
backing.fill(0);
|
||||
const { nameBytes, segmentNameBytes, ...facts } = parsed.value;
|
||||
expect(nameBytes.equals(names)).toBe(true);
|
||||
expect(segmentNameBytes.equals(expectedSegmentName)).toBe(true);
|
||||
expect(facts).toEqual({
|
||||
address,
|
||||
size,
|
||||
offset: 0xffffffff,
|
||||
flags: 0x89abcdef,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each(formats)(
|
||||
"refuses each truncated fixed section record (wide=$wide, littleEndian=$littleEndian)",
|
||||
({ wide, littleEndian, headerSize }) => {
|
||||
const backing = Buffer.alloc(headerSize + 7, 0xff);
|
||||
for (let length = 0; length < headerSize; length++)
|
||||
expect(
|
||||
readMachoSectionHeader(
|
||||
backing.subarray(7, 7 + length),
|
||||
wide,
|
||||
littleEndian,
|
||||
),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
error: {
|
||||
kind: "malformed",
|
||||
message: "Mach-O section header is truncated",
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,39 @@
|
||||
import { err, ok, type Result } from "../result.js";
|
||||
|
||||
export interface MachoSectionHeader {
|
||||
readonly nameBytes: Buffer;
|
||||
readonly segmentNameBytes: Buffer;
|
||||
readonly address: bigint;
|
||||
readonly size: bigint;
|
||||
readonly offset: number;
|
||||
readonly flags: number;
|
||||
}
|
||||
|
||||
/** Fixed section/section_64 facts; mapping, zero-fill and display policies belong to callers. */
|
||||
export const readMachoSectionHeader = (
|
||||
bytes: Uint8Array,
|
||||
wide: boolean,
|
||||
littleEndian: boolean,
|
||||
): Result<
|
||||
MachoSectionHeader,
|
||||
{ readonly kind: "malformed"; readonly message: string }
|
||||
> => {
|
||||
if (bytes.byteLength < (wide ? 80 : 68))
|
||||
return err({
|
||||
kind: "malformed",
|
||||
message: "Mach-O section header is truncated",
|
||||
});
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
return ok({
|
||||
nameBytes: Buffer.from(bytes.subarray(0, 16)),
|
||||
segmentNameBytes: Buffer.from(bytes.subarray(16, 32)),
|
||||
address: wide
|
||||
? view.getBigUint64(32, littleEndian)
|
||||
: BigInt(view.getUint32(32, littleEndian)),
|
||||
size: wide
|
||||
? view.getBigUint64(40, littleEndian)
|
||||
: BigInt(view.getUint32(36, littleEndian)),
|
||||
offset: view.getUint32(wide ? 48 : 40, littleEndian),
|
||||
flags: view.getUint32(wide ? 64 : 56, littleEndian),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { readMachoSegmentHeader } from "./machoSegmentHeader.js";
|
||||
|
||||
const formats = [
|
||||
{ wide: false, littleEndian: false, headerSize: 56 },
|
||||
{ wide: false, littleEndian: true, headerSize: 56 },
|
||||
{ wide: true, littleEndian: false, headerSize: 72 },
|
||||
{ wide: true, littleEndian: true, headerSize: 72 },
|
||||
] as const;
|
||||
|
||||
it.each(formats)(
|
||||
"retains exact raw segment facts (wide=$wide, littleEndian=$littleEndian)",
|
||||
({ wide, littleEndian, headerSize }) => {
|
||||
const backing = Buffer.alloc(headerSize + 19, 0xa5);
|
||||
const bytes = backing.subarray(7, 7 + headerSize);
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const names = Buffer.from([
|
||||
0x5f, 0x80, 0xff, 0, 0xe9, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
|
||||
]);
|
||||
names.copy(bytes, 8);
|
||||
const values = wide
|
||||
? [
|
||||
0xfedcba9876543210n,
|
||||
0x8000000000000001n,
|
||||
0xffffffffffffffffn,
|
||||
0x123456789abcdef0n,
|
||||
]
|
||||
: [0xfedcba98n, 0x80000001n, 0xffffffffn, 0x12345678n];
|
||||
values.forEach((value, index) => {
|
||||
if (wide) view.setBigUint64(24 + index * 8, value, littleEndian);
|
||||
else view.setUint32(24 + index * 4, Number(value), littleEndian);
|
||||
});
|
||||
const protections = wide ? 56 : 40;
|
||||
[0x80000001, 0xfedcba98, 0xffffffff].forEach((value, index) =>
|
||||
view.setUint32(protections + index * 4, value, littleEndian),
|
||||
);
|
||||
const before = Buffer.from(backing);
|
||||
// A borrowed window may include following command bytes; only fixed fields belong here.
|
||||
const parsed = readMachoSegmentHeader(
|
||||
backing.subarray(7),
|
||||
wide,
|
||||
littleEndian,
|
||||
);
|
||||
expect(backing.equals(before)).toBe(true);
|
||||
if (!parsed.ok) throw new Error(parsed.error.message);
|
||||
backing.fill(0);
|
||||
const { nameBytes, ...facts } = parsed.value;
|
||||
expect(nameBytes.equals(names)).toBe(true);
|
||||
expect(facts).toEqual({
|
||||
headerSize,
|
||||
address: values[0],
|
||||
memorySize: values[1],
|
||||
fileOffset: values[2],
|
||||
fileSize: values[3],
|
||||
maxProtection: 0x80000001,
|
||||
protection: 0xfedcba98,
|
||||
sectionCount: 0xffffffff,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each(formats)(
|
||||
"refuses every incomplete fixed segment record (wide=$wide, littleEndian=$littleEndian)",
|
||||
({ wide, littleEndian, headerSize }) => {
|
||||
const backing = Buffer.alloc(headerSize + 7, 0xff);
|
||||
for (let length = 0; length < headerSize; length++)
|
||||
expect(
|
||||
readMachoSegmentHeader(
|
||||
backing.subarray(7, 7 + length),
|
||||
wide,
|
||||
littleEndian,
|
||||
),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
error: {
|
||||
kind: "malformed",
|
||||
message: "Mach-O segment header is truncated",
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,31 @@
|
||||
import { err, ok } from "../result.js";
|
||||
|
||||
/** Decode a fixed LC_SEGMENT/LC_SEGMENT_64 record; callers own command and mapping admission. */
|
||||
export const readMachoSegmentHeader = (
|
||||
bytes: Uint8Array,
|
||||
wide: boolean,
|
||||
littleEndian: boolean,
|
||||
) => {
|
||||
const headerSize = wide ? 72 : 56;
|
||||
if (bytes.byteLength < headerSize)
|
||||
return err({
|
||||
kind: "malformed" as const,
|
||||
message: "Mach-O segment header is truncated",
|
||||
});
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const word = (offset: number): bigint =>
|
||||
wide
|
||||
? view.getBigUint64(offset, littleEndian)
|
||||
: BigInt(view.getUint32(offset, littleEndian));
|
||||
return ok({
|
||||
headerSize,
|
||||
nameBytes: Buffer.from(bytes.subarray(8, 24)),
|
||||
address: word(24),
|
||||
memorySize: word(wide ? 32 : 28),
|
||||
fileOffset: word(wide ? 40 : 32),
|
||||
fileSize: word(wide ? 48 : 36),
|
||||
maxProtection: view.getUint32(wide ? 56 : 40, littleEndian),
|
||||
protection: view.getUint32(wide ? 60 : 44, littleEndian),
|
||||
sectionCount: view.getUint32(wide ? 64 : 48, littleEndian),
|
||||
});
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
import { readMachoSectionHeader } from "../domain/apple/machoSectionHeader.js";
|
||||
import { readMachoSegmentHeader } from "../domain/apple/machoSegmentHeader.js";
|
||||
import {
|
||||
MachoLoadCommandCursor,
|
||||
readMachoLoadCommandDeclarations,
|
||||
@@ -302,33 +304,48 @@ const collectSegment = (
|
||||
{ cursor, size }: LoadCommand,
|
||||
state: LayoutState,
|
||||
): void => {
|
||||
if (size < 72) throw new RangeError("Truncated segment command");
|
||||
const address = bytes.readBigUInt64LE(cursor + 24),
|
||||
fileSize = bytes.readBigUInt64LE(cursor + 48),
|
||||
fileOffset = bytes.readBigUInt64LE(cursor + 40);
|
||||
const parsed = readMachoSegmentHeader(
|
||||
bytes.subarray(cursor, cursor + size),
|
||||
true,
|
||||
true,
|
||||
);
|
||||
if (!parsed.ok) throw new RangeError("Truncated segment command");
|
||||
const {
|
||||
address,
|
||||
fileSize,
|
||||
fileOffset,
|
||||
protection,
|
||||
sectionCount: count,
|
||||
headerSize,
|
||||
} = parsed.value;
|
||||
if (fileOffset + fileSize > BigInt(state.sliceEnd - state.slice))
|
||||
throw new RangeError("Segment file range exceeds target bytes");
|
||||
state.segments.push({
|
||||
address,
|
||||
size: fileSize,
|
||||
offset: state.slice + Number(fileOffset),
|
||||
executable: (bytes.readUInt32LE(cursor + 60) & 4) !== 0,
|
||||
executable: (protection & 4) !== 0,
|
||||
});
|
||||
if (fileOffset === 0n && fileSize > 0n) state.baseAddress ??= address;
|
||||
const count = bytes.readUInt32LE(cursor + 64);
|
||||
if (72 + count * 80 > size) throw new RangeError("Truncated section table");
|
||||
if (headerSize + count * 80 > size)
|
||||
throw new RangeError("Truncated section table");
|
||||
for (let section = 0; section < count; section++) {
|
||||
const position = cursor + 72 + section * 80;
|
||||
const sectionSize = bytes.readBigUInt64LE(position + 40);
|
||||
const position = cursor + headerSize + section * 80;
|
||||
const parsedSection = readMachoSectionHeader(
|
||||
bytes.subarray(position, position + 80),
|
||||
true,
|
||||
true,
|
||||
);
|
||||
if (!parsedSection.ok) throw new RangeError("Truncated section table");
|
||||
const { nameBytes, address, size: sectionSize } = parsedSection.value;
|
||||
if (sectionSize > BigInt(Number.MAX_SAFE_INTEGER))
|
||||
throw new RangeError("Section size exceeds exact numeric range");
|
||||
state.sections.push({
|
||||
name: bytes
|
||||
.subarray(position, position + 16)
|
||||
name: nameBytes
|
||||
// ASCII decoding masks high bits and can fabricate known metadata names.
|
||||
.toString("latin1")
|
||||
.replace(/\0.*$/u, ""),
|
||||
address: bytes.readBigUInt64LE(position + 32),
|
||||
address,
|
||||
size: Number(sectionSize),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
readMachoSectionHeader,
|
||||
type MachoSectionHeader,
|
||||
} from "../../domain/apple/machoSectionHeader.js";
|
||||
import { readMachoSegmentHeader } from "../../domain/apple/machoSegmentHeader.js";
|
||||
import {
|
||||
declaredFileRange,
|
||||
BinaryFormatFailure,
|
||||
@@ -49,20 +54,28 @@ export const readMachoImage = (bytes: Buffer): BinaryContainer => {
|
||||
"malformed",
|
||||
"Mach-O segment command does not match the header width",
|
||||
);
|
||||
const segmentHeader = wide ? 72 : 56;
|
||||
const sectionWidth = wide ? 80 : 68;
|
||||
if (size < segmentHeader)
|
||||
const parsedSegment = readMachoSegmentHeader(
|
||||
bytes.subarray(offset, offset + size),
|
||||
wide,
|
||||
little,
|
||||
);
|
||||
if (!parsedSegment.ok)
|
||||
throw new BinaryFormatFailure(
|
||||
"malformed",
|
||||
"Mach-O segment header is truncated",
|
||||
parsedSegment.error.kind,
|
||||
parsedSegment.error.message,
|
||||
);
|
||||
const address = reader.word(offset + 24, bits);
|
||||
const memorySize = reader.word(offset + (wide ? 32 : 28), bits);
|
||||
const fileOffset = reader.word(offset + (wide ? 40 : 32), bits);
|
||||
const fileSize = reader.word(offset + (wide ? 48 : 36), bits);
|
||||
const maxProtection = reader.u32(offset + (wide ? 56 : 40));
|
||||
const protection = reader.u32(offset + (wide ? 60 : 44));
|
||||
const sectionCount = reader.u32(offset + (wide ? 64 : 48));
|
||||
const {
|
||||
headerSize: segmentHeader,
|
||||
address,
|
||||
memorySize,
|
||||
fileOffset,
|
||||
fileSize,
|
||||
maxProtection,
|
||||
protection,
|
||||
sectionCount,
|
||||
nameBytes,
|
||||
} = parsedSegment.value;
|
||||
const unmapped = memorySize === 0n && protection === 0;
|
||||
if (
|
||||
(!unmapped && fileSize > memorySize) ||
|
||||
@@ -85,9 +98,7 @@ export const readMachoImage = (bytes: Buffer): BinaryContainer => {
|
||||
unmapped || fileRange === undefined
|
||||
? undefined
|
||||
: { ...fileRange, address };
|
||||
const name = fixedName(
|
||||
reader.range(offset + 8, 16, "Mach-O segment name"),
|
||||
);
|
||||
const name = fixedName(nameBytes);
|
||||
if (mapping !== undefined && name !== "__PAGEZERO") {
|
||||
mappings.push(mapping);
|
||||
loadRegions.push({
|
||||
@@ -110,18 +121,21 @@ export const readMachoImage = (bytes: Buffer): BinaryContainer => {
|
||||
});
|
||||
for (let sectionIndex = 0; sectionIndex < sectionCount; sectionIndex++) {
|
||||
const section = offset + segmentHeader + sectionIndex * sectionWidth;
|
||||
const sectionName = fixedName(
|
||||
reader.range(section, 16, "Mach-O section name"),
|
||||
const parsedSection = readMachoSectionHeader(
|
||||
bytes.subarray(section, section + sectionWidth),
|
||||
wide,
|
||||
little,
|
||||
);
|
||||
if (!parsedSection.ok)
|
||||
throw new BinaryFormatFailure(
|
||||
parsedSection.error.kind,
|
||||
parsedSection.error.message,
|
||||
);
|
||||
sections.push(
|
||||
machoImageSection({
|
||||
reader,
|
||||
bits,
|
||||
wide,
|
||||
header: section,
|
||||
name: `${fixedName(reader.range(section + 16, 16, "Mach-O section segment"))},${sectionName}`,
|
||||
fileBytes: bytes.length,
|
||||
section: parsedSection.value,
|
||||
protection,
|
||||
sectionName,
|
||||
segmentMapping: mapping ?? null,
|
||||
segmentUnmapped: unmapped,
|
||||
unmapped: unmapped || mapping === undefined,
|
||||
@@ -161,13 +175,9 @@ export const readMachoImage = (bytes: Buffer): BinaryContainer => {
|
||||
};
|
||||
|
||||
const machoImageSection = ({
|
||||
reader,
|
||||
bits,
|
||||
wide,
|
||||
header,
|
||||
name,
|
||||
fileBytes,
|
||||
section,
|
||||
protection,
|
||||
sectionName,
|
||||
segmentMapping,
|
||||
segmentUnmapped,
|
||||
unmapped,
|
||||
@@ -176,13 +186,9 @@ const machoImageSection = ({
|
||||
segmentFileOffset,
|
||||
segmentFileSize,
|
||||
}: {
|
||||
readonly reader: BinaryReader;
|
||||
readonly bits: 32 | 64;
|
||||
readonly wide: boolean;
|
||||
readonly header: number;
|
||||
readonly name: string;
|
||||
readonly fileBytes: number;
|
||||
readonly section: MachoSectionHeader;
|
||||
readonly protection: number;
|
||||
readonly sectionName: string;
|
||||
readonly segmentMapping: FileMapping | null;
|
||||
readonly segmentUnmapped: boolean;
|
||||
readonly unmapped: boolean;
|
||||
@@ -191,11 +197,11 @@ const machoImageSection = ({
|
||||
readonly segmentFileOffset: bigint;
|
||||
readonly segmentFileSize: bigint;
|
||||
}): ImageSection => {
|
||||
const flags = reader.u32(header + (wide ? 64 : 56));
|
||||
const { flags, address: sectionAddress, size: sectionSize } = section;
|
||||
const sectionOffset = BigInt(section.offset);
|
||||
const sectionName = fixedName(section.nameBytes);
|
||||
const name = `${fixedName(section.segmentNameBytes)},${sectionName}`;
|
||||
const zeroFill = [1, 0xc, 0x12].includes(flags & 0xff);
|
||||
const sectionAddress = reader.word(header + 32, bits);
|
||||
const sectionSize = reader.word(header + (wide ? 40 : 36), bits);
|
||||
const sectionOffset = BigInt(reader.u32(header + (wide ? 48 : 40)));
|
||||
const addressDelta = sectionAddress - segmentAddress;
|
||||
const fileDelta = sectionOffset - segmentFileOffset;
|
||||
const inSegmentMemory =
|
||||
@@ -208,7 +214,7 @@ const machoImageSection = ({
|
||||
sectionSize <= segmentFileSize - fileDelta;
|
||||
const range = unmapped
|
||||
? { offset: 0, size: 0, file_backed: false }
|
||||
: declaredFileRange(reader.bytes.length, sectionOffset, sectionSize);
|
||||
: declaredFileRange(fileBytes, sectionOffset, sectionSize);
|
||||
const fileBacked =
|
||||
range.file_backed && !zeroFill && inSegmentMemory && inSegmentFile;
|
||||
const readOnly =
|
||||
|
||||
Reference in New Issue
Block a user