diff --git a/src/artifacts/apple/MachoLoadCommandReader.test.ts b/src/artifacts/apple/MachoLoadCommandReader.test.ts index f2f3f5fdb3..e9f0980be4 100644 --- a/src/artifacts/apple/MachoLoadCommandReader.test.ts +++ b/src/artifacts/apple/MachoLoadCommandReader.test.ts @@ -1866,3 +1866,21 @@ it.each([true, false])( }); }, ); + +it("observes only the common 28-byte header of a 64-bit slice", async () => { + const bytes = machoImage({ fileType: FILE_TYPE.dylib }).subarray(0, 28); + const readAt = vi.fn(readerOf(bytes)); + expect(await readMachoImage(readAt, 32)).toMatchObject({ + status: "parsed", + slices: [{ file_type: "dylib", file_type_code: FILE_TYPE.dylib }], + }); + expect(readAt.mock.calls).toEqual([ + [0, 8], + [0, 28], + [32, 0], + ]); + expect(await readMachoImage(readerOf(bytes), 28)).toEqual({ + status: "malformed", + reason: "load commands extend beyond the Mach-O slice", + }); +}); diff --git a/src/artifacts/apple/MachoLoadCommandReader.ts b/src/artifacts/apple/MachoLoadCommandReader.ts index aaba78de55..5b2d935e89 100644 --- a/src/artifacts/apple/MachoLoadCommandReader.ts +++ b/src/artifacts/apple/MachoLoadCommandReader.ts @@ -1,5 +1,8 @@ import { readMachoSegmentHeader } from "../../domain/apple/machoSegmentHeader.js"; -import { MachoLoadCommandCursor } from "../../domain/apple/machoLoadCommands.js"; +import { + MachoLoadCommandCursor, + readMachoHeaderDeclarations, +} from "../../domain/apple/machoLoadCommands.js"; import { MAX_JVM_CLASS_IDENTITY_BYTES, JVM_CLASS_IDENTITY_LIMIT_REASON, @@ -21,7 +24,6 @@ import { classifyFatContainerIdentity, readFatContainerHeader, readFatSliceDeclarations, - readMachoHeaderPrefixFacts, validateFatSlice, validateFatSliceIdentity, type FatSliceDeclaration, @@ -312,24 +314,13 @@ const readSlice = async ( readonly alignmentExponent: number; } | null = null, ): Promise => { - const header = viewOf(await readExact(readAt, base, 28, "Mach-O header")); - const headerBytes = new Uint8Array( - header.buffer, - header.byteOffset, - header.byteLength, - ); - const identity = readMachoHeaderPrefixFacts(headerBytes).identity; - const magic = header.getUint32(0, true); - if (magic === MH_MAGIC_64 || magic === MH_MAGIC) { - if (identity === null) - throw new MachoFormatIssue( - "malformed", - `slice at offset ${base} has no Mach-O header`, - ); - const wide = magic === MH_MAGIC_64; - const headerSize = wide ? 32 : 28; - const commandCount = header.getUint32(16, true); - const commandBytes = header.getUint32(20, true); + const header = await readExact(readAt, base, 28, "Mach-O header"); + const parsed = readMachoHeaderDeclarations(header, length); + if (!parsed.ok) + throw new MachoFormatIssue("malformed", "Mach-O header is truncated"); + if (parsed.value !== null && parsed.value.littleEndian) { + const { wide, headerSize, commandCount, commandBytes, fileType, identity } = + parsed.value; if (commandBytes > MAX_LOAD_COMMAND_BYTES) throw new MachoFormatIssue( "unsupported", @@ -350,7 +341,7 @@ const readSlice = async ( wide, cpuType: identity.cpuType, cpuSubtype: identity.cpuSubtype, - fileType: header.getUint32(12, true), + fileType, commandCount, sliceOffset: base, sliceSize: length, @@ -373,10 +364,10 @@ const readSlice = async ( return slice; } throw new MachoFormatIssue( - magic === swap(MH_MAGIC) || magic === swap(MH_MAGIC_64) + parsed.value !== null && !parsed.value.littleEndian ? "unsupported" : "malformed", - magic === swap(MH_MAGIC) || magic === swap(MH_MAGIC_64) + parsed.value !== null && !parsed.value.littleEndian ? "big-endian Mach-O slices are not supported" : `slice at offset ${base} has no Mach-O header`, ); @@ -1021,12 +1012,5 @@ const fileTypeName = (fileType: number): MachoSlice["file_type"] => const version = (value: number): string => `${value >>> 16}.${(value >>> 8) & 0xff}.${value & 0xff}`; -const swap = (value: number): number => - (((value & 0xff) << 24) | - ((value & 0xff00) << 8) | - ((value >>> 8) & 0xff00) | - (value >>> 24)) >>> - 0; - const viewOf = (bytes: Uint8Array): DataView => new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); diff --git a/src/domain/apple/machoLoadCommands.test.ts b/src/domain/apple/machoLoadCommands.test.ts new file mode 100644 index 0000000000..3ef8830340 --- /dev/null +++ b/src/domain/apple/machoLoadCommands.test.ts @@ -0,0 +1,59 @@ +import { expect, it } from "vitest"; +import { + readMachoHeaderDeclarations, + readMachoLoadCommandDeclarations, + readMachoLoadCommandTableHeader, +} from "./machoLoadCommands.js"; + +it.each([ + { magic: 0xfeedface, wide: false, littleEndian: false }, + { magic: 0xcefaedfe, wide: false, littleEndian: true }, + { magic: 0xfeedfacf, wide: true, littleEndian: false }, + { magic: 0xcffaedfe, wide: true, littleEndian: true }, +])( + "separates common header observations from full header and table admission ($magic)", + ({ magic, wide, littleEndian }) => { + const backing = Buffer.alloc(43, 0xa5); + const bytes = backing.subarray(7, 35); + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + view.setUint32(0, magic, false); + view.setUint32(4, 0xfedcba98, littleEndian); + view.setUint32(8, 0x89abcdef, littleEndian); + view.setUint32(12, 0xffffffff, littleEndian); + view.setUint32(16, 0x80000001, littleEndian); + view.setUint32(20, 0xffffffff, littleEndian); + const expected = { + headerSize: wide ? 32 : 28, + wide, + littleEndian, + commandCount: 0x80000001, + commandBytes: 0xffffffff, + fileType: 0xffffffff, + identity: { cpuType: 0xfedcba98, cpuSubtype: 0x89abcdef }, + }; + expect(readMachoHeaderDeclarations(bytes, 32)).toEqual({ + ok: true, + value: expected, + }); + expect(readMachoLoadCommandDeclarations(bytes, 32)).toEqual( + wide + ? { + ok: false, + error: + "truncated Mach-O header: a 64-bit header needs 32 bytes; the file has 32", + } + : { ok: true, value: expected }, + ); + const complete = backing.subarray(7, wide ? 39 : 35); + expect(readMachoLoadCommandDeclarations(complete, 32)).toEqual({ + ok: true, + value: expected, + }); + expect(readMachoLoadCommandTableHeader(complete, 32)).toMatchObject({ + ok: false, + }); + expect( + readMachoHeaderDeclarations(bytes.subarray(0, 27), 32), + ).toMatchObject({ ok: false }); + }, +); diff --git a/src/domain/apple/machoLoadCommands.ts b/src/domain/apple/machoLoadCommands.ts index f8889e32f2..f7232c2bbb 100644 --- a/src/domain/apple/machoLoadCommands.ts +++ b/src/domain/apple/machoLoadCommands.ts @@ -1,5 +1,8 @@ import { err, ok, type Result } from "../result.js"; -import { machoLoadCommandAlignment } from "./machoContainer.js"; +import { + machoLoadCommandAlignment, + readMachoHeaderPrefixFacts, +} from "./machoContainer.js"; /** Header commitments needed to walk commands without retaining their payloads. */ export interface MachoLoadCommandTableHeader { @@ -10,23 +13,31 @@ export interface MachoLoadCommandTableHeader { readonly commandBytes: number; } -/** Decode thin-header declarations without admitting their table extent. */ -export const readMachoLoadCommandDeclarations = ( +export interface MachoHeaderDeclarations extends MachoLoadCommandTableHeader { + readonly fileType: number; + readonly identity: { readonly cpuType: number; readonly cpuSubtype: number }; +} + +/** Decode the common 28-byte header without requiring the 64-bit reserved word. */ +export const readMachoHeaderDeclarations = ( bytes: Uint8Array, fileSize: number, -): Result => { - if (bytes.byteLength < 4) return ok(null); - const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); - const magic = view.getUint32(0, false); - if (![0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(magic)) +): Result => { + const prefix = readMachoHeaderPrefixFacts(bytes); + const magic = prefix.magic; + if ( + magic === null || + ![0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(magic) + ) return ok(null); const wide = magic === 0xfeedfacf || magic === 0xcffaedfe; const headerSize = wide ? 32 : 28; - if (bytes.byteLength < headerSize) + if (bytes.byteLength < 28 || prefix.identity === null) return err( `truncated Mach-O header: a ${wide ? 64 : 32}-bit header needs ${headerSize} bytes; the file has ${fileSize}`, ); const littleEndian = magic === 0xcefaedfe || magic === 0xcffaedfe; + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const commandBytes = view.getUint32(20, littleEndian); return ok({ headerSize, @@ -34,9 +45,26 @@ export const readMachoLoadCommandDeclarations = ( littleEndian, commandCount: view.getUint32(16, littleEndian), commandBytes, + fileType: view.getUint32(12, littleEndian), + identity: prefix.identity, }); }; +/** Require the full declared 28/32-byte thin header, without admitting its command table. */ +export const readMachoLoadCommandDeclarations = ( + bytes: Uint8Array, + fileSize: number, +): Result => { + const parsed = readMachoHeaderDeclarations(bytes, fileSize); + if (!parsed.ok || parsed.value === null) return parsed; + const { wide, headerSize } = parsed.value; + if (bytes.byteLength < headerSize) + return err( + `truncated Mach-O header: a ${wide ? 64 : 32}-bit header needs ${headerSize} bytes; the file has ${fileSize}`, + ); + return parsed; +}; + /** Admit the declared command table within the complete file's extent. */ export const readMachoLoadCommandTableHeader = ( bytes: Uint8Array, diff --git a/src/native/AppleMachoSelection.ts b/src/native/AppleMachoSelection.ts index 6e06305c46..390ac96761 100644 --- a/src/native/AppleMachoSelection.ts +++ b/src/native/AppleMachoSelection.ts @@ -15,7 +15,6 @@ import { import { machoCpuSubtypeBase } from "../domain/apple/machoCpuIdentity.js"; import { readFatSliceDeclarations, - readMachoHeaderPrefixFacts, validateFatSlice, validateFatSliceIdentity, type FatSliceDeclaration, @@ -180,12 +179,7 @@ export const parseMachoLayout = ( ); } const table = declarations.value; - const identity = readMachoHeaderPrefixFacts(selectedBytes).identity; - if (identity === null) - throw new TypeError( - "Only little-endian 64-bit Mach-O metadata is supported", - ); - const { cpuType, cpuSubtype } = identity; + const { cpuType, cpuSubtype } = table.identity; const fat = selection.fatDeclaration; if (fat !== undefined) { const issue = validateFatSliceIdentity(fat, { diff --git a/src/native/metadata/MachoImage.ts b/src/native/metadata/MachoImage.ts index 56e5af40d3..e0c116df28 100644 --- a/src/native/metadata/MachoImage.ts +++ b/src/native/metadata/MachoImage.ts @@ -150,7 +150,7 @@ export const readMachoImage = (bytes: Buffer): BinaryContainer => { } const complete = commands.finish(); if (!complete.ok) throw new BinaryFormatFailure("malformed", complete.error); - const cpu = reader.u32(4); + const cpu = table.identity.cpuType; const names = new Map([ [7, "386"], [0x01000007, "amd64"],