diff --git a/src/artifacts/MachOSliceArtifactReader.ts b/src/artifacts/MachOSliceArtifactReader.ts index 5928956ee..f567458c7 100644 --- a/src/artifacts/MachOSliceArtifactReader.ts +++ b/src/artifacts/MachOSliceArtifactReader.ts @@ -8,11 +8,11 @@ import { type NativeCommandRunner, } from "../native/CommandRunner.js"; import { - lipoSubtypeBits, parseLipoArchitectures, type LipoArchitecture, } from "../native/parsers/lipo.js"; import type { MachoSlice } from "../domain/apple/dylibResolution.js"; +import { machoCpuSubtypeBase } from "../domain/apple/machoCpuIdentity.js"; import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js"; import { readMachoImage, type ReadAt } from "./apple/MachoLoadCommandReader.js"; import { @@ -471,8 +471,10 @@ const lipoSubtypeMatches = ( ): boolean => { if (reported.cpu_subtype_code !== null) return reported.cpu_subtype_code === observedCpuSubtype; - const bits = lipoSubtypeBits(reported.cpu_subtype); - return bits === null || bits === (observedCpuSubtype & 0x00ffffff); + return ( + reported.cpu_subtype_base === null || + reported.cpu_subtype_base === machoCpuSubtypeBase(observedCpuSubtype) + ); }; /** diff --git a/src/native/AppleMachoSelection.ts b/src/native/AppleMachoSelection.ts index e63ec082d..c0c68068d 100644 --- a/src/native/AppleMachoSelection.ts +++ b/src/native/AppleMachoSelection.ts @@ -1,9 +1,9 @@ import type { FixupCommands } from "./AppleMachoFixups.js"; import { MACHO_ARCHITECTURE_POLICIES, - baseMachoCpuSubtype, machoSubtypeRank, } from "./MachoArchitectureSelection.js"; +import { machoCpuSubtypeBase } from "../domain/apple/machoCpuIdentity.js"; import { readFatSliceDeclarations, validateFatSlice, @@ -117,8 +117,8 @@ export const selectMachoSlice = ( // Equal ranks keep FAT table order unless the slices share a subtype. if ( rank === bestRank && - baseMachoCpuSubtype(declaration.cpuSubtype) === - baseMachoCpuSubtype(best.cpuSubtype) + machoCpuSubtypeBase(declaration.cpuSubtype) === + machoCpuSubtypeBase(best.cpuSubtype) ) ambiguous = true; continue; diff --git a/src/native/MachoArchitectureSelection.ts b/src/native/MachoArchitectureSelection.ts index b6e6dcec4..14c5f696f 100644 --- a/src/native/MachoArchitectureSelection.ts +++ b/src/native/MachoArchitectureSelection.ts @@ -1,3 +1,6 @@ +import { machoCpuSubtypeBase } from "../domain/apple/machoCpuIdentity.js"; +import type { LipoArchitecture } from "./parsers/lipo.js"; + interface MachoArchitecturePolicy { readonly cpuType: number; readonly subtypes: readonly { @@ -28,17 +31,13 @@ export const MACHO_ARCHITECTURE_POLICIES: Readonly< }, }; -/** High subtype bits carry capabilities, such as the arm64e PAC ABI version. */ -export const baseMachoCpuSubtype = (subtype: number): number => - subtype & 0x00ffffff; - /** Unlisted subtypes follow the preferred slices in FAT table order. */ export const machoSubtypeRank = ( policy: MachoArchitecturePolicy, subtype: number, ): number => { const rank = policy.subtypes.findIndex( - (candidate) => candidate.subtype === baseMachoCpuSubtype(subtype), + (candidate) => candidate.subtype === machoCpuSubtypeBase(subtype), ); return rank < 0 ? policy.subtypes.length : rank; }; @@ -46,9 +45,36 @@ export const machoSubtypeRank = ( /** Select a tool spelling using the same preference order as FAT selection. */ export const selectMachoArchitectureName = ( policy: MachoArchitecturePolicy, - available: readonly string[], + available: readonly LipoArchitecture[], ): string | undefined => { - for (const { name } of policy.subtypes) - if (available.includes(name)) return name; - return available.find((name) => policy.variantName?.test(name)); + let selected: string | undefined; + let bestRank = Number.POSITIVE_INFINITY; + for (const architecture of available) { + if ( + architecture.cpu_type_code !== null && + architecture.cpu_type_code !== policy.cpuType + ) + continue; + const namedSubtype = policy.subtypes.find( + ({ name }) => name === architecture.name, + ); + const recognizedName = + namedSubtype !== undefined || policy.variantName?.test(architecture.name); + if ( + !recognizedName && + (architecture.cpu_type_code === null || + architecture.cpu_subtype_base === null) + ) + continue; + // Parsed identity is authoritative. Names support thin/partial lipo records. + const subtype = architecture.cpu_subtype_base ?? namedSubtype?.subtype; + const rank = + subtype === undefined + ? policy.subtypes.length + : machoSubtypeRank(policy, subtype); + if (rank >= bestRank) continue; + selected = architecture.name; + bestRank = rank; + } + return selected; }; diff --git a/src/native/NativeMachoInspection.test.ts b/src/native/NativeMachoInspection.test.ts index 7ddff2491..640e540be 100644 --- a/src/native/NativeMachoInspection.test.ts +++ b/src/native/NativeMachoInspection.test.ts @@ -94,6 +94,8 @@ describe("Mach-O tool architecture selection", () => { { names: ["arm64v8"], selected: "arm64v8" }, { names: ["arm64v8", "arm64e.x1"], selected: "arm64v8" }, { names: ["arm64e.x1", "arm64v8"], selected: "arm64v8" }, + { names: ["arm64v8", "arm64e.v1"], selected: "arm64e.v1" }, + { names: ["arm64e.v1", "arm64v8"], selected: "arm64e.v1" }, ] as const)( "keeps inspection and dispatch on $selected for FAT order $names", async ({ names, selected }) => { @@ -102,6 +104,7 @@ describe("Mach-O tool architecture selection", () => { arm64v8: { type: CPU.arm64.type, subtype: 1 }, arm64e: CPU.arm64e, "arm64e.x1": { type: CPU.arm64.type, subtype: 0x8000000c }, + "arm64e.v1": { type: CPU.arm64.type, subtype: 0x81000002 }, }; const slices = names.map((name) => { const cpu = cpus[name]; @@ -125,9 +128,12 @@ describe("Mach-O tool architecture selection", () => { "-h", "-l", "-arch", - selected, + selected.split(".")[0], "/fixture/app", ]); + expect(commands.find(({ tool }) => tool === "dyld_info")?.args).toContain( + selected, + ); expect(selectMachoSlice(fat, "arm64").slice).toBe( (names.findIndex((name) => name === selected) + 1) * 4096, ); diff --git a/src/native/NativeMachoInspection.ts b/src/native/NativeMachoInspection.ts index 836c3de1e..5fca089cb 100644 --- a/src/native/NativeMachoInspection.ts +++ b/src/native/NativeMachoInspection.ts @@ -14,7 +14,10 @@ import { selectMachoArchitectureName, } from "./MachoArchitectureSelection.js"; import { parseDyldSymbols } from "./parsers/dyldInfo.js"; -import { parseLipoArchitectures } from "./parsers/lipo.js"; +import { + parseLipoArchitectures, + type LipoArchitecture, +} from "./parsers/lipo.js"; import { withoutEchoedPathHeader } from "./parsers/echoedPath.js"; import { parseOtoolLoadCommands } from "./parsers/otool.js"; import { reconcileMachoUuid } from "./parsers/machoUuid.js"; @@ -72,8 +75,7 @@ export const inspectNativeMacho = async ( const lipo = captures.find(({ tool }) => tool === "lipo"); if (lipo === undefined) throw new TypeError("Missing lipo capture"); const architectures = parseLipoArchitectures(lipo.stdout); - const availableArchitectures = architectures.map(({ name }) => name); - const selection = selectArchitecture(context.target, availableArchitectures); + const selection = selectArchitecture(context.target, architectures); if (selection.status === "absent") return err( new AnalysisOutputError( @@ -401,7 +403,7 @@ type ArchitectureSelection = const selectArchitecture = ( target: BinaryTarget, - available: readonly string[], + available: readonly LipoArchitecture[], ): ArchitectureSelection => { // BinaryTarget lists CPU families and lipo lists slices. A universal file // can hold several slices of one family (arm64e and arm64e.x1), and slice @@ -416,7 +418,7 @@ const selectArchitecture = ( const policy = MACHO_ARCHITECTURE_POLICIES[target.architecture]; const selected = policy === undefined - ? available.find((name) => name === normalized) + ? available.find(({ name }) => name === normalized)?.name : selectMachoArchitectureName(policy, available); if (selected !== undefined) return { status: "selected", architecture: selected }; diff --git a/src/native/parsers/lipo.test.ts b/src/native/parsers/lipo.test.ts index 43f08d8b8..96f2afbbc 100644 --- a/src/native/parsers/lipo.test.ts +++ b/src/native/parsers/lipo.test.ts @@ -59,6 +59,7 @@ architecture arm64e cpu_type_code: null, cpu_subtype: "CPU_SUBTYPE_FUTURE", cpu_subtype_code: null, + cpu_subtype_base: null, }, ]); }); @@ -85,8 +86,29 @@ architecture arm64e { cpu_subtype: "CPU_SUBTYPE_X86_ALL", cpu_subtype_code: null, + cpu_subtype_base: 3, capabilities: null, }, ]); }); + + it.each([ + { capabilities: "PTR_AUTH_VERSION USERSPACE 1", code: 0x81000002 }, + { capabilities: "", code: null }, + ])( + "retains an arm64e base subtype with capabilities '$capabilities'", + ({ capabilities, code }) => { + expect( + parseLipoArchitectures( + `architecture arm64e.v1\n cputype CPU_TYPE_ARM64\n cpusubtype CPU_SUBTYPE_ARM64E\n capabilities ${capabilities}\n`, + ), + ).toMatchObject([ + { + cpu_type_code: 0x0100000c, + cpu_subtype_code: code, + cpu_subtype_base: 2, + }, + ]); + }, + ); }); diff --git a/src/native/parsers/lipo.ts b/src/native/parsers/lipo.ts index 4f56d8b6b..cc65cca41 100644 --- a/src/native/parsers/lipo.ts +++ b/src/native/parsers/lipo.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { machoCpuSubtypeBase } from "../../domain/apple/machoCpuIdentity.js"; const architectureSchema = z.object({ name: z.string().min(1), @@ -6,6 +7,7 @@ const architectureSchema = z.object({ cpu_subtype: z.string().nullable(), cpu_type_code: z.number().int().nonnegative().nullable(), cpu_subtype_code: z.number().int().nonnegative().nullable(), + cpu_subtype_base: z.number().int().nonnegative().nullable(), capabilities: z.string().nullable(), file_offset: z.number().int().min(0).nullable(), size: z.number().int().min(0).nullable(), @@ -30,6 +32,7 @@ export const parseLipoArchitectures = (output: string): LipoArchitecture[] => { cpu_subtype: null, cpu_type_code: null, cpu_subtype_code: null, + cpu_subtype_base: null, capabilities: null, file_offset: null, size: null, @@ -54,6 +57,7 @@ export const parseLipoArchitectures = (output: string): LipoArchitecture[] => { current.cpusubtype, current.capabilities, ), + cpu_subtype_base: cpuSubtypeBase(current.cpusubtype), capabilities: current.capabilities ?? null, file_offset: integer(current.offset), size: integer(current.size), @@ -158,10 +162,10 @@ const cpuSubtypeCode = ( }; /** The subtype bits a lipo record names, without its capability byte. */ -export const lipoSubtypeBits = (value: string | null): number | null => { - if (value === null) return null; +const cpuSubtypeBase = (value: string | undefined): number | null => { + if (value === undefined) return null; const subtype = unsigned32(value) ?? CPU_SUBTYPES[value]; - return subtype === undefined ? null : subtype & 0x00ffffff; + return subtype === undefined ? null : machoCpuSubtypeBase(subtype); }; /** lipo names the LIB64 capability and prints other capability bits in hex. */ diff --git a/tests/boundary/providers/native/nativeSelectedArchitecture.test.ts b/tests/boundary/providers/native/nativeSelectedArchitecture.test.ts index 90b598f5d..a651e22f6 100644 --- a/tests/boundary/providers/native/nativeSelectedArchitecture.test.ts +++ b/tests/boundary/providers/native/nativeSelectedArchitecture.test.ts @@ -1,4 +1,5 @@ import { expect, it } from "vitest"; +import { CPU } from "../../../../src/artifacts/apple/MachoImage.fixture.js"; import { inspectMachoSchema } from "../../../../src/domain/native/nativeInspection.js"; import { err } from "../../../../src/domain/result.js"; import { @@ -90,18 +91,39 @@ it.each([ selected: "arm64e", dyldArchitecture: "arm64e.v1", }, -])( + { + architectures: ["arm64v8", "arm64e.v1"], + selected: "arm64e", + dyldArchitecture: "arm64e.v1", + }, + { + architectures: ["arm64e.v1", "arm64v8"], + selected: "arm64e", + dyldArchitecture: "arm64e.v1", + }, +] as const)( "selects the $dyldArchitecture slice of a universal arm64e target", async ({ architectures, selected, dyldArchitecture }) => { const calls: { readonly tool: string; readonly arguments_: readonly string[]; }[] = []; + const cpus = { + x86_64: CPU.x86_64, + arm64e: CPU.arm64e, + arm64v8: { type: CPU.arm64.type, subtype: 1 }, + "arm64e.x1": { type: CPU.arm64.type, subtype: 0x8000000c }, + "arm64e.v1": { type: CPU.arm64.type, subtype: 0x81000002 }, + }; const lipo = architectures - .map( - (architecture, index) => - `architecture ${architecture}\n cputype 16777228\n cpusubtype 2\n offset ${index * 16384}\n size 8192\n align 2^14 (16384)`, - ) + .map((architecture, index) => { + const cpu = cpus[architecture]; + const subtype = + architecture === "arm64e.v1" + ? "CPU_SUBTYPE_ARM64E\n capabilities PTR_AUTH_VERSION USERSPACE 1" + : String(cpu.subtype); + return `architecture ${architecture}\n cputype ${cpu.type}\n cpusubtype ${subtype}\n offset ${index * 16384}\n size 8192\n align 2^14 (16384)`; + }) .join("\n"); const fixtures = new NativeFixtureRunner({ lipo, @@ -118,6 +140,11 @@ it.each([ if (!result.ok) throw result.error; expect(result.value.result).toMatchObject({ uuid: "01234567-89AB-CDEF-0123-456789ABCDEF", + selected_architecture: { + name: dyldArchitecture, + container_file_offset: + architectures.findIndex((name) => name === dyldArchitecture) * 16384, + }, architectures: { items: architectures.map((name) => ({ name })), },