// 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)); }