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
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:
parent
c1e0921c3e
commit
21a88e00d0
@ -9,6 +9,7 @@ import { spawnSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
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 electrobunDir = path.join(rootDir, "node_modules/electrobun");
|
||||
@ -124,4 +125,7 @@ const targetArch = getReleaseArch(
|
||||
const archesToEnsure = new Set([hostArch, targetArch]);
|
||||
for (const arch of archesToEnsure) {
|
||||
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);
|
||||
}
|
||||
|
||||
152
scripts/fix-macos-headerpad.mjs
Normal file
152
scripts/fix-macos-headerpad.mjs
Normal 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));
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user