diff --git a/scripts/ensure-electrobun-core.mjs b/scripts/ensure-electrobun-core.mjs index 359a899..1881010 100644 --- a/scripts/ensure-electrobun-core.mjs +++ b/scripts/ensure-electrobun-core.mjs @@ -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); } diff --git a/scripts/fix-macos-headerpad.mjs b/scripts/fix-macos-headerpad.mjs new file mode 100644 index 0000000..cc673c3 --- /dev/null +++ b/scripts/fix-macos-headerpad.mjs @@ -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)); +}