Add macOS headerpad fix for x64 binaries
Some checks reported errors
farmcontrol/farmcontrol-ui/pipeline/head Something is wrong with the build of this commit

- Introduced `fix-macos-headerpad.mjs` to address issues with Mach-O headerpad in x64 binaries, preventing corruption during code signing.
- Updated `ensure-electrobun-core.mjs` to call the new headerpad fix function, ensuring compatibility with macOS environments.
- This change resolves issue #485 by freeing load-command slots before signing binaries, enhancing stability on Intel Macs.
This commit is contained in:
Tom Butcher 2026-08-08 20:57:37 +01:00
parent c1e0921c3e
commit 21a88e00d0
2 changed files with 156 additions and 0 deletions

View File

@ -9,6 +9,7 @@ import { spawnSync } from "node:child_process";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { getReleaseArch } from "./release-artifact-utils.mjs"; import { getReleaseArch } from "./release-artifact-utils.mjs";
import { fixMacosHeaderpad } from "./fix-macos-headerpad.mjs";
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const electrobunDir = path.join(rootDir, "node_modules/electrobun"); const electrobunDir = path.join(rootDir, "node_modules/electrobun");
@ -124,4 +125,7 @@ const targetArch = getReleaseArch(
const archesToEnsure = new Set([hostArch, targetArch]); const archesToEnsure = new Set([hostArch, targetArch]);
for (const arch of archesToEnsure) { for (const arch of archesToEnsure) {
await ensureCoreDependencies(targetOS, arch); await ensureCoreDependencies(targetOS, arch);
// electrobun#485: x64 core binaries lack Mach-O headerpad and get corrupted
// by codesign; free a load-command slot before anything copies or signs them.
fixMacosHeaderpad(getPlatformPaths(targetOS, arch).platformDistDir);
} }

View File

@ -0,0 +1,152 @@
// Workaround for https://github.com/blackboardsh/electrobun/issues/485
//
// Electrobun's darwin-x64 core binaries (launcher, extractor, libasar.dylib,
// bsdiff, zig-asar in v1.18.1) are Zig-built with zero/near-zero Mach-O
// headerpad and no code signature. When codesign adds the 16-byte
// LC_CODE_SIGNATURE load command it silently overwrites the first bytes of
// __text, and the signed binary segfaults on launch (Intel Macs only;
// arm64 binaries always ship with a signature slot that is re-signed in
// place).
//
// This script frees room in the load-command area by dropping the 16-byte
// LC_SOURCE_VERSION command (LC_UUID as a fallback) from any thin x86_64
// Mach-O that has less than 16 bytes of headerpad and no existing code
// signature. It is idempotent and a no-op once upstream ships rebuilt
// binaries with headerpad_size set.
import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const MH_MAGIC_64 = 0xfeedfacf;
const CPU_TYPE_X86_64 = 0x01000007;
const LC_SEGMENT_64 = 0x19;
const LC_UUID = 0x1b;
const LC_CODE_SIGNATURE = 0x1d;
const LC_SOURCE_VERSION = 0x2a;
const HEADER_SIZE = 32;
const CODE_SIGNATURE_CMD_SIZE = 16;
function analyzeMachO(buffer) {
if (buffer.length < HEADER_SIZE || buffer.readUInt32LE(0) !== MH_MAGIC_64) {
return null;
}
const cputype = buffer.readInt32LE(4);
const ncmds = buffer.readUInt32LE(16);
const sizeofcmds = buffer.readUInt32LE(20);
let offset = HEADER_SIZE;
let firstSectionOffset = null;
let hasCodeSignature = false;
let sourceVersionCmd = null;
let uuidCmd = null;
for (let i = 0; i < ncmds; i += 1) {
const cmd = buffer.readUInt32LE(offset);
const cmdsize = buffer.readUInt32LE(offset + 4);
if (cmd === LC_CODE_SIGNATURE) {
hasCodeSignature = true;
} else if (cmd === LC_SOURCE_VERSION) {
sourceVersionCmd = { offset, cmdsize };
} else if (cmd === LC_UUID) {
uuidCmd = { offset, cmdsize };
} else if (cmd === LC_SEGMENT_64) {
const nsects = buffer.readUInt32LE(offset + 64);
let sectionOffset = offset + 72;
for (let s = 0; s < nsects; s += 1) {
const size = Number(buffer.readBigUInt64LE(sectionOffset + 40));
const fileOffset = buffer.readUInt32LE(sectionOffset + 48);
if (fileOffset > 0 && size > 0) {
firstSectionOffset =
firstSectionOffset === null
? fileOffset
: Math.min(firstSectionOffset, fileOffset);
}
sectionOffset += 80;
}
}
offset += cmdsize;
}
return {
cputype,
ncmds,
sizeofcmds,
headerpad:
firstSectionOffset === null
? Infinity
: firstSectionOffset - (HEADER_SIZE + sizeofcmds),
hasCodeSignature,
removableCmd: sourceVersionCmd ?? uuidCmd,
};
}
function fixFile(filePath) {
const buffer = readFileSync(filePath);
const info = analyzeMachO(buffer);
const name = path.basename(filePath);
if (!info || info.cputype !== CPU_TYPE_X86_64) {
return;
}
if (info.hasCodeSignature || info.headerpad >= CODE_SIGNATURE_CMD_SIZE) {
return;
}
if (!info.removableCmd) {
console.warn(
`fix-macos-headerpad: ${name} has headerpad ${info.headerpad} but no removable load command; signing may corrupt it`,
);
return;
}
const { offset, cmdsize } = info.removableCmd;
const loadCommandsEnd = HEADER_SIZE + info.sizeofcmds;
// Shift the remaining load commands over the removed one, then zero the
// freed tail so codesign finds clean padding.
buffer.copyWithin(offset, offset + cmdsize, loadCommandsEnd);
buffer.fill(0, loadCommandsEnd - cmdsize, loadCommandsEnd);
buffer.writeUInt32LE(info.ncmds - 1, 16);
buffer.writeUInt32LE(info.sizeofcmds - cmdsize, 20);
writeFileSync(filePath, buffer);
console.log(
`fix-macos-headerpad: ${name} freed ${cmdsize} bytes for LC_CODE_SIGNATURE (headerpad was ${info.headerpad})`,
);
}
export function fixMacosHeaderpad(directory) {
if (process.platform !== "darwin") {
return;
}
let entries;
try {
entries = readdirSync(directory);
} catch {
return;
}
for (const entry of entries) {
const filePath = path.join(directory, entry);
if (statSync(filePath).isFile()) {
fixFile(filePath);
}
}
}
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : null;
const modulePath = fileURLToPath(import.meta.url);
if (invokedPath === modulePath) {
const rootDir = path.resolve(path.dirname(modulePath), "..");
const target =
process.argv[2] ||
path.join(rootDir, "node_modules/electrobun/dist-macos-x64");
fixMacosHeaderpad(path.resolve(target));
}