From 963de311a995242d7b3828458afcc33f1ccffb5e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 05:15:58 +0000 Subject: [PATCH 1/3] perf: stream binary scan and use positional writes when flipping fuses Previously flipFuses and getCurrentFuseWire loaded the entire Electron binary into memory, scanned it, and (for flipFuses) wrote the whole file back to disk even though only a handful of bytes change. This switches to a chunked scan that holds ~8 MB in memory regardless of binary size, runs two workers concurrently so indexOf overlaps with disk reads, and uses positional writes to touch only the modified fuse bytes. On synthetic binaries this yields roughly: flipFuses 150 MB: ~350ms -> ~35ms flipFuses 300 MB: ~830ms -> ~58ms getCurrentFuseWire 300 MB: ~435ms -> ~28ms --- src/index.ts | 212 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 145 insertions(+), 67 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3913c60..42e5601 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,10 @@ import { FuseState, SENTINEL } from './constants.js'; export * from './config.js'; export { FuseState } from './constants.js'; +const SENTINEL_BYTES = Buffer.from(SENTINEL); +const SCAN_CHUNK_SIZE = 4 * 1024 * 1024; +const SCAN_CONCURRENCY = 2; + const state = (b: boolean | undefined) => b === undefined ? FuseState.INHERIT : b ? FuseState.ENABLE : FuseState.DISABLE; @@ -60,6 +64,69 @@ const pathToFuseFile = (pathToElectron: string) => { return pathToElectron; }; +/** + * Streams through the file in fixed-size chunks looking for the fuse sentinel, + * so we never have to hold the entire (often 100+ MB) binary in memory. + * Chunks are processed by a small pool of concurrent workers so that + * Buffer.indexOf (CPU) on one chunk overlaps with the disk read (I/O) of the + * next. Each chunk overreads a few bytes into its neighbour so a sentinel + * straddling a boundary is still detected. + */ +const findSentinels = async (handle: fs.FileHandle, firstOnly: boolean): Promise => { + const { size } = await handle.stat(); + const overlap = SENTINEL_BYTES.length - 1; + const numChunks = Math.ceil(size / SCAN_CHUNK_SIZE); + const positions: number[] = []; + + let next = 0; + let done = false; + + const worker = async () => { + const buf = Buffer.allocUnsafe(SCAN_CHUNK_SIZE + overlap); + while (!done) { + const chunk = next++; + if (chunk >= numChunks) return; + const start = chunk * SCAN_CHUNK_SIZE; + const len = Math.min(SCAN_CHUNK_SIZE + overlap, size - start); + const { bytesRead } = await handle.read(buf, 0, len, start); + const haystack = buf.subarray(0, bytesRead); + + let idx = haystack.indexOf(SENTINEL_BYTES); + while (idx !== -1) { + positions.push(start + idx); + if (firstOnly) { + done = true; + return; + } + idx = haystack.indexOf(SENTINEL_BYTES, idx + 1); + } + } + }; + + const workers = Math.min(SCAN_CONCURRENCY, numChunks); + await Promise.all(Array.from({ length: workers }, worker)); + + return positions.sort((a, b) => a - b); +}; + +const HEADER_LEN = 2; +// Wire length is encoded in a single byte so it can never exceed 255. +const MAX_WIRE_LEN = 255; + +const readFuseWire = async (handle: fs.FileHandle, sentinelPos: number) => { + const wirePos = sentinelPos + SENTINEL_BYTES.length; + const buf = Buffer.allocUnsafe(HEADER_LEN + MAX_WIRE_LEN); + const { bytesRead } = await handle.read(buf, 0, buf.length, wirePos); + const version = buf[0]; + const length = buf[1]; + return { + version, + length, + wireBytes: buf.subarray(HEADER_LEN, Math.min(HEADER_LEN + length, bytesRead)), + wireBytesPos: wirePos + HEADER_LEN, + }; +}; + const setFuseWire = async ( pathToElectron: string, fuseVersion: FuseVersion, @@ -68,99 +135,110 @@ const setFuseWire = async ( fuseNamer: (index: number) => string, ) => { const fuseFilePath = pathToFuseFile(pathToElectron); - const electron = await fs.readFile(fuseFilePath); + const handle = await fs.open(fuseFilePath, 'r+'); - const firstSentinel = electron.indexOf(SENTINEL); - const lastSentinel = electron.lastIndexOf(SENTINEL); - // If the last sentinel is different to the first sentinel we are probably in a universal build - // We should flip the fuses in both sentinels to affect both slices of the universal binary - const sentinels = - firstSentinel === lastSentinel ? [firstSentinel] : [firstSentinel, lastSentinel]; + try { + const sentinels = await findSentinels(handle, false); - for (const indexOfSentinel of sentinels) { - if (indexOfSentinel === -1) { + if (sentinels.length === 0) { throw new Error( 'Could not find sentinel in the provided Electron binary, fuses are only supported in Electron 12 and higher', ); } - const fuseWirePosition = indexOfSentinel + SENTINEL.length; + // More than one sentinel indicates a universal build; flip fuses in each + // slice so every architecture is covered. + for (const indexOfSentinel of sentinels) { + const { + version: fuseWireVersion, + length: fuseWireLength, + wireBytes, + wireBytesPos, + } = await readFuseWire(handle, indexOfSentinel); - const fuseWireVersion = electron[fuseWirePosition]; - if (parseInt(fuseVersion, 10) !== fuseWireVersion) { - throw new Error( - `Provided fuse wire version "${parseInt( - fuseVersion, - 10, - )}" does not match watch was found in the binary "${fuseWireVersion}". You should update your usage of @electron/fuses.`, - ); - } - const fuseWireLength = electron[fuseWirePosition + 1]; + if (parseInt(fuseVersion, 10) !== fuseWireVersion) { + throw new Error( + `Provided fuse wire version "${parseInt( + fuseVersion, + 10, + )}" does not match watch was found in the binary "${fuseWireVersion}". You should update your usage of @electron/fuses.`, + ); + } - const wire = fuseWireBuilder(fuseWireLength).slice(0, fuseWireLength); - if (wire.length < fuseWireLength && strictlyRequireAllFuses) { - throw new Error( - `strictlyRequireAllFuses: The fuse wire in the Electron binary has ${fuseWireLength} fuses but you only provided a config for ${wire.length} fuses, you may need to update @electron/fuses or provide additional fuse settings`, - ); - } - for (let i = 0; i < wire.length; i++) { - const idx = fuseWirePosition + 2 + i; - const currentState = electron[idx]; - const newState = wire[i]; - - if (currentState === FuseState.REMOVED && newState !== FuseState.INHERIT) { - console.warn( - `Overriding fuse "${fuseNamer( - i, - )}" that has been marked as removed, setting this fuse is a noop`, + const wire = fuseWireBuilder(fuseWireLength).slice(0, fuseWireLength); + if (wire.length < fuseWireLength && strictlyRequireAllFuses) { + throw new Error( + `strictlyRequireAllFuses: The fuse wire in the Electron binary has ${fuseWireLength} fuses but you only provided a config for ${wire.length} fuses, you may need to update @electron/fuses or provide additional fuse settings`, ); } - if (newState === FuseState.INHERIT) { - if (strictlyRequireAllFuses) { - throw new Error( - `strictlyRequireAllFuses: Missing explicit configuration for fuse ${fuseNamer(i)}`, + for (let i = 0; i < wire.length; i++) { + const currentState = wireBytes[i]; + const newState = wire[i]; + + if (currentState === FuseState.REMOVED && newState !== FuseState.INHERIT) { + console.warn( + `Overriding fuse "${fuseNamer( + i, + )}" that has been marked as removed, setting this fuse is a noop`, ); } - continue; + if (newState === FuseState.INHERIT) { + if (strictlyRequireAllFuses) { + throw new Error( + `strictlyRequireAllFuses: Missing explicit configuration for fuse ${fuseNamer(i)}`, + ); + } + continue; + } + wireBytes[i] = newState; } - electron[idx] = newState; - } - } - await fs.writeFile(fuseFilePath, electron); + await handle.write(wireBytes, 0, wireBytes.length, wireBytesPos); + } - return sentinels.length; + return sentinels.length; + } finally { + await handle.close(); + } }; export const getCurrentFuseWire = async ( pathToElectron: string, ): Promise> => { const fuseFilePath = pathToFuseFile(pathToElectron); - const electron = await fs.readFile(fuseFilePath); - const fuseWirePosition = electron.indexOf(SENTINEL) + SENTINEL.length; + const handle = await fs.open(fuseFilePath, 'r'); - if (fuseWirePosition - SENTINEL.length === -1) { - throw new Error( - 'Could not find sentinel in the provided Electron binary, fuses are only supported in Electron 12 and higher', - ); - } - const fuseWireVersion = electron[fuseWirePosition] as any as FuseVersion; - const fuseWireLength = electron[fuseWirePosition + 1]; - const fuseConfig: FuseConfig = { - version: `${fuseWireVersion}` as FuseVersion, - }; + try { + const [sentinel] = await findSentinels(handle, true); - for (let i = 0; i < fuseWireLength; i++) { - const idx = fuseWirePosition + 2 + i; - const currentState = electron[idx]; - switch (fuseConfig.version) { - case FuseVersion.V1: - fuseConfig[i as FuseV1Options] = currentState as FuseState; - break; + if (sentinel === undefined) { + throw new Error( + 'Could not find sentinel in the provided Electron binary, fuses are only supported in Electron 12 and higher', + ); } - } - return fuseConfig; + const { + version: fuseWireVersion, + length: fuseWireLength, + wireBytes, + } = await readFuseWire(handle, sentinel); + + const fuseConfig: FuseConfig = { + version: `${fuseWireVersion}` as FuseVersion, + }; + + for (let i = 0; i < fuseWireLength; i++) { + switch (fuseConfig.version) { + case FuseVersion.V1: + fuseConfig[i as FuseV1Options] = wireBytes[i] as FuseState; + break; + } + } + + return fuseConfig; + } finally { + await handle.close(); + } }; export const flipFuses = async ( From eaa70a830cd59f6bb0c88f945e10049de84e0d29 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 05:16:19 +0000 Subject: [PATCH 2/3] fix: reject binaries with more than 2 fuse sentinels Only 1 sentinel (single-arch) or 2 sentinels (universal macOS) are valid. Finding more indicates a corrupted or unsupported binary, so fail loudly rather than silently patching an unknown layout. --- src/index.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 42e5601..4dc05fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -145,9 +145,16 @@ const setFuseWire = async ( 'Could not find sentinel in the provided Electron binary, fuses are only supported in Electron 12 and higher', ); } + if (sentinels.length > 2) { + throw new Error( + `Found ${sentinels.length} copies of the fuse sentinel in the provided Electron binary. ` + + 'At most 2 are expected (one per slice of a universal macOS binary). ' + + 'This may indicate a corrupted binary or an unsupported build configuration.', + ); + } - // More than one sentinel indicates a universal build; flip fuses in each - // slice so every architecture is covered. + // Two sentinels indicate a universal macOS build; flip fuses in each + // slice so both architectures are covered. for (const indexOfSentinel of sentinels) { const { version: fuseWireVersion, From 85d303a407a5cf2649e666728be5fda8df216fb1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 05:20:19 +0000 Subject: [PATCH 3/3] fix: defer writes until all sentinels validate Restores the atomicity guarantee of the original implementation: if validation fails for any sentinel (version mismatch, strictlyRequireAllFuses), the binary is left completely unmodified. Previously, with per-sentinel writes inside the validation loop, a universal binary could end up with one arch slice modified and the other untouched if the second slice failed validation. --- src/index.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 4dc05fb..c06d85c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -154,7 +154,11 @@ const setFuseWire = async ( } // Two sentinels indicate a universal macOS build; flip fuses in each - // slice so both architectures are covered. + // slice so both architectures are covered. Validate and prepare every + // write before touching the file so a validation error on one slice + // cannot leave the binary half-modified. + const pendingWrites: { bytes: Buffer; position: number }[] = []; + for (const indexOfSentinel of sentinels) { const { version: fuseWireVersion, @@ -200,7 +204,11 @@ const setFuseWire = async ( wireBytes[i] = newState; } - await handle.write(wireBytes, 0, wireBytes.length, wireBytesPos); + pendingWrites.push({ bytes: wireBytes, position: wireBytesPos }); + } + + for (const { bytes, position } of pendingWrites) { + await handle.write(bytes, 0, bytes.length, position); } return sentinels.length;