From 0b32577a90aef97e36adf2a7af4f3eccd5d82357 Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Fri, 9 Oct 2026 02:37:03 +0000 Subject: [PATCH] fix(artifacts): match lipo arm64e variant names to their FAT slices Newer lipo names arm64e ABI variants `arm64e.`: `arm64e.v1` for pointer-auth ABI version 1 on freshly built universal binaries, and `arm64e.x1` on recent macOS system binaries such as /bin/ls. The structural FAT parser names the same slice `arm64e`, so the exact name comparison failed and inspect_artifact reported a valid, unmodified universal binary as an integrity mismatch ("Artifact is invalid or has changed"). Accept `arm64e.` for an `arm64e` FAT slice. Offset, size, CPU type, subtype (including the pointer-auth version bits when lipo prints them) and alignment are still compared field by field. The slice keeps lipo's name in its logical path. Fixes #1168 Co-Authored-By: Claude Opus 5.5 --- src/artifacts/MachOSliceArtifactReader.ts | 11 ++- .../artifactProvider.machOSlices.test.ts | 69 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/artifacts/MachOSliceArtifactReader.ts b/src/artifacts/MachOSliceArtifactReader.ts index a242feaf4..fcda0b3d8 100644 --- a/src/artifacts/MachOSliceArtifactReader.ts +++ b/src/artifacts/MachOSliceArtifactReader.ts @@ -255,7 +255,7 @@ const lipoMatchesSlice = ( reported.cpu_subtype_code !== null; return ( (hasPhysicalIdentity || structurallyUnique) && - reported.name === observed.architecture && + lipoNameMatches(reported.name, observed.architecture) && (reported.file_offset === null || reported.file_offset === observed.slice_offset) && (reported.size === null || reported.size === observed.slice_size) && @@ -269,6 +269,15 @@ const lipoMatchesSlice = ( ); }; +/** + * Newer lipo names an arm64e ABI variant `arm64e.` (for example + * `arm64e.v1` for pointer-auth ABI version 1). The FAT table names that slice + * `arm64e`; its physical identity is still compared field by field. + */ +const lipoNameMatches = (reported: string, observed: string): boolean => + reported === observed || + (observed === "arm64e" && /^arm64e\.[A-Za-z0-9_]+$/u.test(reported)); + const parseSliceKeyInteger = (value: string | undefined): number | null => { if (value === undefined || !/^\d+$/u.test(value)) return null; const parsed = Number(value); diff --git a/tests/boundary/providers/native/artifactProvider.machOSlices.test.ts b/tests/boundary/providers/native/artifactProvider.machOSlices.test.ts index 2da3bb269..289301a74 100644 --- a/tests/boundary/providers/native/artifactProvider.machOSlices.test.ts +++ b/tests/boundary/providers/native/artifactProvider.machOSlices.test.ts @@ -186,3 +186,72 @@ const lipoRunner = (stdout: string): NativeCommandRunner => ({ }), ), }); + +// Newer lipo names arm64e ABI variants `arm64e.`; the FAT table names +// the same slice `arm64e`, and its physical identity must still match. +describe("artifact Mach-O arm64e variant slices", () => { + const ARM64E_V1 = { type: 0x0100000c, subtype: 0x81000002 } as const; + const variantSlices = async ( + cpu: { readonly type: number; readonly subtype: number }, + lipo: (size: number) => string, + ) => { + const root = await createTestTempDirectory("rea-slices-arm64e-"); + const binary = join(root, "fat"); + const x86 = machoImage({ cpu: CPU.x86_64 }); + const arm = machoImage({ cpu }); + await writeFile( + binary, + fatImage([ + { cpu: CPU.x86_64, bytes: x86 }, + { cpu, bytes: arm }, + ]), + ); + const reader = new MachOSliceArtifactReader( + binary, + lipoRunner( + `architecture x86_64\n cputype CPU_TYPE_X86_64\n cpusubtype CPU_SUBTYPE_X86_64_ALL\n offset 4096\n size ${String(x86.length)}\n align 2^12 (4096)\n${lipo(arm.length)}`, + ), + ); + const entries = []; + for await (const entry of reader.entries()) entries.push(entry); + return entries.map(({ path, byteOffset }) => [path, byteOffset]); + }; + + it.each([ + [ + "arm64e.v1", + " cputype CPU_TYPE_ARM64\n cpusubtype CPU_SUBTYPE_ARM64E\n capabilities PTR_AUTH_VERSION USERSPACE 1", + ], + [ + "arm64e.x1", + " cputype CPU_TYPE_ARM64\n cpusubtype unknown arm64 cpusubtype", + ], + ])("accepts lipo's %s name for the FAT arm64e slice", async (name, cpu) => { + expect( + await variantSlices( + ARM64E_V1, + (size) => + `architecture ${name}\n${cpu}\n offset 8192\n size ${String(size)}\n align 2^12 (4096)\n`, + ), + ).toEqual([ + ["slices/x86_64", 4096], + [`slices/${name}`, 8192], + ]); + }); + + it.each([ + ["a plain arm64 slice", CPU.arm64, "8192"], + ["a different offset", ARM64E_V1, "8193"], + ])( + "still rejects an arm64e variant name for %s", + async (_label, cpu, offset) => { + await expect( + variantSlices( + cpu, + (size) => + `architecture arm64e.v1\n cputype CPU_TYPE_ARM64\n cpusubtype unknown arm64 cpusubtype\n offset ${offset}\n size ${String(size)}\n align 2^12 (4096)\n`, + ), + ).rejects.toMatchObject({ reason: "integrity" }); + }, + ); +});