fix(native): unify legacy Mach-O platform interpretation

This commit is contained in:
morluto committed 2026-10-24 04:49:21 +08:00
1 parent 5f4bd6ba34
commit 2b8e388bbe
5 files changed
+109 -84

No files matched your search

@@ -108,6 +108,7 @@ it.each([
{ command: LC.VERSION_MIN_IPHONEOS, cpu: CPU.i386, platform: 7 },
{ command: LC.VERSION_MIN_IPHONEOS, cpu: CPU.x86_64, platform: 7 },
{ command: LC.VERSION_MIN_IPHONEOS, cpu: CPU.arm64, platform: 2 },
{ command: LC.VERSION_MIN_TVOS, cpu: CPU.i386, platform: 3 },
{ command: LC.VERSION_MIN_TVOS, cpu: CPU.x86_64, platform: 8 },
{ command: LC.VERSION_MIN_TVOS, cpu: CPU.arm64, platform: 3 },
{ command: LC.VERSION_MIN_WATCHOS, cpu: CPU.i386, platform: 9 },
+14 -45
View File
@@ -4,6 +4,10 @@ import type {
MachoSlice,
} from "../../domain/apple/dylibResolution.js";
import { machoCpuSubtypeBase } from "../../domain/apple/machoCpuIdentity.js";
import {
legacyMachoPlatform,
type MachoVersionMinPlatform,
} from "../../domain/apple/machoPlatform.js";
import {
machoLoadCommandAlignment,
classifyFatContainerIdentity,
@@ -53,53 +57,18 @@ const LC_LOAD_UPWARD_DYLIB = 0x80000023;
const LC_RPATH = 0x8000001c;
const LC_DYLD_ENVIRONMENT = 0x27;
const LC_CODE_SIGNATURE = 0x1d;
const LC_VERSION_MIN_MACOSX = 0x24;
const LC_VERSION_MIN_IPHONEOS = 0x25;
const LC_VERSION_MIN_TVOS = 0x2f;
const LC_VERSION_MIN_WATCHOS = 0x30;
const LC_BUILD_VERSION = 0x32;
// `mach-o/loader.h` `PLATFORM_*` numbers observed via Apple linker output.
const PLATFORM_MACOS = 1;
const PLATFORM_IOS = 2;
const PLATFORM_TVOS = 3;
const PLATFORM_WATCHOS = 4;
const PLATFORM_IOSSIMULATOR = 7;
const PLATFORM_TVOSSIMULATOR = 8;
const PLATFORM_WATCHOSSIMULATOR = 9;
const CPU_TYPE_I386 = 7;
const CPU_TYPE_X86_64 = 0x01000007;
const CPU_TYPE_ARM64 = 0x0100000c;
/**
* Legacy `LC_VERSION_MIN_*` commands predate explicit simulator platform
* IDs. Per Apple's dyld rules the image CPU type distinguishes simulator
* from device: Intel images are simulator, ARM images are device
* (tvOS simulator applies to x86_64 only). `LC_BUILD_VERSION` platform
* numbers are preserved verbatim; unknown future numbers stay unknown
* downstream, never mapped to nearest.
*/
const legacyVersionMinPlatform = (
command: number,
cpuType: number,
): number | undefined => {
const isIntel = cpuType === CPU_TYPE_I386 || cpuType === CPU_TYPE_X86_64;
switch (command) {
case LC_VERSION_MIN_MACOSX:
return PLATFORM_MACOS;
case LC_VERSION_MIN_IPHONEOS:
return isIntel ? PLATFORM_IOSSIMULATOR : PLATFORM_IOS;
case LC_VERSION_MIN_TVOS:
return cpuType === CPU_TYPE_X86_64
? PLATFORM_TVOSSIMULATOR
: PLATFORM_TVOS;
case LC_VERSION_MIN_WATCHOS:
return isIntel ? PLATFORM_WATCHOSSIMULATOR : PLATFORM_WATCHOS;
default:
return undefined;
}
};
/** loader.h command codes and their device platform identities. */
const VERSION_MIN_PLATFORMS: ReadonlyMap<number, MachoVersionMinPlatform> =
new Map([
[0x24, 1], // LC_VERSION_MIN_MACOSX
[0x25, 2], // LC_VERSION_MIN_IPHONEOS
[0x2f, 3], // LC_VERSION_MIN_TVOS
[0x30, 4], // LC_VERSION_MIN_WATCHOS
]);
/** `mach-o/loader.h` `dylib_use_command`: `nameoff == 28` and this marker. */
const DYLIB_USE_MARKER = 0x1a741800;
@@ -691,14 +660,14 @@ const decodeCommand = (
slice.platforms.push(platform);
return;
}
const legacyPlatform = legacyVersionMinPlatform(command, slice.cpu_type);
const legacyPlatform = VERSION_MIN_PLATFORMS.get(command);
if (legacyPlatform !== undefined) {
if (body.byteLength < 16)
throw new MachoFormatIssue(
"malformed",
`load command ${index} is too short for a version-min command`,
);
slice.platforms.push(legacyPlatform);
slice.platforms.push(legacyMachoPlatform(legacyPlatform, slice.cpu_type));
return;
}
if (command === LC_LAZY_LOAD_DYLIB_INFO)
+19
View File
@@ -0,0 +1,19 @@
/** Device platform encoded by a legacy LC_VERSION_MIN_* command. */
export type MachoVersionMinPlatform = 1 | 2 | 3 | 4;
/**
* dyld derives legacy simulator platforms from the CPU type. tvOS uses
* x86_64 only; iOS and watchOS also recognize i386. Explicit LC_BUILD_VERSION
* platforms do not use this rule.
*/
export const legacyMachoPlatform = (
platform: MachoVersionMinPlatform,
cpuType: number | null,
): number => {
if (platform === 3) return cpuType === 0x01000007 ? 8 : 3;
if (cpuType === 7 || cpuType === 0x01000007) {
if (platform === 2) return 7;
if (platform === 4) return 9;
}
return platform;
};
+22 -19
View File
@@ -1,3 +1,8 @@
import {
legacyMachoPlatform,
type MachoVersionMinPlatform,
} from "../../domain/apple/machoPlatform.js";
interface ParsedSection {
readonly segment: string;
readonly name: string;
@@ -70,6 +75,14 @@ const DYLIB_FIELDS = [
const DYLIB_ENCODING_FIELDS = ["time stamp", "options"];
const EMPTY_VALUE_FIELDS = new Set(["options", "segname", "sectname"]);
const VERSION_MIN_PLATFORMS: ReadonlyMap<string, MachoVersionMinPlatform> =
new Map([
["LC_VERSION_MIN_MACOSX", 1],
["LC_VERSION_MIN_IPHONEOS", 2],
["LC_VERSION_MIN_TVOS", 3],
["LC_VERSION_MIN_WATCHOS", 4],
]);
/**
* Non-verbose otool prints LC_BUILD_VERSION platform and tool codes as
* numbers. Name the codes declared in the SDK's <mach-o/loader.h>, without
@@ -408,8 +421,16 @@ const collectBuild = (
}
if (kind.startsWith("LC_VERSION_MIN_")) {
const platform = kind.slice("LC_VERSION_MIN_".length) || null;
const devicePlatform = VERSION_MIN_PLATFORMS.get(kind);
const derivedPlatform =
devicePlatform === undefined
? undefined
: legacyMachoPlatform(devicePlatform, state.cpuType);
state.builds.push({
platform: legacySimulatorPlatform(platform, state.cpuType),
platform:
derivedPlatform === undefined || derivedPlatform === devicePlatform
? platform
: buildCodeName(BUILD_PLATFORM_NAMES, String(derivedPlatform)),
minimum_os: stringField(fields, "version"),
sdk: stringField(fields, "sdk"),
tools: [],
@@ -417,24 +438,6 @@ const collectBuild = (
}
};
// Apple dyld interprets legacy iOS/tvOS/watchOS minimum-version commands on
// Intel slices as simulator platforms. Newer commands encode this directly.
const legacySimulatorPlatform = (
platform: string | null,
cpuType: number | null,
): string | null => {
if (![7, 0x01000007].includes(cpuType ?? -1)) return platform;
return (
(
{
IPHONEOS: "IOSSIMULATOR",
TVOS: "TVOSSIMULATOR",
WATCHOS: "WATCHOSSIMULATOR",
} as const
)[platform as "IPHONEOS" | "TVOS" | "WATCHOS"] ?? platform
);
};
const collectDependency = (
kind: string,
fields: Readonly<Record<string, string | number | null>>,
@@ -269,33 +269,66 @@ Section
});
it.each([
["LC_VERSION_MIN_IPHONEOS", "IOSSIMULATOR"],
["LC_VERSION_MIN_TVOS", "TVOSSIMULATOR"],
["LC_VERSION_MIN_WATCHOS", "WATCHOSSIMULATOR"],
])("classifies legacy Intel simulator command %s", (command, platform) => {
["LC_VERSION_MIN_IPHONEOS", 7, "IOSSIMULATOR"],
["LC_VERSION_MIN_IPHONEOS", 16777223, "IOSSIMULATOR"],
["LC_VERSION_MIN_IPHONEOS", 16777228, "IPHONEOS"],
["LC_VERSION_MIN_TVOS", 7, "TVOS"],
["LC_VERSION_MIN_TVOS", 16777223, "TVOSSIMULATOR"],
["LC_VERSION_MIN_TVOS", 16777228, "TVOS"],
["LC_VERSION_MIN_WATCHOS", 7, "WATCHOSSIMULATOR"],
["LC_VERSION_MIN_WATCHOS", 16777223, "WATCHOSSIMULATOR"],
["LC_VERSION_MIN_WATCHOS", 16777228, "WATCHOS"],
["LC_VERSION_MIN_MACOSX", 7, "MACOSX"],
["LC_VERSION_MIN_MACOSX", 16777223, "MACOSX"],
["LC_VERSION_MIN_MACOSX", 16777228, "MACOSX"],
])(
"classifies legacy command %s on CPU %s as %s",
(command, cpuType, platform) => {
const text = [
"Mach header",
" magic cputype cpusubtype caps filetype ncmds sizeofcmds flags",
` ${cpuType === 7 ? "0xfeedface" : "0xfeedfacf"} ${cpuType} 3 0x00 2 1 16 0x0`,
"Load command 0",
` cmd ${command}`,
" cmdsize 16",
" version 12.0",
" sdk 12.1",
].join("\n");
expect(parseOtoolLoadCommands(text).builds[0]?.platform).toBe(platform);
},
);
it.each([
["3", "TVOS"],
["4294967295", "4294967295"],
])("preserves explicit build platform %s on Intel", (code, platform) => {
const text = [
"Mach header",
" magic cputype cpusubtype caps filetype ncmds sizeofcmds flags",
" 0xfeedfacf 16777223 3 0x00 2 1 24 0x0",
"Load command 0",
" cmd LC_BUILD_VERSION",
" cmdsize 24",
` platform ${code}`,
" minos 12.0",
" sdk 12.1",
" ntools 0",
].join("\n");
expect(parseOtoolLoadCommands(text).builds[0]?.platform).toBe(platform);
});
it("preserves an unknown legacy platform spelling on Intel", () => {
const text = [
"Mach header",
" magic cputype cpusubtype caps filetype ncmds sizeofcmds flags",
" 0xfeedfacf 16777223 3 0x00 2 1 16 0x0",
"Load command 0",
` cmd ${command}`,
" cmd LC_VERSION_MIN_FUTURE",
" cmdsize 16",
" version 12.0",
" sdk 12.1",
].join("\n");
expect(parseOtoolLoadCommands(text).builds[0]?.platform).toBe(platform);
});
it("keeps legacy device platforms on arm64 slices", () => {
const text = [
"Mach header",
" magic cputype cpusubtype caps filetype ncmds sizeofcmds flags",
" 0xfeedfacf 16777228 0 0x00 2 1 16 0x0",
"Load command 0",
" cmd LC_VERSION_MIN_IPHONEOS",
" cmdsize 16",
" version 12.0",
" sdk 12.1",
].join("\n");
expect(parseOtoolLoadCommands(text).builds[0]?.platform).toBe("IPHONEOS");
const result = parseOtoolLoadCommands(text);
expect(result.builds[0]?.platform).toBe("FUTURE");
expect(result.commands[0]?.kind).toBe("LC_VERSION_MIN_FUTURE");
});