From f3e6582e9ed327c5accb8f9068ea0eb9bac49b1f Mon Sep 17 00:00:00 2001 From: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:32:31 -0400 Subject: [PATCH] fix(native): inspect Apple dispatch metadata in FAT64 binaries (#590) Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> --- src/native/AppleDispatchMetadata.test.ts | 74 ++++++++++++++++++++++++ src/native/AppleDispatchMetadata.ts | 56 ++++++++++-------- 2 files changed, 107 insertions(+), 23 deletions(-) diff --git a/src/native/AppleDispatchMetadata.test.ts b/src/native/AppleDispatchMetadata.test.ts index 3d75710f5..aa3156209 100644 --- a/src/native/AppleDispatchMetadata.test.ts +++ b/src/native/AppleDispatchMetadata.test.ts @@ -117,3 +117,77 @@ describe("Apple dispatch binary metadata", () => { ).toThrow("Truncated"); }); }); + +describe("universal Apple dispatch metadata", () => { + it.each([false, true])("decodes selected FAT slices (fat64=%s)", (fat64) => { + const thin = fixture(); + const offset = 256; + const bytes = Buffer.alloc(offset + thin.length); + bytes.writeUInt32BE(fat64 ? 0xcafebabf : 0xcafebabe, 0); + bytes.writeUInt32BE(1, 4); + bytes.writeUInt32BE(0x0100000c, 8); + if (fat64) { + bytes.writeBigUInt64BE(BigInt(offset), 16); + bytes.writeBigUInt64BE(BigInt(thin.length), 24); + } else { + bytes.writeUInt32BE(offset, 16); + bytes.writeUInt32BE(thin.length, 20); + } + thin.copy(bytes, offset); + const result = decodeAppleDispatchMetadata(bytes, 100, provenance); + expect(result.objc_classes[0]).toMatchObject({ + name: "Fixture", + location: { address: "0x100000200", file_offset: offset + 512 }, + }); + expect(result.objc_dispatch_implementations[0]).toMatchObject({ + implementation_address: "0x100000700", + location: { file_offset: offset + 0x488 }, + }); + }); +}); + +describe("FAT64 dispatch slice validation", () => { + const wrapped = () => { + const thin = fixture(); + const bytes = Buffer.alloc(256 + thin.length); + bytes.writeUInt32BE(0xcafebabf, 0); + bytes.writeUInt32BE(1, 4); + bytes.writeUInt32BE(0x0100000c, 8); + bytes.writeBigUInt64BE(256n, 16); + bytes.writeBigUInt64BE(BigInt(thin.length), 24); + thin.copy(bytes, 256); + return bytes; + }; + + it("rejects a truncated architecture table", () => { + expect(() => + decodeAppleDispatchMetadata(wrapped().subarray(0, 32), 100, provenance), + ).toThrow("Malformed FAT architecture table"); + }); + + it.each([16, 24])( + "checks the full 64-bit slice extent at field %i", + (field) => { + const bytes = wrapped(); + bytes.writeBigUInt64BE(0x100000000n, field); + expect(() => decodeAppleDispatchMetadata(bytes, 100, provenance)).toThrow( + "FAT slice exceeds file", + ); + }, + ); + + it("rejects ambiguous matching architectures", () => { + const bytes = wrapped(); + bytes.writeUInt32BE(2, 4); + bytes.copy(bytes, 40, 8, 40); + expect(() => decodeAppleDispatchMetadata(bytes, 100, provenance)).toThrow( + "Ambiguous FAT architecture slice", + ); + }); + + it("rejects a missing selected architecture", () => { + expect(() => + decodeAppleDispatchMetadata(wrapped(), 100, provenance, "x86_64"), + ).toThrow("Requested FAT architecture is absent"); + }); +}); diff --git a/src/native/AppleDispatchMetadata.ts b/src/native/AppleDispatchMetadata.ts index 8dd8e13e3..6ee6b6bb3 100644 --- a/src/native/AppleDispatchMetadata.ts +++ b/src/native/AppleDispatchMetadata.ts @@ -31,30 +31,8 @@ export const decodeAppleDispatchMetadata = ( provenance: { path: string; sha256: string }, architecture = "arm64", ): ObjcSwiftMetadata => { - let slice = 0; - let sliceEnd = bytes.length; if (bytes.length < 32) throw new RangeError("Truncated Mach-O header"); - if (bytes.readUInt32BE(0) === 0xcafebabe) { - const count = bytes.readUInt32BE(4); - if (count > 128 || 8 + count * 20 > bytes.length) - throw new RangeError("Malformed FAT architecture table"); - const cpu = architecture === "arm64" ? 0x0100000c : 0x01000007; - let selected: number | undefined; - for (let index = 0; index < count; index++) { - const offset = 8 + index * 20; - if (bytes.readUInt32BE(offset) === cpu) { - if (selected !== undefined) - throw new TypeError("Ambiguous FAT architecture slice"); - selected = bytes.readUInt32BE(offset + 8); - sliceEnd = selected + bytes.readUInt32BE(offset + 12); - if (sliceEnd > bytes.length || selected < 8 + count * 20) - throw new RangeError("FAT slice exceeds file"); - } - } - if (selected === undefined) - throw new TypeError("Requested FAT architecture is absent"); - slice = selected; - } + const { slice, sliceEnd } = selectMachoSlice(bytes, architecture); if (slice + 32 > sliceEnd || bytes.readUInt32LE(slice) !== 0xfeedfacf) throw new TypeError( "Only little-endian 64-bit Mach-O metadata is supported", @@ -672,3 +650,35 @@ export const inspectAppleDispatchMetadata = async ( await handle.close(); } }; + +const selectMachoSlice = (bytes: Buffer, architecture: string) => { + const magic = bytes.readUInt32BE(0); + if (magic !== 0xcafebabe && magic !== 0xcafebabf) + return { slice: 0, sliceEnd: bytes.length }; + const fat64 = magic === 0xcafebabf; + const stride = fat64 ? 32 : 20; + const count = bytes.readUInt32BE(4); + const headerEnd = 8 + count * stride; + if (count > 128 || headerEnd > bytes.length) + throw new RangeError("Malformed FAT architecture table"); + const cpu = architecture === "arm64" ? 0x0100000c : 0x01000007; + let selected: { slice: number; sliceEnd: number } | undefined; + for (let index = 0; index < count; index++) { + const offset = 8 + index * stride; + if (bytes.readUInt32BE(offset) !== cpu) continue; + if (selected !== undefined) + throw new TypeError("Ambiguous FAT architecture slice"); + const start = fat64 + ? bytes.readBigUInt64BE(offset + 8) + : BigInt(bytes.readUInt32BE(offset + 8)); + const size = fat64 + ? bytes.readBigUInt64BE(offset + 16) + : BigInt(bytes.readUInt32BE(offset + 12)); + if (start < BigInt(headerEnd) || start + size > BigInt(bytes.length)) + throw new RangeError("FAT slice exceeds file"); + selected = { slice: Number(start), sliceEnd: Number(start + size) }; + } + if (selected === undefined) + throw new TypeError("Requested FAT architecture is absent"); + return selected; +};