Conversation
…he destination path
## Summary
- Fixes a bug introduced in `4.1.2` where `createPackageFromStreams` could embed the **wrong per-file integrity hash** in the archive header. The `insertFile` synchronous fast path computes integrity via `fs.readFileSync(p)`, but for the streams API `p` is the file's *destination path inside the archive* (relative), so it resolves against the process CWD. If the CWD happens to contain a file at the same relative path (e.g. `package.json` when building a project from its root), the header records the hash of that unrelated file while the archive stores the correct bytes from the stream.
- Apps packed this way and built with the `EnableEmbeddedAsarIntegrityValidation` fuse fail Electron's load-time integrity check and exit immediately at launch. The corruption is silent at pack time and environment-dependent: when no colliding file exists, the `readFileSync` throws `ENOENT` and the code silently falls back to (correct) stream hashing.
- Fix ([src/filesystem.ts](src/filesystem.ts), [src/asar.ts](src/asar.ts)):
- `insertFile` accepts a new internal option `fromStream`; when set, the `readFileSync` fast path is skipped entirely and integrity is always computed from `streamGenerator()` — the only authoritative source for stream content.
- `createPackageFromStreams` passes `{ fromStream: true }`.
- Safety guard in the remaining fast path (files API): the buffer returned by `fs.readFileSync(p)` is only trusted when `fileBuffer.length === size` (the size recorded in the header); otherwise it falls through to stream hashing. This protects against stat/read races and any future caller passing a `p` that isn't the real source file.
- No performance impact: the stream write path (`streamFilesystem`) never consumed `file.cachedBuffer`, so the fast path provided no write-batching benefit for streams — it only produced the (potentially wrong) hash. `createPackageFromFiles` behavior is unchanged.
## Regression test
[test/api-spec.ts](test/api-spec.ts) — `should compute stream package integrity from stream content, not from a same-named file in the CWD`:
1. Creates a source file with known content and a **decoy** file with different content located in the CWD at the same relative path as the archive destination (`index.js`).
2. Packs via `createPackageFromStreams` while `chdir`'d into the decoy directory (emulating building a project from its root).
3. Asserts the archived bytes are the stream content **and** the header integrity hash equals `sha256(stream content)`.
Verified the test fails on unpatched code (header hash matched the decoy's sha256) and passes with the fix. Full suite: `yarn lint` clean, `yarn vitest run` 188/188 passing.
|
Note: I didn't see any previous usage of logging, so I'm curious about the approach/policy there, as I think some of the swallowed errors and/or if-statements could use some ( |
There was a problem hiding this comment.
Working with @mmaietta
Change looks right: p is the archive destination path in the streams API, so the sync fast path could hash an unrelated CWD file. Skipping the fast path via fromStream is the minimal fix, and after main's content dedupe (#465) a wrong hash would also have aliased contentOffsets, so this matters more now.
Merged main into this branch locally: lint clean, 204/204 vitest passing. The head lives on your fork, which I can't push to, so the conflict resolution is below.
A few non-blocking notes:
test/api-spec.ts:229—fs.mkdtemp(os.tmpdir(), ...)is never removed, so each run leaks a directory. The other tests write underTEST_APPS_DIR, whichbeforeEachwipes; buildingworkthere (or removing it in thefinally) keeps that consistent.test/api-spec.ts:241—process.chdironly works because vitest runs withpool: 'forks'; underthreadsit throws. A one-line comment would save someone a confusing failure later.src/filesystem.ts:160— thefileBuffer.length === sizeguard is fine as belt-and-braces, but for the files API a mismatch means the file changed during packing, and we still record the stale stat size and stream whatever is on disk, so the archive is corrupt either way. Either throw a clear error on mismatch or narrow the comment to "don't trust a buffer that doesn't match the header size".- Nothing exercises the
length !== sizefall-through; a smallfilesystem-speccase callinginsertFilewith a mismatchedstat.sizewould cover it.
Conflict resolution against main (two hunks)
src/asar.ts, createPackageFromStreams case 'file' — keep main's ordering (insertFile before files.push, capture duplicate) and add the PR's fromStream option:
case 'file': {
const duplicate = await filesystem.insertFile(
filename,
stream.streamGenerator,
stream.unpacked,
{ type: 'file', stat: stream.stat },
// `filename` is the destination path inside the archive, not a path
// on disk, so integrity must be computed from the stream.
{ fromStream: true },
);
files.push({
filename,
streamGenerator: stream.streamGenerator,
link: undefined,
mode: stream.stat.mode,
unpack: stream.unpacked,
duplicate,
});
break;
}src/filesystem.ts, insertFile fast path — keep the PR's size guard around main's storeFileEntry body:
if (!options.fromStream && size <= BUFFER_HASH_THRESHOLD) {
// Fully synchronous fast path — no Promise, no stream, no microtask yield
try {
const fileBuffer = fs.readFileSync(p);
// Only trust the buffer if it matches the size recorded in the header;
// otherwise the file at `p` is not the content being archived.
if (fileBuffer.length === size) {
const integrity = getFileIntegrityFromBuffer(fileBuffer);
const duplicate = this.storeFileEntry(node, size, executable, integrity);
if (!duplicate) {
file.cachedBuffer = fileBuffer;
}
return Promise.resolve(duplicate);
}
} catch {
// Fall through to stream path
}
}
return getFileIntegrity(streamGenerator()).then((integrity) =>
this.storeFileEntry(node, size, executable, integrity),
);Everything else (the fromStream?: boolean option, yarn.lock) merges cleanly.
Generated by Claude Code
…am-integrity-fast-path # Conflicts: # src/asar.ts # src/filesystem.ts
|
Thanks for the review. Merged
|
|
@claude review |
Summary
4.1.2wherecreatePackageFromStreamscould embed the wrong per-file integrity hash in the archive header. TheinsertFilesynchronous fast path computes integrity viafs.readFileSync(p), but for the streams APIpis the file's destination path inside the archive (relative), so it resolves against the process CWD. If the CWD happens to contain a file at the same relative path (e.g.package.jsonwhen building a project from its root), the header records the hash of that unrelated file while the archive stores the correct bytes from the stream.EnableEmbeddedAsarIntegrityValidationfuse fail Electron's load-time integrity check and exit immediately at launch. The corruption is silent at pack time and environment-dependent: when no colliding file exists, thereadFileSyncthrowsENOENTand the code silently falls back to (correct) stream hashing.insertFileaccepts a new internal optionfromStream; when set, thereadFileSyncfast path is skipped entirely and integrity is always computed fromstreamGenerator()— the only authoritative source for stream content.createPackageFromStreamspasses{ fromStream: true }.fs.readFileSync(p)is only trusted whenfileBuffer.length === size(the size recorded in the header); otherwise it falls through to stream hashing. This protects against stat/read races and any future caller passing apthat isn't the real source file.streamFilesystem) never consumedfile.cachedBuffer, so the fast path provided no write-batching benefit for streams — it only produced the (potentially wrong) hash.createPackageFromFilesbehavior is unchanged.Regression test
test/api-spec.ts —
should compute stream package integrity from stream content, not from a same-named file in the CWD:index.js).createPackageFromStreamswhilechdir'd into the decoy directory (emulating building a project from its root).sha256(stream content).Verified the test fails on unpatched code (header hash matched the decoy's sha256) and passes with the fix. Full suite:
yarn lintclean,yarn vitest run188/188 passing.