fix(native): rank Mach-O slices by parsed CPU subtype
This commit is contained in:
parent
96fc84f1fb
commit
c02fb0ea80
|
|
@ -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)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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 })),
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue