fix: verify universal macOS binaries reliably

This commit is contained in:
webadderall
2026-09-05 20:04:51 +10:00
parent 264829ac28
commit ec456eff2a
3 changed files with 44 additions and 18 deletions
+15
View File
@@ -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",
+13
View File
@@ -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
+16 -18
View File
@@ -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("<?xml");
@@ -281,23 +295,7 @@ function verifyEntitlements(appPath, label, tempRoot, check) {
function verifyMachOBinaries(appPath, arch, check) {
check("packaged app: nested Mach-O signatures and architectures", () => {
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");