From ec456eff2a5823a053d810fa087e6d23b1b66d20 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Sat, 5 Sep 2026 20:04:51 +1000 Subject: [PATCH] fix: verify universal macOS binaries reliably --- electron/macosDistributionPolicy.test.mjs | 15 ++++++++++ scripts/macos-distribution-policy.mjs | 13 +++++++++ scripts/verify-macos-distribution.mjs | 34 +++++++++++------------ 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/electron/macosDistributionPolicy.test.mjs b/electron/macosDistributionPolicy.test.mjs index bd744802c..8fd6f05cd 100644 --- a/electron/macosDistributionPolicy.test.mjs +++ b/electron/macosDistributionPolicy.test.mjs @@ -5,6 +5,7 @@ import { collectCodeSigningMetadataErrors, collectEntitlementErrors, expectedMachOArchitecture, + hasMachOMagic, parseLipoArchitectures, } from "../scripts/macos-distribution-policy.mjs"; @@ -95,6 +96,20 @@ describe("macOS distribution entitlement policy", () => { }); describe("macOS distribution architecture policy", () => { + it("recognizes thin and universal Mach-O magic bytes without parsing file output", () => { + for (const header of [ + [0xfe, 0xed, 0xfa, 0xce], + [0xcf, 0xfa, 0xed, 0xfe], + [0xca, 0xfe, 0xba, 0xbe], + [0xbf, 0xba, 0xfe, 0xca], + ]) { + expect(hasMachOMagic(Uint8Array.from(header))).toBe(true); + } + + expect(hasMachOMagic(Uint8Array.from([0x7f, 0x45, 0x4c, 0x46]))).toBe(false); + expect(hasMachOMagic(Uint8Array.from([0xfe, 0xed, 0xfa]))).toBe(false); + }); + it("parses thin and fat lipo output", () => { expect(parseLipoArchitectures("Non-fat file: App is architecture: arm64")).toEqual([ "arm64", diff --git a/scripts/macos-distribution-policy.mjs b/scripts/macos-distribution-policy.mjs index 5a6a4db90..b281288f9 100644 --- a/scripts/macos-distribution-policy.mjs +++ b/scripts/macos-distribution-policy.mjs @@ -1,5 +1,9 @@ const TEAM_ID_PATTERN = /^[A-Z0-9]{10}$/; +const MACH_O_MAGIC_NUMBERS = new Set([ + 0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe, 0xcafebabe, 0xcafebabf, 0xbebafeca, 0xbfbafeca, +]); + export const REQUIRED_MACOS_ENTITLEMENTS = Object.freeze([ "com.apple.security.cs.allow-jit", "com.apple.security.cs.allow-unsigned-executable-memory", @@ -25,6 +29,15 @@ export function assertValidAppleTeamId(teamId) { } } +export function hasMachOMagic(header) { + if (!(header instanceof Uint8Array) || header.byteLength < 4) { + return false; + } + + const view = new DataView(header.buffer, header.byteOffset, header.byteLength); + return MACH_O_MAGIC_NUMBERS.has(view.getUint32(0, false)); +} + export function collectCodeSigningMetadataErrors(details, expectedTeamId) { const errors = []; const authorities = details diff --git a/scripts/verify-macos-distribution.mjs b/scripts/verify-macos-distribution.mjs index 0bd85e6e2..bd8e4bbcf 100644 --- a/scripts/verify-macos-distribution.mjs +++ b/scripts/verify-macos-distribution.mjs @@ -2,11 +2,14 @@ import { spawnSync } from "node:child_process"; import { + closeSync, existsSync, lstatSync, mkdtempSync, + openSync, readdirSync, readFileSync, + readSync, rmSync, statSync, writeFileSync, @@ -19,6 +22,7 @@ import { collectArchitectureErrors, collectCodeSigningMetadataErrors, collectEntitlementErrors, + hasMachOMagic, } from "./macos-distribution-policy.mjs"; const projectRoot = process.cwd(); @@ -26,7 +30,6 @@ const packageJson = JSON.parse(readFileSync(path.join(projectRoot, "package.json const productName = packageJson.productName ?? packageJson.name ?? "Recordly"; const expectedBundleId = "dev.recordly.app"; const commandTimeoutMs = 5 * 60 * 1000; -const fileClassificationBatchSize = 100; const maxReportDetailLength = 4_000; function parseArguments(argv) { @@ -174,6 +177,17 @@ function walkRegularFiles(rootPath) { return files; } +function isMachOBinary(filePath) { + const header = new Uint8Array(4); + const descriptor = openSync(filePath, "r"); + try { + const bytesRead = readSync(descriptor, header, 0, header.byteLength, 0); + return bytesRead === header.byteLength && hasMachOMagic(header); + } finally { + closeSync(descriptor); + } +} + function extractPlist(commandResult) { const output = [commandResult.stdout, commandResult.stderr].filter(Boolean).join("\n"); const xmlStart = output.indexOf(" { - const machOBinaries = []; - const regularFiles = walkRegularFiles(appPath); - for (let index = 0; index < regularFiles.length; index += fileClassificationBatchSize) { - const batch = regularFiles.slice(index, index + fileClassificationBatchSize); - const fileTypes = runProcess("file", ["-b", ...batch]).stdout.split(/\r?\n/); - if (fileTypes.length !== batch.length) { - throw new Error( - `file classification returned ${fileTypes.length} rows for ${batch.length} paths`, - ); - } - - for (let batchIndex = 0; batchIndex < batch.length; batchIndex += 1) { - if (fileTypes[batchIndex].includes("Mach-O")) { - machOBinaries.push(batch[batchIndex]); - } - } - } + const machOBinaries = walkRegularFiles(appPath).filter(isMachOBinary); if (machOBinaries.length === 0) { throw new Error("no Mach-O binaries were found in the app bundle");