mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(macho): unify segment mapping admission
This commit is contained in:
1 parent
0a9d92a54b
commit
8d747c2acd
4 files changed
+126
-19
No files matched your search
@@ -29,3 +29,23 @@ export const readMachoSegmentHeader = (
|
||||
sectionCount: view.getUint32(wide ? 64 : 48, littleEndian),
|
||||
});
|
||||
};
|
||||
|
||||
/** Admit VM sizes once for consumers that construct segment mappings. */
|
||||
export const admitMachoSegmentMapping = (
|
||||
segment: {
|
||||
readonly address: bigint;
|
||||
readonly memorySize: bigint;
|
||||
readonly fileSize: bigint;
|
||||
readonly protection: number;
|
||||
},
|
||||
bits: 32 | 64,
|
||||
) => {
|
||||
const unmapped = segment.memorySize === 0n && segment.protection === 0;
|
||||
return (!unmapped && segment.fileSize > segment.memorySize) ||
|
||||
segment.address + segment.memorySize > 1n << BigInt(bits)
|
||||
? err({
|
||||
kind: "malformed" as const,
|
||||
message: "Mach-O segment addresses or sizes are invalid",
|
||||
})
|
||||
: ok({ unmapped });
|
||||
};
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
selectMachoSlice,
|
||||
UnsupportedMachoMetadataFormatError,
|
||||
} from "./AppleMachoSelection.js";
|
||||
import { parsePointerFixups } from "./AppleMachoFixups.js";
|
||||
import { decodeAppleDispatchMetadata } from "./AppleDispatchMetadata.js";
|
||||
|
||||
/** macOS 27 system binaries ship CPU_SUBTYPE_ARM64E_X1 (12) with the PAC ABI bit. */
|
||||
@@ -158,6 +159,77 @@ const mappingImage = (commands: readonly Buffer[]): Buffer => {
|
||||
};
|
||||
|
||||
describe("Mach-O metadata span mapping", () => {
|
||||
it.each([
|
||||
["file size larger than VM size", mappingSegment(0x1000n, 0, 128, 64)],
|
||||
["VM address overflow", mappingSegment((1n << 64n) - 1n, 0, 2)],
|
||||
])("rejects %s consistently across metadata consumers", (_name, segment) => {
|
||||
const bytes = mappingImage([segment]);
|
||||
expect(() => parseMachoLayout(bytes, "arm64")).toThrow(
|
||||
"Mach-O segment addresses or sizes are invalid",
|
||||
);
|
||||
expect(() => readMachoMetadataImage(bytes)).toThrow(
|
||||
"Mach-O segment addresses or sizes are invalid",
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["unmapped", "pagezero"])(
|
||||
"does not expose %s file bytes as metadata pointers",
|
||||
(kind) => {
|
||||
const segment = mappingSegment(0x1000n, 0, 128);
|
||||
if (kind === "unmapped") {
|
||||
segment.writeBigUInt64LE(0n, 32);
|
||||
segment.writeUInt32LE(0, 60);
|
||||
} else segment.write("__PAGEZERO", 8, "ascii");
|
||||
const bytes = mappingImage([segment]);
|
||||
const layout = parseMachoLayout(bytes, "arm64");
|
||||
// Dyld fixup segment ordinals retain the original command order.
|
||||
expect(layout.segments).toHaveLength(1);
|
||||
expect(layout.baseAddress).toBe(0n);
|
||||
expect(() => layout.offset(0x1000n)).toThrow(RangeError);
|
||||
expect(readMachoMetadataImage(bytes).mappings).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves dyld segment ordinals across PAGEZERO and unmapped segments", () => {
|
||||
const pagezero = mappingSegment(0n, 0, 0, 4096);
|
||||
pagezero.write("__PAGEZERO", 8, "ascii");
|
||||
const unmapped = mappingSegment(0x1000n, 0, 0, 0);
|
||||
unmapped.writeUInt32LE(0, 60);
|
||||
const data = mappingSegment(0x2000n, 256, 128);
|
||||
const opcodes = Buffer.from([
|
||||
0x40,
|
||||
...Buffer.from("_local\0"),
|
||||
0x72,
|
||||
8,
|
||||
0x90,
|
||||
0,
|
||||
]);
|
||||
const dyldInfo = Buffer.alloc(48);
|
||||
dyldInfo.writeUInt32LE(0x80000022, 0);
|
||||
dyldInfo.writeUInt32LE(48, 4);
|
||||
dyldInfo.writeUInt32LE(400, 16);
|
||||
dyldInfo.writeUInt32LE(opcodes.length, 20);
|
||||
const bytes = mappingImage([pagezero, unmapped, data, dyldInfo]);
|
||||
opcodes.copy(bytes, 400);
|
||||
const layout = parseMachoLayout(bytes, "arm64");
|
||||
expect(layout.offset(0x2008n, 8)).toBe(264);
|
||||
expect(() => layout.offset(8n, 8)).toThrow(RangeError);
|
||||
expect(() => layout.offset(0x1000n, 8)).toThrow(RangeError);
|
||||
const fixups = parsePointerFixups(
|
||||
bytes,
|
||||
layout.fixups,
|
||||
layout.segments,
|
||||
layout.baseAddress,
|
||||
);
|
||||
expect(fixups.issues).toEqual([]);
|
||||
expect(fixups.decode(0x2008n, 0n)).toMatchObject({
|
||||
kind: "bind",
|
||||
symbol: "_local",
|
||||
library_ordinal: 0,
|
||||
binding_source: "bind",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a conflicting alias covering only the end of a pointer", () => {
|
||||
const layout = parseMachoLayout(
|
||||
mappingImage([
|
||||
|
||||
@@ -3,7 +3,10 @@ import {
|
||||
readMachoDylibCommandFacts,
|
||||
} from "../domain/apple/machoDylibCommand.js";
|
||||
import { readMachoSectionHeader } from "../domain/apple/machoSectionHeader.js";
|
||||
import { readMachoSegmentHeader } from "../domain/apple/machoSegmentHeader.js";
|
||||
import {
|
||||
admitMachoSegmentMapping,
|
||||
readMachoSegmentHeader,
|
||||
} from "../domain/apple/machoSegmentHeader.js";
|
||||
import {
|
||||
MachoLoadCommandCursor,
|
||||
readMachoLoadCommandDeclarations,
|
||||
@@ -13,6 +16,7 @@ import {
|
||||
BinaryFormatFailure,
|
||||
createFileMappingIndex,
|
||||
type ZeroFillMapping,
|
||||
type FileMapping,
|
||||
} from "./metadata/BinaryContainer.js";
|
||||
|
||||
import type { FixupCommands } from "./AppleMachoFixups.js";
|
||||
@@ -202,6 +206,7 @@ export const parseMachoLayout = (
|
||||
slice,
|
||||
sliceEnd,
|
||||
segments: [],
|
||||
mappings: [],
|
||||
zeroFills: [],
|
||||
sections: [],
|
||||
dylibs: [],
|
||||
@@ -226,14 +231,7 @@ export const parseMachoLayout = (
|
||||
const complete = framing.finish();
|
||||
if (!complete.ok) throw new RangeError(complete.error);
|
||||
const { segments, sections, chained, binds, rebases, dylibs } = state;
|
||||
const mappings = createFileMappingIndex(
|
||||
segments.map((segment) => ({
|
||||
address: segment.address,
|
||||
offset: segment.offset,
|
||||
size: Number(segment.size),
|
||||
})),
|
||||
state.zeroFills,
|
||||
);
|
||||
const mappings = createFileMappingIndex(state.mappings, state.zeroFills);
|
||||
const offset = (address: bigint, size = 1): number => {
|
||||
try {
|
||||
return mappings.offset(address, size);
|
||||
@@ -275,6 +273,7 @@ interface LayoutState {
|
||||
readonly slice: number;
|
||||
readonly sliceEnd: number;
|
||||
readonly segments: Segment[];
|
||||
readonly mappings: FileMapping[];
|
||||
readonly zeroFills: ZeroFillMapping[];
|
||||
readonly sections: Section[];
|
||||
readonly dylibs: FixupCommands["dylibs"][number][];
|
||||
@@ -313,20 +312,35 @@ const collectSegment = (
|
||||
sectionCount: count,
|
||||
headerSize,
|
||||
} = parsed.value;
|
||||
const admitted = admitMachoSegmentMapping(parsed.value, 64);
|
||||
if (!admitted.ok) throw new RangeError(admitted.error.message);
|
||||
if (fileOffset + fileSize > BigInt(state.sliceEnd - state.slice))
|
||||
throw new RangeError("Segment file range exceeds target bytes");
|
||||
const name = decodeMachoFixedName(nameBytes);
|
||||
state.segments.push({
|
||||
address,
|
||||
size: fileSize,
|
||||
offset: state.slice + Number(fileOffset),
|
||||
executable: (protection & 4) !== 0,
|
||||
});
|
||||
if (memorySize > fileSize && decodeMachoFixedName(nameBytes) !== "__PAGEZERO")
|
||||
if (!admitted.value.unmapped && name !== "__PAGEZERO")
|
||||
state.mappings.push({
|
||||
address,
|
||||
size: Number(fileSize),
|
||||
offset: state.slice + Number(fileOffset),
|
||||
});
|
||||
if (memorySize > fileSize && name !== "__PAGEZERO")
|
||||
state.zeroFills.push({
|
||||
address: address + fileSize,
|
||||
size: memorySize - fileSize,
|
||||
});
|
||||
if (fileOffset === 0n && fileSize > 0n) state.baseAddress ??= address;
|
||||
if (
|
||||
!admitted.value.unmapped &&
|
||||
name !== "__PAGEZERO" &&
|
||||
fileOffset === 0n &&
|
||||
fileSize > 0n
|
||||
)
|
||||
state.baseAddress ??= address;
|
||||
if (headerSize + count * 80 > size)
|
||||
throw new RangeError("Truncated section table");
|
||||
for (let section = 0; section < count; section++) {
|
||||
|
||||
@@ -3,7 +3,10 @@ import {
|
||||
readMachoSectionHeader,
|
||||
type MachoSectionHeader,
|
||||
} from "../../domain/apple/machoSectionHeader.js";
|
||||
import { readMachoSegmentHeader } from "../../domain/apple/machoSegmentHeader.js";
|
||||
import {
|
||||
admitMachoSegmentMapping,
|
||||
readMachoSegmentHeader,
|
||||
} from "../../domain/apple/machoSegmentHeader.js";
|
||||
import {
|
||||
declaredFileRange,
|
||||
BinaryFormatFailure,
|
||||
@@ -77,15 +80,13 @@ export const readMachoImage = (bytes: Buffer): BinaryContainer => {
|
||||
sectionCount,
|
||||
nameBytes,
|
||||
} = parsedSegment.value;
|
||||
const unmapped = memorySize === 0n && protection === 0;
|
||||
if (
|
||||
(!unmapped && fileSize > memorySize) ||
|
||||
address + memorySize > 1n << BigInt(bits)
|
||||
)
|
||||
const admitted = admitMachoSegmentMapping(parsedSegment.value, bits);
|
||||
if (!admitted.ok)
|
||||
throw new BinaryFormatFailure(
|
||||
"malformed",
|
||||
"Mach-O segment addresses or sizes are invalid",
|
||||
admitted.error.kind,
|
||||
admitted.error.message,
|
||||
);
|
||||
const { unmapped } = admitted.value;
|
||||
if (sectionCount > Math.floor((size - segmentHeader) / sectionWidth))
|
||||
throw new BinaryFormatFailure(
|
||||
"malformed",
|
||||
|
||||
Reference in new issue
Block a user