import { observeMzWindowsHeader } from "../domain/dosMz.js"; import { readPeCoffHeader } from "../domain/peCoffHeader.js"; import { readElfFixedHeader } from "../domain/elfFixedHeader.js"; import { decodePeMachine } from "../domain/peMachine.js"; import type { ResolvedElectronFuseFile } from "./ElectronFusePath.js"; import { createHash } from "node:crypto"; import { lstat, type FileHandle } from "node:fs/promises"; import { readMachoImage } from "../artifacts/apple/MachoLoadCommandReader.js"; import { readFileHandleRange } from "../filesystem/readFileHandleRange.js"; import { hasMachoMagic, readFatHeaderPrefix, } from "../domain/apple/machoContainer.js"; import type { ReadAt } from "../artifacts/apple/MachoLoadCommandReader.js"; import { electronFuseName, electronFuseState, ELECTRON_FUSE_SENTINEL, type ElectronFuseSentinel, type ElectronFuseSlice, } from "../domain/electron/electronFuses.js"; import { machoImageSlices } from "../domain/apple/dylibResolution.js"; import { RegularFileChangedError, sameRegularFileState, } from "../filesystem/RegularFile.js"; import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { assertArtifactReadActive, withArtifactRegularFile, } from "../artifacts/ArtifactFileOperation.js"; /** Admission and search bounds for one Electron executable. */ export const ELECTRON_FUSE_LIMITS = { maximumFileBytes: 1024 * 1024 * 1024, chunkBytes: 256 * 1024, maximumSentinelsPerSlice: 32, maximumPeHeaderOffset: 1024 * 1024, } as const; const SENTINEL = Buffer.from(ELECTRON_FUSE_SENTINEL, "ascii"); /** Container identity and per-slice fuse wires. Offsets are file-absolute. */ export interface ElectronFuseImage { readonly sha256: string; readonly bytes: number; readonly format: "mach-o" | "mach-o-universal" | "pe" | "elf"; readonly slices: readonly ElectronFuseSlice[]; } type ElectronFuseContainerFormat = ElectronFuseImage["format"]; interface SliceRange { readonly index: number; readonly fileOffset: number; readonly size: number; readonly architecture: string | null; readonly hidden?: boolean; } /** The selected container is malformed or outside the supported formats. */ export class ElectronFuseFormatFailure extends Error { constructor( readonly kind: "malformed" | "unsupported", message: string, ) { super(message); this.name = "ElectronFuseFormatFailure"; } } /** A measured bound was hit before a complete fuse report could be returned. */ export class ElectronFuseResourceFailure extends Error { constructor( readonly boundary: "file-size" | "capacity", message: string, ) { super(message); this.name = "ElectronFuseResourceFailure"; } } /** * Hash one admitted regular file and search each slice for the fuse sentinel. * The search keeps only a sentinel-sized overlap between chunks. */ export const readElectronFuseImage = async ( path: string, options?: { readonly signal?: AbortSignal; readonly chunkBytes?: number; readonly selection?: ResolvedElectronFuseFile; readonly resources?: ArtifactResourceScope; }, ): Promise => { const signal = options?.signal; assertArtifactReadActive(signal); const selection = options?.selection; const recheckPath = async (candidate: string) => { assertArtifactReadActive(signal); try { const metadata = await lstat(candidate, { bigint: true }); assertArtifactReadActive(signal); return metadata; } catch (cause: unknown) { if (selection !== undefined && isMissingPath(cause)) throw new RegularFileChangedError(path); throw cause; } }; const assertPathNodes = async (): Promise => { if (selection === undefined) return; if (selection.path !== path) throw new RegularFileChangedError(path); for (const node of selection.nodes) { const current = await recheckPath(node.path); if ( current.dev !== node.metadata.dev || current.ino !== node.metadata.ino || current.isDirectory() !== node.metadata.isDirectory() || current.isSymbolicLink() !== node.metadata.isSymbolicLink() ) throw new RegularFileChangedError(path); } }; await assertPathNodes(); const before = await recheckPath(path); if ( selection !== undefined && !sameRegularFileState(selection.metadata, before) ) throw new RegularFileChangedError(path); if (!before.isFile() || before.isSymbolicLink()) throw new ElectronFuseFormatFailure( "unsupported", `Expected a regular file without a symlink: ${path}`, ); if (before.size > BigInt(ELECTRON_FUSE_LIMITS.maximumFileBytes)) throw new ElectronFuseResourceFailure( "file-size", `Selected artifact exceeds ${ELECTRON_FUSE_LIMITS.maximumFileBytes} bytes: ${path}`, ); const size = Number(before.size); const chunkBytes = options?.chunkBytes ?? ELECTRON_FUSE_LIMITS.chunkBytes; if ( !Number.isSafeInteger(chunkBytes) || chunkBytes < 1 || chunkBytes > ELECTRON_FUSE_LIMITS.chunkBytes ) throw new TypeError( `chunkBytes must be an integer between 1 and ${ELECTRON_FUSE_LIMITS.chunkBytes}.`, ); return withArtifactRegularFile( path, options?.resources ?? new ArtifactResourceScope(), signal, async ({ handle: owned }) => { const opened = await owned.stat({ bigint: true }); if (!sameRegularFileState(before, opened)) throw new RegularFileChangedError(path); await assertPathNodes(); const readAt = readerFor(owned, size, path, signal); const container = await identifyContainer(readAt, size, path); const scanned = await scanSlices( owned, path, size, container.slices, chunkBytes, signal, ); const current = await recheckPath(path); if (!sameRegularFileState(before, current)) throw new RegularFileChangedError(path); await assertPathNodes(); return { sha256: scanned.sha256, bytes: size, format: container.format, slices: scanned.slices, }; }, ); }; /** Sentence describing the file and sentinel bounds enforced by this reader. */ export const electronFuseReadLimitation = (): string => `The selected file is admitted as one regular file of at most 1 GiB. Sentinel search uses ${ELECTRON_FUSE_LIMITS.chunkBytes}-byte windows, and each slice keeps at most ${ELECTRON_FUSE_LIMITS.maximumSentinelsPerSlice} sentinels.`; const readerFor = ( handle: FileHandle, size: number, path: string, signal: AbortSignal | undefined, ): ReadAt => async (offset, length) => { assertArtifactReadActive(signal); if (length <= 0 || offset < 0 || offset >= size) return new Uint8Array(); const bounded = Math.min(length, size - offset); const buffer = await readFileHandleRange(handle, offset, bounded, () => assertArtifactReadActive(signal), ); if (buffer.length !== bounded) throw new RegularFileChangedError(path); return buffer; }; const identifyContainer = async ( readAt: ReadAt, size: number, path: string, ): Promise<{ readonly format: ElectronFuseContainerFormat; readonly slices: readonly SliceRange[]; }> => { const head = await readAt(0, Math.min(size, 8)); if (hasMachoMagic(head)) return identifyMacho(readAt, size, head, path); if (isElf(head)) { const header = await readAt(0, 64); const parsed = readElfFixedHeader(Buffer.from(header), size); return { format: "elf", slices: [ { index: 0, fileOffset: 0, size, architecture: parsed.ok ? parsed.value.architecture : null, }, ], }; } const pe = await identifyPe(readAt, size); if (pe !== undefined) return { format: "pe", slices: [ { index: 0, fileOffset: 0, size, architecture: pe.architecture }, ], }; throw new ElectronFuseFormatFailure( "unsupported", `Selected artifact ${path} does not contain a Mach-O, ELF, or PE container.`, ); }; const identifyMacho = async ( readAt: ReadAt, size: number, head: Uint8Array, path: string, ): Promise<{ readonly format: ElectronFuseContainerFormat; readonly slices: readonly SliceRange[]; }> => { const facts = await readMachoImage(readAt, size); if (facts.status !== "parsed") { const reason = facts.status === "not-mach-o" ? `Selected artifact ${path} does not contain a Mach-O, ELF, or PE container.` : facts.reason; throw new ElectronFuseFormatFailure( facts.status === "malformed" ? "malformed" : "unsupported", reason, ); } const fat = readFatHeaderPrefix(head); const format = fat.state === "parsed" || fat.state === "incomplete" ? "mach-o-universal" : "mach-o"; const slices = machoImageSlices(facts).map((slice, index) => ({ index, fileOffset: slice.slice_offset, size: slice.slice_size, architecture: slice.architecture, ...(slice.hidden === undefined ? {} : { hidden: slice.hidden }), })); return { format, slices, }; }; const scanSlices = async ( handle: FileHandle, path: string, size: number, ranges: readonly SliceRange[], chunkBytes: number, signal: AbortSignal | undefined, ): Promise<{ readonly sha256: string; readonly slices: readonly ElectronFuseSlice[]; }> => { const states = ranges.map((slice) => ({ slice, carry: Buffer.alloc(0), hits: [] as number[], })); const hash = createHash("sha256"); const chunk = Buffer.allocUnsafe(chunkBytes); let position = 0; while (position < size) { assertArtifactReadActive(signal); const length = Math.min(chunkBytes, size - position); const read = await handle.read(chunk, 0, length, position); if (read.bytesRead === 0) throw new RegularFileChangedError(path); assertArtifactReadActive(signal); const piece = chunk.subarray(0, read.bytesRead); hash.update(piece); absorbChunk(states, piece, position); position += read.bytesRead; } if (position !== size) throw new RegularFileChangedError(path); const slices: ElectronFuseSlice[] = []; for (const state of states) { const sentinels: ElectronFuseSentinel[] = []; for (const offset of state.hits) sentinels.push( await parseWire( handle, path, offset, state.slice.fileOffset + state.slice.size, signal, ), ); slices.push({ index: state.slice.index, file_offset: state.slice.fileOffset, size: state.slice.size, architecture: state.slice.architecture, ...(state.slice.hidden === undefined ? {} : { hidden: state.slice.hidden }), sentinel_count: sentinels.length, sentinels, }); } return { sha256: hash.digest("hex"), slices }; }; const absorbChunk = ( states: readonly { readonly slice: SliceRange; carry: Buffer; readonly hits: number[]; }[], chunk: Buffer, position: number, ): void => { const chunkEnd = position + chunk.length; for (const state of states) { const start = Math.max(position, state.slice.fileOffset); const end = Math.min(chunkEnd, state.slice.fileOffset + state.slice.size); if (start >= end) continue; const piece = chunk.subarray(start - position, end - position); const carryLength = state.carry.length; const window = carryLength === 0 ? piece : Buffer.concat([state.carry, piece]); const windowFileStart = start - carryLength; let from = 0; while (from <= window.length - SENTINEL.length) { const found = window.indexOf(SENTINEL, from); if (found < 0) break; const sentinelOffset = windowFileStart + found; if (!state.hits.includes(sentinelOffset)) { if (state.hits.length >= ELECTRON_FUSE_LIMITS.maximumSentinelsPerSlice) throw new ElectronFuseResourceFailure( "capacity", `Slice ${state.slice.index} contains more than ${ELECTRON_FUSE_LIMITS.maximumSentinelsPerSlice} Electron fuse sentinels.`, ); state.hits.push(sentinelOffset); } from = found + 1; } const tail = SENTINEL.length - 1; state.carry = window.length <= tail ? Buffer.from(window) : Buffer.from(window.subarray(window.length - tail)); } }; const parseWire = async ( handle: FileHandle, path: string, sentinelOffset: number, sliceEnd: number, signal: AbortSignal | undefined, ): Promise => { const wireStart = sentinelOffset + SENTINEL.length; const available = sliceEnd - wireStart; if (available < 1) return { sentinel_offset: sentinelOffset, wire_version: null, declared_length: null, fuses: [], complete: false, }; const header = await readExact( handle, path, wireStart, Math.min(available, 2), signal, ); const wireVersion = header[0]; if (wireVersion === undefined || header.length < 2) return { sentinel_offset: sentinelOffset, wire_version: wireVersion ?? null, declared_length: null, fuses: [], complete: false, }; const declared = header[1] ?? 0; const fuseLength = Math.min(declared, available - 2); const fuseBytes = fuseLength === 0 ? Buffer.alloc(0) : await readExact(handle, path, wireStart + 2, fuseLength, signal); return { sentinel_offset: sentinelOffset, wire_version: wireVersion, declared_length: declared, fuses: [...fuseBytes].map((raw, index) => ({ index, name: electronFuseName(wireVersion, index), state: electronFuseState(raw), raw_byte: raw, })), complete: fuseBytes.length === declared, }; }; const readExact = async ( handle: FileHandle, path: string, offset: number, length: number, signal: AbortSignal | undefined, ): Promise => { assertArtifactReadActive(signal); const buffer = await readFileHandleRange(handle, offset, length, () => assertArtifactReadActive(signal), ); if (buffer.length !== length) throw new RegularFileChangedError(path); return buffer; }; const isElf = (head: Uint8Array): boolean => head.length >= 4 && head[0] === 0x7f && head[1] === 0x45 && head[2] === 0x4c && head[3] === 0x46; const identifyPe = async ( readAt: ReadAt, size: number, ): Promise<{ readonly architecture: string | null } | undefined> => { if (size < 64) return undefined; const header = await readAt(0, 64); if (header.length < 64 || header[0] !== 0x4d || header[1] !== 0x5a) return undefined; const prefix = Buffer.from( header.buffer, header.byteOffset, header.byteLength, ); const observed = observeMzWindowsHeader(prefix, size); if (observed.state !== "signature-needed" && observed.state !== "declared") return undefined; const offset = observed.offset; if ( offset < 64 || offset > ELECTRON_FUSE_LIMITS.maximumPeHeaderOffset || offset > size - 4 ) return undefined; const bytes = Buffer.from(await readAt(offset, 24)); const confirmed = observeMzWindowsHeader(prefix, size, { offset, bytes }); if (confirmed.state !== "declared" || confirmed.signature !== "PE") return undefined; // A signature establishes the existing scan boundary; only a complete COFF // header establishes its machine. Managed OS overrides need separate proof. const coff = readPeCoffHeader(bytes.subarray(4)); const machine = bytes.length === 24 && coff.ok ? decodePeMachine(coff.value.machine) : undefined; return { architecture: machine?.nativeOsOverride === false ? machine.architecture : null, }; }; const isMissingPath = (cause: unknown): boolean => cause instanceof Error && "code" in cause && ["ENOENT", "ENOTDIR"].includes(String(cause.code));