Skip to content

Commit 68bca43

Browse files
authored
Merge pull request #887 from webadderallorg/codex/fix-macos-verifier
Fix macOS release verification for universal binaries
2 parents 264829a + ec456ef commit 68bca43

3 files changed

Lines changed: 44 additions & 18 deletions

File tree

electron/macosDistributionPolicy.test.mjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
collectCodeSigningMetadataErrors,
66
collectEntitlementErrors,
77
expectedMachOArchitecture,
8+
hasMachOMagic,
89
parseLipoArchitectures,
910
} from "../scripts/macos-distribution-policy.mjs";
1011

@@ -95,6 +96,20 @@ describe("macOS distribution entitlement policy", () => {
9596
});
9697

9798
describe("macOS distribution architecture policy", () => {
99+
it("recognizes thin and universal Mach-O magic bytes without parsing file output", () => {
100+
for (const header of [
101+
[0xfe, 0xed, 0xfa, 0xce],
102+
[0xcf, 0xfa, 0xed, 0xfe],
103+
[0xca, 0xfe, 0xba, 0xbe],
104+
[0xbf, 0xba, 0xfe, 0xca],
105+
]) {
106+
expect(hasMachOMagic(Uint8Array.from(header))).toBe(true);
107+
}
108+
109+
expect(hasMachOMagic(Uint8Array.from([0x7f, 0x45, 0x4c, 0x46]))).toBe(false);
110+
expect(hasMachOMagic(Uint8Array.from([0xfe, 0xed, 0xfa]))).toBe(false);
111+
});
112+
98113
it("parses thin and fat lipo output", () => {
99114
expect(parseLipoArchitectures("Non-fat file: App is architecture: arm64")).toEqual([
100115
"arm64",

scripts/macos-distribution-policy.mjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
const TEAM_ID_PATTERN = /^[A-Z0-9]{10}$/;
22

3+
const MACH_O_MAGIC_NUMBERS = new Set([
4+
0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe, 0xcafebabe, 0xcafebabf, 0xbebafeca, 0xbfbafeca,
5+
]);
6+
37
export const REQUIRED_MACOS_ENTITLEMENTS = Object.freeze([
48
"com.apple.security.cs.allow-jit",
59
"com.apple.security.cs.allow-unsigned-executable-memory",
@@ -25,6 +29,15 @@ export function assertValidAppleTeamId(teamId) {
2529
}
2630
}
2731

32+
export function hasMachOMagic(header) {
33+
if (!(header instanceof Uint8Array) || header.byteLength < 4) {
34+
return false;
35+
}
36+
37+
const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
38+
return MACH_O_MAGIC_NUMBERS.has(view.getUint32(0, false));
39+
}
40+
2841
export function collectCodeSigningMetadataErrors(details, expectedTeamId) {
2942
const errors = [];
3043
const authorities = details

scripts/verify-macos-distribution.mjs

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
import { spawnSync } from "node:child_process";
44
import {
5+
closeSync,
56
existsSync,
67
lstatSync,
78
mkdtempSync,
9+
openSync,
810
readdirSync,
911
readFileSync,
12+
readSync,
1013
rmSync,
1114
statSync,
1215
writeFileSync,
@@ -19,14 +22,14 @@ import {
1922
collectArchitectureErrors,
2023
collectCodeSigningMetadataErrors,
2124
collectEntitlementErrors,
25+
hasMachOMagic,
2226
} from "./macos-distribution-policy.mjs";
2327

2428
const projectRoot = process.cwd();
2529
const packageJson = JSON.parse(readFileSync(path.join(projectRoot, "package.json"), "utf8"));
2630
const productName = packageJson.productName ?? packageJson.name ?? "Recordly";
2731
const expectedBundleId = "dev.recordly.app";
2832
const commandTimeoutMs = 5 * 60 * 1000;
29-
const fileClassificationBatchSize = 100;
3033
const maxReportDetailLength = 4_000;
3134

3235
function parseArguments(argv) {
@@ -174,6 +177,17 @@ function walkRegularFiles(rootPath) {
174177
return files;
175178
}
176179

180+
function isMachOBinary(filePath) {
181+
const header = new Uint8Array(4);
182+
const descriptor = openSync(filePath, "r");
183+
try {
184+
const bytesRead = readSync(descriptor, header, 0, header.byteLength, 0);
185+
return bytesRead === header.byteLength && hasMachOMagic(header);
186+
} finally {
187+
closeSync(descriptor);
188+
}
189+
}
190+
177191
function extractPlist(commandResult) {
178192
const output = [commandResult.stdout, commandResult.stderr].filter(Boolean).join("\n");
179193
const xmlStart = output.indexOf("<?xml");
@@ -281,23 +295,7 @@ function verifyEntitlements(appPath, label, tempRoot, check) {
281295

282296
function verifyMachOBinaries(appPath, arch, check) {
283297
check("packaged app: nested Mach-O signatures and architectures", () => {
284-
const machOBinaries = [];
285-
const regularFiles = walkRegularFiles(appPath);
286-
for (let index = 0; index < regularFiles.length; index += fileClassificationBatchSize) {
287-
const batch = regularFiles.slice(index, index + fileClassificationBatchSize);
288-
const fileTypes = runProcess("file", ["-b", ...batch]).stdout.split(/\r?\n/);
289-
if (fileTypes.length !== batch.length) {
290-
throw new Error(
291-
`file classification returned ${fileTypes.length} rows for ${batch.length} paths`,
292-
);
293-
}
294-
295-
for (let batchIndex = 0; batchIndex < batch.length; batchIndex += 1) {
296-
if (fileTypes[batchIndex].includes("Mach-O")) {
297-
machOBinaries.push(batch[batchIndex]);
298-
}
299-
}
300-
}
298+
const machOBinaries = walkRegularFiles(appPath).filter(isMachOBinary);
301299

302300
if (machOBinaries.length === 0) {
303301
throw new Error("no Mach-O binaries were found in the app bundle");

0 commit comments

Comments
 (0)