diff --git a/src/domain/apple/machoLoadCommands.ts b/src/domain/apple/machoLoadCommands.ts index b31dadeb67..f8889e32f2 100644 --- a/src/domain/apple/machoLoadCommands.ts +++ b/src/domain/apple/machoLoadCommands.ts @@ -10,8 +10,8 @@ export interface MachoLoadCommandTableHeader { readonly commandBytes: number; } -/** Read only the thin header's load-command framing; other formats return null. */ -export const readMachoLoadCommandTableHeader = ( +/** Decode thin-header declarations without admitting their table extent. */ +export const readMachoLoadCommandDeclarations = ( bytes: Uint8Array, fileSize: number, ): Result => { @@ -28,10 +28,6 @@ export const readMachoLoadCommandTableHeader = ( ); const littleEndian = magic === 0xcefaedfe || magic === 0xcffaedfe; const commandBytes = view.getUint32(20, littleEndian); - if (commandBytes > fileSize - headerSize) - return err( - `truncated Mach-O load commands: the header declares ${commandBytes} bytes after its ${headerSize}-byte header; the file has ${fileSize}`, - ); return ok({ headerSize, wide, @@ -41,6 +37,21 @@ export const readMachoLoadCommandTableHeader = ( }); }; +/** Admit the declared command table within the complete file's extent. */ +export const readMachoLoadCommandTableHeader = ( + bytes: Uint8Array, + fileSize: number, +): Result => { + const parsed = readMachoLoadCommandDeclarations(bytes, fileSize); + if (!parsed.ok || parsed.value === null) return parsed; + const { commandBytes, headerSize } = parsed.value; + if (commandBytes > fileSize - headerSize) + return err( + `truncated Mach-O load commands: the header declares ${commandBytes} bytes after its ${headerSize}-byte header; the file has ${fileSize}`, + ); + return parsed; +}; + /** One owner's count, alignment and extent proof for a sequential command walk. */ export class MachoLoadCommandCursor { #index = 0; diff --git a/src/native/metadata/MachoImage.ts b/src/native/metadata/MachoImage.ts index 62408057bf..9a41228db2 100644 --- a/src/native/metadata/MachoImage.ts +++ b/src/native/metadata/MachoImage.ts @@ -8,18 +8,21 @@ import { type LoadRegion, type ZeroFillMapping, } from "./BinaryContainer.js"; -import { MachoLoadCommandCursor } from "../../domain/apple/machoLoadCommands.js"; +import { + MachoLoadCommandCursor, + readMachoLoadCommandDeclarations, +} from "../../domain/apple/machoLoadCommands.js"; /** Read thin Mach-O images, retaining the slice's exact file-backed mappings. */ export const readMachoImage = (bytes: Buffer): BinaryContainer => { - const magic = bytes.readUInt32BE(0); - const little = magic === 0xcefaedfe || magic === 0xcffaedfe; - const bits = magic === 0xfeedfacf || magic === 0xcffaedfe ? 64 : 32; + const parsed = readMachoLoadCommandDeclarations(bytes, bytes.length); + if (!parsed.ok) throw new BinaryFormatFailure("malformed", parsed.error); + if (parsed.value === null) + throw new BinaryFormatFailure("malformed", "Unrecognized Mach-O header"); + const table = parsed.value; + const { headerSize, commandBytes, littleEndian: little } = table; + const bits = table.wide ? 64 : 32; const reader = new BinaryReader(bytes, little); - const headerSize = bits === 64 ? 32 : 28; - reader.range(0, headerSize, "Mach-O header"); - const count = reader.u32(16); - const commandBytes = reader.u32(20); const commandStart = reader.table( BigInt(headerSize), commandBytes, @@ -32,12 +35,7 @@ export const readMachoImage = (bytes: Buffer): BinaryContainer => { const zeroFills: ZeroFillMapping[] = []; const sections: ImageSection[] = []; let chainedFixups = false; - const commands = new MachoLoadCommandCursor({ - wide: bits === 64, - littleEndian: little, - commandCount: count, - commandBytes, - }); + const commands = new MachoLoadCommandCursor(table); while (commands.nextOffset !== null) { const offset = commandStart + commands.nextOffset; const frame = commands.read(bytes.subarray(offset, offset + 8)); diff --git a/tests/boundary/filesystem/machoCommandAdmission.test.ts b/tests/boundary/filesystem/machoCommandAdmission.test.ts index 9edd6d5320..64d0abe735 100644 --- a/tests/boundary/filesystem/machoCommandAdmission.test.ts +++ b/tests/boundary/filesystem/machoCommandAdmission.test.ts @@ -2,6 +2,9 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it } from "vitest"; +import { BinaryResourceFailure } from "../../../src/native/metadata/BinaryContainer.js"; +import { readMachoLoadCommandTableHeader } from "../../../src/domain/apple/machoLoadCommands.js"; +import { readBinaryContainer } from "../../../src/native/metadata/readBinaryContainer.js"; import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js"; import { thinMach } from "../../../src/domain/binaryTarget.fixture.js"; import { parseExecutableHeader } from "../../../src/domain/binaryTarget.js"; @@ -46,6 +49,7 @@ it.each([ { label: "undersized", field: 36, value: 4 }, { label: "unaligned", field: 36, value: 12 }, { label: "outside table", field: 36, value: 0xffffffff }, + { label: "truncated declared table", field: 20, value: 24 }, { label: "too many commands", field: 16, value: 2 }, { label: "unclaimed table bytes", field: 16, value: 0 }, { label: "trailing table bytes", field: 36, value: 8 }, @@ -60,6 +64,7 @@ it.each([ ); await writeFile(path, bytes); expect(parseExecutableHeader(bytes, "arm64").ok).toBe(false); + expect(() => readBinaryContainer(bytes)).toThrow(/load command/iu); const resolved = await parseBinaryTarget(path); expect(resolved.ok).toBe(false); if (resolved.ok || !(resolved.error instanceof BinaryTargetError)) @@ -86,6 +91,11 @@ it.each([ "image", ); await writeFile(path, bytes); + expect(readBinaryContainer(bytes)).toMatchObject({ + format: "macho", + bits: wide ? 64 : 32, + byte_order: littleEndian ? "little" : "big", + }); expect(await parseBinaryTarget(path)).toMatchObject({ ok: true, value: { format: "mach-o", architecture: wide ? "arm64" : "x86" }, @@ -150,3 +160,14 @@ it.each([null, 0, 1])( }); }, ); + +it("preserves native capacity priority when the command table also exceeds the file", () => { + const bytes = commandImage(true, true, [16]); + bytes.writeUInt32LE(16 * 1024 * 1024 + 1, 20); + expect(() => readBinaryContainer(bytes)).toThrow(BinaryResourceFailure); + const bounded = readMachoLoadCommandTableHeader(bytes, bytes.length); + expect(bounded).toMatchObject({ + ok: false, + error: expect.stringContaining("truncated Mach-O load commands"), + }); +});