fix(native): inspect Apple dispatch metadata in FAT64 binaries (#590)

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
This commit is contained in:
Rudy Mizrahi Celekli 2026-10-05 12:32:31 -04:00 committed by GitHub
parent b37e47c9f7
commit f3e6582e9e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 107 additions and 23 deletions

View File

@ -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");
});
});

View File

@ -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;
};