From cc13c5d095048cc53bbc05e7f0342ad85ea60380 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Mon, 9 Nov 2026 19:34:28 +0800 Subject: [PATCH] fix(native): preserve unknown ARM64 subtype in hardening (#9600) --- src/domain/apple/machoCpuIdentity.ts | 23 +++++++-- src/native/hardening/MachoHardening.ts | 18 +++++-- tests/boundary/mcp/binaryHardeningMcp.test.ts | 49 ++++++++++++++++++- 3 files changed, 81 insertions(+), 9 deletions(-) diff --git a/src/domain/apple/machoCpuIdentity.ts b/src/domain/apple/machoCpuIdentity.ts index cb16f9647f..37fe90db80 100644 --- a/src/domain/apple/machoCpuIdentity.ts +++ b/src/domain/apple/machoCpuIdentity.ts @@ -5,6 +5,24 @@ const CPU_SUBTYPE_CAPABILITY_MASK = 0xff000000; export const machoCpuSubtypeBase = (cpuSubtype: number): number => cpuSubtype & ~CPU_SUBTYPE_CAPABILITY_MASK; +/** Known ARM64 subtype identity, distinct from the family display fallback. */ +export const machoKnownArm64Subtype = ( + cpuSubtype: number, +): string | undefined => { + switch (machoCpuSubtypeBase(cpuSubtype)) { + case 0: + return "arm64"; + case 1: + return "arm64v8"; + case 2: + return "arm64e"; + case 12: + return "arm64e.x1"; + default: + return undefined; + } +}; + /** * Display known byte-header identities without inferring pointer-auth ABI names. * Subtype 12: llvm-project b8007a8e4020b8bca2b12e941660e10bf5bf6716, @@ -16,10 +34,7 @@ export const machoArchitectureName = ( ): string => { const subtype = machoCpuSubtypeBase(cpuSubtype); if (cpuType === 0x0100000c) { - if (subtype === 12) return "arm64e.x1"; - if (subtype === 2) return "arm64e"; - if (subtype === 1) return "arm64v8"; - return "arm64"; + return machoKnownArm64Subtype(cpuSubtype) ?? "arm64"; } if (cpuType === 0x0200000c) return "arm64_32"; if (cpuType === 0x01000007) return subtype === 8 ? "x86_64h" : "x86_64"; diff --git a/src/native/hardening/MachoHardening.ts b/src/native/hardening/MachoHardening.ts index 08eba9e373..021a9b905d 100644 --- a/src/native/hardening/MachoHardening.ts +++ b/src/native/hardening/MachoHardening.ts @@ -1,5 +1,8 @@ import { validateFatSliceIdentity } from "../../domain/apple/machoContainer.js"; -import { machoArchitectureName } from "../../domain/apple/machoCpuIdentity.js"; +import { + machoArchitectureName, + machoKnownArm64Subtype, +} from "../../domain/apple/machoCpuIdentity.js"; import { MACHO_HEADER_FLAGS_OFFSET, readMachoLinkeditDataCommand, @@ -386,7 +389,6 @@ const buildRows = ( flagLocation, ); }; - const architecture = machoArchitectureName(header.cpuType, header.cpuSubtype); const pie = header.fileType === MH_DYLIB || header.fileType === MH_BUNDLE ? row( @@ -435,7 +437,7 @@ const buildRows = ( }, ), encryptionRow(encrypted), - arm64Row(header.cpuType, architecture, ref.offset), + arm64Row(header.cpuType, header.cpuSubtype, ref.offset), stackRow(bytes, ref, header.wide, symtab), row( "hardened_runtime", @@ -523,7 +525,7 @@ const encryptionRow = (encrypted: MachoSliceEncryption): BinaryHardeningRow => { const arm64Row = ( cpuType: number, - architecture: string, + cpuSubtype: number, sliceOffset: number, ): BinaryHardeningRow => { const location = { offset: sliceOffset + 8, bytes: 4 }; @@ -534,6 +536,14 @@ const arm64Row = ( `CPU type 0x${cpuType.toString(16)} is not ARM64.`, location, ); + const architecture = machoKnownArm64Subtype(cpuSubtype); + if (architecture === undefined) + return row( + "arm64e", + "unknown", + `Unrecognized ARM64 CPU subtype 0x${(cpuSubtype >>> 0).toString(16)}; arm64e status is unknown.`, + { offset: sliceOffset + 8, bytes: 4 }, + ); const enabled = architecture === "arm64e" || architecture === "arm64e.x1"; return row( "arm64e", diff --git a/tests/boundary/mcp/binaryHardeningMcp.test.ts b/tests/boundary/mcp/binaryHardeningMcp.test.ts index 2188163f2c..f1c61c8fc9 100644 --- a/tests/boundary/mcp/binaryHardeningMcp.test.ts +++ b/tests/boundary/mcp/binaryHardeningMcp.test.ts @@ -3,7 +3,10 @@ import { join } from "node:path"; import { expect } from "vitest"; import { toolContract } from "../../../src/contracts/toolContracts.js"; import { createServer } from "../../../src/server/createServer.js"; -import { elfHardeningFixture } from "../../../src/native/hardening/BinaryHardening.fixture.js"; +import { + elfHardeningFixture, + machoHardeningFixture, +} from "../../../src/native/hardening/BinaryHardening.fixture.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { mcpTest } from "../../support/mcp/mcpFixture.js"; @@ -63,3 +66,47 @@ mcpTest( }, 60_000, ); + +mcpTest( + "reports an unknown ARM64 subtype without treating its display fallback as arm64e proof", + async ({ mcp }) => { + const session = createTestBinarySession(() => { + throw new Error("Must not start a deep provider"); + }); + const client = await mcp.connect( + createServer({ kind: "session", session }), + ); + try { + const root = await createTestTempDirectory("rea-arm64-subtype-"); + const path = join(root, "image"); + for (const [subtype, state] of [ + [0, "disabled"], + [1, "disabled"], + [2, "enabled"], + [0x8000000c, "enabled"], + [3, "unknown"], + ] as const) { + const bytes = machoHardeningFixture({ cpuSubtype: subtype }); + await writeFile(path, bytes); + const response = await client.callTool({ + name: "inspect_binary_hardening", + arguments: { path }, + }); + expect(response.isError, JSON.stringify(response.content)).not.toBe( + true, + ); + const evidence = toolContract( + "inspect_binary_hardening", + ).outputSchema.parse(response.structuredContent); + const image = evidence.normalized_result.images[0]; + expect(image?.status).toBe("parsed"); + expect( + image?.rows.find((row) => row.property === "arm64e")?.state, + ).toBe(state); + } + } finally { + await session.close(); + } + }, + 60_000, +);