diff --git a/scripts/verify/apple/dispatch.mjs b/scripts/verify/apple/dispatch.mjs index 6f6dd4bf34..5f419e89ad 100644 --- a/scripts/verify/apple/dispatch.mjs +++ b/scripts/verify/apple/dispatch.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { execFile } from "node:child_process"; -import { mkdtemp, rm } from "node:fs/promises"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -300,6 +300,50 @@ const root = await mkdtemp(join(tmpdir(), "rea-dispatch-")); let report; try { const results = []; + const commands = 4097; + const commandTable = Buffer.alloc(32 + commands * 16); + commandTable.writeUInt32LE(0xfeedfacf); + commandTable.writeUInt32LE(0x0100000c, 4); + commandTable.writeUInt32LE(2, 12); + commandTable.writeUInt32LE(commands, 16); + commandTable.writeUInt32LE(commands * 16, 20); + for (let cursor = 32; cursor < commandTable.length; cursor += 16) { + commandTable.writeUInt32LE(0x2a, cursor); // LC_SOURCE_VERSION + commandTable.writeUInt32LE(16, cursor + 4); + } + const commandTablePath = join(root, "many-commands.macho"); + await writeFile(commandTablePath, commandTable); + const cli = await artifactCliEvidence( + "inspect-native-dispatch-metadata", + commandTablePath, + ["--max-input-bytes", String(commandTable.length)], + ); + const cliResult = nativeDispatchMetadataResultSchema.parse( + cli.normalized_result, + ); + assert.deepEqual(cliResult.input_admission, { + maximum_bytes: commandTable.length, + source: "caller", + retained_bytes: commandTable.length, + }); + assert.equal( + cliResult.result.target_selection.byte_length, + commandTable.length, + ); + assert.deepEqual(cliResult.result.objc_classes, []); + await withArtifactMcp(commandTablePath, async (client) => { + const mcpResult = nativeDispatchMetadataResultSchema.parse( + await artifactMcpResult(client, "inspect_native_dispatch_metadata", { + max_input_bytes: commandTable.length, + }), + ); + assert.deepEqual(mcpResult, cliResult); + }); + results.push({ + variant: "many-load-commands", + commands, + target_sha256: cliResult.target_sha256, + }); for (const mode of LINK_MODES) { const objcPath = join(root, `fixture-${mode.mode}`); await exec("/usr/bin/xcrun", [ diff --git a/src/native/AppleMachoSelection.test.ts b/src/native/AppleMachoSelection.test.ts index b9fbe75dac..2d9498ec81 100644 --- a/src/native/AppleMachoSelection.test.ts +++ b/src/native/AppleMachoSelection.test.ts @@ -110,6 +110,41 @@ it.each([false, true])( }, ); +it.each([ + { container: "thin", corrupt: false }, + { container: "thin", corrupt: true }, + { container: "fat", corrupt: false }, + { container: "fat", corrupt: true }, +])( + "walks the entire selected command table beyond 4096 commands ($container, corrupt=$corrupt)", + ({ container, corrupt }) => { + const command = Buffer.alloc(16); + command.writeUInt32LE(0x2a); // LC_SOURCE_VERSION + command.writeUInt32LE(16, 4); + const thin = machoImage({ + commands: Array.from({ length: 4097 }, () => command), + }); + if (corrupt) + new DataView(thin.buffer).setUint32(32 + 4096 * 16 + 4, 4, true); + const bytes = Buffer.from( + container === "fat" ? fatImage([{ cpu: CPU.arm64, bytes: thin }]) : thin, + ); + const decode = () => + decodeAppleDispatchMetadata(bytes, 10, { + path: "/many-commands", + sha256: "fixture", + }); + if (corrupt) { + expect(decode).toThrow(/load command 4096 .*invalid cmdsize 4/u); + return; + } + expect(decode()).toMatchObject({ + objc_classes: [], + target_selection: { container, byte_length: thin.byteLength }, + }); + }, +); + it.each([ { source: "__objc_const\0\nxx", expected: "__objc_const" }, { source: "abcdefghijklmnop", expected: "abcdefghijklmnop" }, diff --git a/src/native/AppleMachoSelection.ts b/src/native/AppleMachoSelection.ts index 5949198699..2cba27ad30 100644 --- a/src/native/AppleMachoSelection.ts +++ b/src/native/AppleMachoSelection.ts @@ -198,9 +198,8 @@ export const parseMachoLayout = ( throw new UnsupportedMachoMetadataFormatError( `Unsupported 32-bit Mach-O metadata for requested CPU type ${architecture}`, ); - const commands = table.commandCount; const commandEnd = slice + table.headerSize + table.commandBytes; - if (commands > 4096 || commandEnd > sliceEnd) + if (commandEnd > sliceEnd) throw new RangeError("Malformed Mach-O command bounds"); const state: LayoutState = { slice,