install: hash patch contents with SHA-1 instead of Wyhash11 - #32749
Conversation
|
Updated 5:21 PM PT - Aug 11th, 2026
✅ @robobun, your commit 05b6dc36dafc030ee165c9d3ac224261966183d8 passed in 🧪 To try this PR locally: bunx bun-pr 32749That installs a local version of the PR into your bun-32749 --bun |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughPatch hashing now derives a ChangesCollision-resistant cache keys
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/install/patch_install.rs`:
- Around line 701-706: Shorten the rationale comment in the patch hash section
so it fits the 3-line comment limit while preserving the key invariant that
SHA-1 truncated to 64 bits replaces Wyhash11 to avoid cache collisions. Update
the existing comment near the patch hash logic in patch_install.rs to be
concise, and keep the longer explanation only in the PR discussion. Focus on the
comment block around the patch hash/cache folder explanation, not the
surrounding code.
In `@src/jsc/RuntimeTranspilerCache.rs`:
- Around line 619-624: Trim the source-hash comment in RuntimeTranspilerCache to
match the repo’s 3-line comment style by keeping only the cache-key invariant
and the issue reference; update the nearby comment around the hash
computation/`input_hash` logic so it briefly states that the hash must prevent
distinct source files from sharing a cache entry, and retain the
oven-sh/bun#32741 reference without the extended rationale.
In `@src/sql_jsc/mysql/MySQLQuery.rs`:
- Around line 341-345: The new MySQL cache comments in MySQLQuery should be
shortened to comply with the 3-line comment limit while preserving the key
invariant and ownership summary. Update the explanatory blocks around the cache
lookup and prepare path so they are collapsed into brief inline comments,
keeping the important collision/mismatch behavior tied to the query cache logic
in MySQLQuery::prepare and the related lookup code.
In `@src/sql_jsc/postgres/PostgresSQLQuery.rs`:
- Around line 644-650: Shorten the collision comment in PostgresSQLQuery so it
fits the 3-line comment guideline by keeping only the key invariant and the
action taken. Update the explanatory block near the cached prepare logic to
mention that wyhash(signature.name) can collide, that a stored-name mismatch
must be treated as a cache miss, and that execution should fall through to the
uncached prepare path without changing the existing map entry.
In `@test/cli/install/bun-install-patch.test.ts`:
- Around line 1049-1074: The install helper in bun-install-patch.test.ts still
resolves dependencies against the public npm registry because only
BUN_INSTALL_CACHE_DIR is overridden. Update mkProject/install to use the same
dummy local registry fixture as the other install tests so the is-odd@3.0.1
resolution is fully isolated and deterministic, while keeping the sharedCache
for cache isolation.
In `@test/cli/install/wyhash-std-collision.ts`:
- Around line 95-100: The helper comment in wyhash-std-collision should be
shortened to fit the 3-line comment limit while still documenting the layout
invariant. Update the comment near the collision-array builder logic to keep
only the essential explanation of the byte-array form, the collision goal under
std.Wyhash(seed), and the purpose of tailMinLen/freeOffset, using a compact
3-line version.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 7ac45246-8d99-46b2-907b-42b7acd0dc28
📒 Files selected for processing (8)
src/install/patch_install.rssrc/jsc/RuntimeTranspilerCache.rssrc/sql_jsc/mysql/MySQLQuery.rssrc/sql_jsc/postgres/PostgresSQLQuery.rstest/cli/install/bun-install-patch.test.tstest/cli/install/wyhash-std-collision.tstest/cli/run/transpiler-cache.test.tstest/js/sql/sql-statement-cache-hash-collision.test.ts
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/jsc/RuntimeTranspilerCache.rs (1)
44-45: 🗄️ Data Integrity & Integration | 🟡 MinorAlign the cache version constants.
EXPECTED_VERSIONis 23, butRUNTIME_TRANSPILER_CACHE_VERSIONis 20, so newly written cache entries will be treated as stale immediately.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/jsc/RuntimeTranspilerCache.rs` around lines 44 - 45, Align the cache version constants used by RuntimeTranspilerCache so they match. Update the version value referenced by EXPECTED_VERSION and the RUNTIME_TRANSPILER_CACHE_VERSION logic together, and make sure the code paths that read/write the cache use the same version number so entries written by the transpiler are not immediately invalidated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/jsc/RuntimeTranspilerCache.rs`:
- Around line 44-45: Align the cache version constants used by
RuntimeTranspilerCache so they match. Update the version value referenced by
EXPECTED_VERSION and the RUNTIME_TRANSPILER_CACHE_VERSION logic together, and
make sure the code paths that read/write the cache use the same version number
so entries written by the transpiler are not immediately invalidated.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 1b629f28-2e19-4e22-851b-2c77d1c30bef
📒 Files selected for processing (6)
src/install/patch_install.rssrc/jsc/RuntimeTranspilerCache.rssrc/sql_jsc/mysql/MySQLQuery.rssrc/sql_jsc/postgres/PostgresSQLQuery.rstest/cli/install/wyhash-std-collision.tstest/js/sql/sql-statement-cache-hash-collision.test.ts
|
Re the out-of-diff CodeRabbit note about The only code that encodes the on-disk Since the constant's own comment declares it a mirror of |
There was a problem hiding this comment.
Thanks — the 3a13cc7 fix to the ErrorResponse handler addresses the UAF I flagged and the added regression test covers it. I'm not auto-approving because this is security-hardening that reworks unsafe intrusive ref-counting across the MySQL/Postgres statement caches and changes on-disk cache-key hashing, so it warrants a human sign-off.
Extended reasoning...
Overview
This PR hardens three hash-keyed caches against constructed wyhash collisions (#32741): the MySQL/Postgres per-connection prepared-statement cache (adds a stored-name comparison and an uncached fall-through path, plus a pointer-identity guard in the Postgres ErrorResponse eviction), the patchedDependencies contents hash (Wyhash11 → SHA-1/64), and the runtime transpiler cache input_hash (Wyhash seed-42 → SHA-1/64, with a cache-version bump mirrored in src/bundler/cache.rs). It extends the test-side wyhash collision helper and adds regression tests for all three.
Security risks
The change is itself the security fix; the residual risk is in the new control-flow it introduces. The Postgres/MySQL paths add a state where an uncached ref_count=1 statement coexists with a different cached statement under the same wyhash key — my earlier review found a UAF in the Postgres ErrorResponse handler on exactly that state, which the author confirmed and fixed in 3a13cc7 by comparing the map entry's pointer to the failing statement before evicting/derefing. The SHA-1/64 swaps are straightforward and the cache-version bump invalidates stale entries; I see no new exposure there.
Level of scrutiny
High. This is security-relevant hardening that edits unsafe intrusive ref-counting and raw-pointer eviction logic in two database drivers, changes the on-disk identity of two persistent caches, and already required one correctness fix during review. None of that is mechanical.
Other factors
All prior review feedback (CodeRabbit comment-length nits, the registry-fixture note, my 🟡 named-statement leak note, and my 🔴 UAF) is resolved; the author enumerated the remaining statements map accesses to bound the UAF class and added a Parse-failure regression test that exercises the fixed eviction path under await using teardown. The robobun build-status comment still shows failures on the latest commit, which a human reviewer should check before merging.
|
One correction to the note above so nobody goes looking for it: there is no robobun build-status comment reporting failures on this PR, and as of now CI on 3a13cc7 has no failing checks (55 passed, 9 still running, 0 failed). The only red CI this PR has had was on the previous commit, where the darwin-aarch64 test lane timed out downloading the build artifact before running any tests; 3a13cc7 superseded that build. Current state for whoever picks this up: every review finding is addressed and every thread is resolved, including the real use-after-free claude flagged in the Postgres ErrorResponse eviction (fixed in 3a13cc7 with a regression test). Agreed this wants a human sign-off given it touches intrusive ref-counting in the statement caches and changes two on-disk cache keys. |
Jarred-Sumner
left a comment
There was a problem hiding this comment.
Remove the runtime transpiler cache changes - that's not in scope.
For the other changes, use a trait/HashMap equality to do this instead of one-off implementations in each call site.
|
Done, both points. Transpiler cache: reverted entirely in 9e75379 (the hash change, the cache version bump, the test, and the collision-helper additions that only existed for that test). I also corrected my comment on #32741 so it no longer claims that PoC is addressed by this PR, and rewrote the title and description to match the narrower scope. Prepared statements: 8fef8fb re-keys both caches by the signature name itself, The patchedDependencies |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/sql_jsc/mysql/MySQLConnection.rs`:
- Around line 1728-1730: Remove the issue reference from the runtime invariant
comment in MySQLConnection around the cached statement map. Keep the durable
explanation about keying by signature.name and collision behavior, but delete
the bug-history reference so the comment in this area only describes the
invariant; update the adjacent comment text if needed without changing the
logic.
In `@src/sql_jsc/postgres/PostgresSQLConnection.rs`:
- Around line 56-58: The cache invariant comment in PostgresSQLConnection should
keep the durable explanation but remove the issue reference text, since bug
history should not live in source comments. Update the comment near the
statement cache map to preserve the statement-signature and hash-collision
invariant wording while dropping the “#32741” mention.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: a6acd847-c77a-4a63-9cca-9f934e003ddc
📒 Files selected for processing (5)
src/sql_jsc/mysql/JSMySQLConnection.rssrc/sql_jsc/mysql/MySQLConnection.rssrc/sql_jsc/mysql/MySQLQuery.rssrc/sql_jsc/postgres/PostgresSQLConnection.rssrc/sql_jsc/postgres/PostgresSQLQuery.rs
|
CI status for the final head (eaad76e): build #65112 finished, and none of its failures involve this PR's code or tests. The complete failing set from the build's annotations:
The tests added here ( |
eaad76e to
5b06fe4
Compare
|
Rebased onto main to clear the merge conflicts. Notable: the SQL prepared-statement name-keying fix landed on main independently while this was in review (both MySQL and Postgres now use |
There was a problem hiding this comment.
The remaining source change is small and looks correct (Wyhash11 → SHA-1/64 in PatchTask::calc_hash, API verified against bun_sha_hmac::sha::hashers::SHA1), but since it's a cache-poisoning hardening that also changes the on-disk _patch_hash= cache key for every user, I'd like a human to confirm the algorithm choice and the one-time re-patch cost are acceptable.
Extended reasoning...
Overview
After the rebase, this PR now touches only three files: a ~10-line hash-algorithm swap in src/install/patch_install.rs (PatchTask::calc_hash now streams into BoringSSL SHA-1 and truncates the digest to a u64 instead of using Wyhash11(seed=0)), a new regression test in test/cli/install/bun-install-patch.test.ts with a fixed pair of Wyhash11-colliding patch files, and a new test/js/sql/sql-statement-cache-hash-collision.test.ts that guards the SQL prepared-statement invariant whose source fix already landed on main. All of the SQL source changes I previously commented on (PostgresSQLQuery.rs / PostgresSQLConnection.rs / MySQL*) were dropped in the rebase and are no longer in this diff; every prior inline thread is resolved.
Security risks
The change fixes a security issue rather than introducing one: under Wyhash11 an adversarially-constructed patch file could share a _patch_hash= cache folder with a different patch and cause the second project to observe the first project's patched package. Truncated SHA-1 makes that a ~2^64 second-preimage problem. I verified the SHA1 hasher API (init()/update()/r#final(&mut [u8; 20]), DIGEST = 20) matches the call site, and bun_sha_hmac is already a dependency of the install crate. The u64 interface (folder suffix, .bun-patch-hash- tag file, in-memory PatchedDep) is preserved. I don't see a way this weakens anything.
Level of scrutiny
Even though the diff is now mechanically trivial, it is a security hardening in the package manager and it changes the derivation of an on-disk cache identity for every user of patchedDependencies — existing installs will re-patch once because the tag filename differs. That's the kind of user-visible behavior/algorithm choice (SHA-1 truncated to 64 bits vs. e.g. full digest hex) that I think a maintainer should explicitly sign off on rather than have a bot approve. The PR also went through substantial scope surgery (transpiler-cache revert at Jarred's request, SQL re-keying, then SQL source dropped entirely on rebase), so a human confirming the final shape matches intent seems worthwhile.
Other factors
No bugs were found by the bug-hunting system on this revision. All CodeRabbit and prior claude[bot] threads are resolved. The new install test follows the file-local convention (public registry, tempDirWithFiles, early {stdout,stderr,exitCode} guard before reading generated files). The SQL test is regression-only against a fix already on main and gates on the container harness. The bun_wyhash::Wyhash11 import is removed but bun_wyhash remains a live dependency of the install crate (still used elsewhere), so no Cargo.toml change is needed here.
|
CI status for the rebased head (5b06fe4), build #69386: 250 lanes passed (including the Linux docker lanes that run this PR's
The 3-file diff (patch |
Jarred-Sumner
left a comment
There was a problem hiding this comment.
@robobun rebase + make sure we hash the whole patch file instead of just the first 16 KB. We can read it in chunks of say 64 KB though.
A constructed Wyhash11 collision let two distinct patch files share one `_patch_hash=` cache folder, so the second project installed the first's patched package. Derive the u64 from SHA-1 instead; the folder-suffix / tag-file interface is unchanged. See #32741. Adds the patch-cache collision regression test, plus a SQL prepared-statement collision regression test covering the name-keyed statement cache (the MySQL/Postgres hash-collision fix itself landed separately on main).
read_fill_buf always reads from file offset 0, so the chunked loop re-hashed the same leading bytes until it had counted the file's size. Any two patches with an identical first chunk therefore hashed equal. Read with pread_all at an explicit running offset, 64 KiB at a time. Also moves the patch-hash tests onto tempDir, which replaced tempDirWithFiles in this file upstream.
5b06fe4 to
a337310
Compare
|
Done, both parts. Rebased onto current main (clean, single commit). The hashing was actually worse than only covering the first chunk: |
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🟡
src/install/patch_install.rs:707— This PR rewirescalc_hashfromfile.read_fill_buf(...)tofile.pread_all(...), which was the sole caller ofbun_sys::File::read_fill_buf— repo-wide grep now shows only its definition at src/sys/file.rs:232 and this comment. Per REVIEW.md ("Delete dead code in the same PR that makes it dead … helpers whose last caller you rewired … Public items escape dead-code lints — grep for callers manually"), deleteread_fill_bufalongside; it's also the platform-divergent footgun (POSIXpreadfrom offset 0 vs Windows cursorread()) that caused the first-chunk-only bug fixed here.Extended reasoning...
What
Commit a337310 ("install: hash the whole patch file, not just its first chunk") in this PR rewires
PatchTask::calc_hashfromfile.read_fill_buf(&mut stack[..])tofile.pread_all(&mut chunk[..], offset). That was the only caller ofbun_sys::File::read_fill_bufin the repository. After this change,rg read_fill_bufacross the whole tree returns exactly two hits:src/sys/file.rs:232— thepub fn read_fill_buf<'b>(&self, buf: &'b mut [u8]) -> Maybe<&'b mut [u8]>definitionsrc/install/patch_install.rs:701— the change-narration comment ("read_fill_bufalways reads from file offset 0, so looping over it re-hashes the first chunk")
No other call sites, re-exports, or macro-generated references exist.
Why it belongs in this PR
REVIEW.md is explicit that this is required scope, not scope creep:
Delete dead code in the same PR that makes it dead (required scope — name the deletions in the description): superseded implementations, helpers whose last caller you rewired, fields nothing reads … Public items escape dead-code lints — grep for callers manually.
read_fill_bufis exactly "a helper whose last caller you rewired", and it ispub fn, so rustc'sdead_codelint will not flag it — the manual grep the guidelines call for is what surfaces it. This PR already followed the same rule for thebun_wyhashdependency insrc/sql_jsc/Cargo.toml(dropped in eaad76e when its last use was removed);read_fill_bufis the same case one layer deeper.Why it's worth deleting rather than leaving in the API
Beyond hygiene,
read_fill_bufis a footgun. Looking at src/sys/file.rs:235-240:// POSIX uses pread() from offset 0 so a pre-advanced cursor // doesn't truncate; Windows falls back to read(). #[cfg(unix)] let rc = pread(self.handle, &mut buf[read_amount..], read_amount as i64); #[cfg(not(unix))] let rc = read(self.handle, &mut buf[read_amount..]);
On POSIX, every call
preads starting at absolute file offset 0 (the offset argument isread_amount, which resets to 0 on each call to the function). On Windows, itread()s from the current file cursor. So calling this in a loop — as the pre-PRcalc_hashdid — has divergent semantics: POSIX re-reads the same firstbuf.len()bytes on every iteration, while Windows advances through the file. That divergence is precisely what caused the pre-existing "only the first chunk is hashed" bug this PR's HEAD commit fixes (and covers with the new "patches that differ only after the first 64 KiB" test). Leaving the helper callable invites the next caller to hit the same trap.Step-by-step proof
- Base branch:
calc_hashat src/install/patch_install.rs calledfile.read_fill_buf(&mut stack[..])in awhile (read as u64) < sizeloop. - PR diff: that call is replaced by
file.pread_all(&mut chunk[..], offset)with an explicitoffsetaccumulator. rg -n read_fill_bufon the PR head →src/sys/file.rs:232(definition) +src/install/patch_install.rs:701(comment). No.rscall sites.read_fill_bufispub fn→ escapes#[warn(dead_code)]→ CI will not catch it.- Therefore the helper became dead in this PR and should be deleted here.
Impact
None at runtime — this is dead-code hygiene. The function compiles into
libbun_rust.aunused (or is stripped by the linker), and no behavior changes either way.Fix
Delete
pub fn read_fill_buf(src/sys/file.rs:230-248, including its doc comment). Optionally also drop the change-narration comment at src/install/patch_install.rs:701-702, since once the helper is gone there's nothing to contrast against — the explicitoffsetaccumulator speaks for itself (and comment-cop already flagged that block).
calc_hash was read_fill_buf's last caller. The helper also read from offset 0 on POSIX but from the cursor on Windows, which is what made looping over it hash only the first chunk there. describeWithContainer already skips when no container source is available, checking the env override and coordinator before isDockerEnabled() (which can throw on Linux CI); the outer guard inverted that order and had no coordinator check.
|
Addressed the three follow-up findings in 33a9059 (plus 4d50023, which had already landed before that review posted):
Net diff is now |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it swaps the hash function that names the shared _patch_hash= cache folders (a security-relevant collision-resistance choice) and invalidates every existing patched-package cache entry on upgrade, a human look would still be worthwhile.
What was reviewed:
calc_hash's newpread_allloop — offset advances by the returned byte count, so the whole file feeds SHA-1;n == 0breaks on short files.bun_sha_hmac::sha::hashers::SHA1API (init/update/r#final/DIGEST) matches the call sites;bun_sha_hmacwas already a dependency ofbun_install.read_fill_bufhas no remaining callers undersrc/after the deletion.- The SQL regression test's
constructStdCollisionimport resolves (test/cli/install/wyhash-std-collision.tsexists) anddescribeWithContaineris called unconditionally per the harness invariant.
Extended reasoning...
Overview
Four files: src/install/patch_install.rs swaps calc_hash from Wyhash11(seed=0) to SHA-1-truncated-to-u64 and rewrites the read loop from read_fill_buf (which restarted at file offset 0 each call on POSIX, so only the first chunk was ever hashed) to pread_all at an explicit running offset; src/sys/file.rs deletes the now-dead read_fill_buf; two test files add regression coverage for the wyhash-collision patch-cache poisoning and (separately) for the SQL prepared-statement cache collision whose source fix already landed on main.
Security risks
The whole point of the change is security: the _patch_hash=<hex> suffix on the shared install-cache folder and the .bun-patch-hash-<hex> tag file were derived from a non-cryptographic hash of attacker-influenceable file bytes, so a constructed collision let two projects share one patched-package folder. Moving to SHA-1/64 raises the second-preimage cost to 2^64. The truncation to u64 keeps the on-disk interface unchanged. I don't see a new risk introduced by the diff itself — the read loop is bounds-correct and the digest buffer is exactly SHA1::DIGEST bytes — but the choice (SHA-1 truncated to 64 bits, rather than e.g. full SHA-256 hex in the folder name) is a design call a maintainer should sign off on.
Level of scrutiny
Higher than a mechanical refactor. This is a hash-function selection for a shared on-disk cache identity (crypto-adjacent per the "do not approve security-sensitive" guideline), it changes behavior for every existing install with a patchedDependencies entry (all cached patched packages re-patch once because the tag filename differs), and a maintainer has already been actively shaping this PR's scope (the transpiler-cache portion was reverted at Jarred's request per the description). Those all point at human review.
Other factors
All of my prior inline comments on this PR were nits (comment length, redundant test gating, dead Cargo dep, helper duplication) and are marked resolved with follow-up commits through 05b6dc3. No human review is on the timeline. The bug-hunting pass on the current head found nothing. The 16 KiB stack buffer became a 64 KiB heap Vec, which is fine for a once-per-patch-file cold path. pread_all's offset: u64 → i64 internal cast is a non-issue at patch-file sizes.
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it changes the hash used for a security-relevant cache identity (patch cache poisoning from #32741) and deletes a public bun_sys::File helper, a human sign-off would still be worthwhile.
What was reviewed:
calc_hash: SHA-1 API (init/update/r#final/DIGEST=20) matches;digest[0..8]unwrap is infallible;bun_sha_hmacwas already aninstallcrate dep.pread_allloop: offset advances byn, breaks onn == 0, bounded bysize; the 80 KiB-prefix test guards the old first-chunk-only bug.read_fill_bufremoval: no remaining callers repo-wide; the removedWyhash11import was the file's onlybun_wyhashreference.- SQL regression test:
constructStdCollisionhelper exists at the imported path;describeWithContainerself-gates so the tests skip cleanly without docker.
Extended reasoning...
Overview
The PR touches four files: src/install/patch_install.rs (swap Wyhash11 → truncated SHA-1 for the patch contents_hash, and fix the read loop to hash the whole file via pread_all at a running offset instead of read_fill_buf which restarted at offset 0 on POSIX), src/sys/file.rs (delete now-dead read_fill_buf), and two test files (two new patch-hash regression tests in bun-install-patch.test.ts; a new SQL prepared-statement collision regression test whose source fix already landed on main).
Security risks
The change is itself the security fix: it hardens the shared-cache folder / .bun-patch-hash tag identity against constructed second-preimage collisions (Wyhash11 → SHA-1/64). No new attack surface is introduced — the u64 interface (folder suffix, tag filename, lockfile field) is unchanged, and the input is a local patch file the user already controls. The read-loop fix is a strict correctness improvement (previously any two patches sharing an identical first ~16 KiB hashed equal on POSIX). I checked that the digest truncation is well-defined and that bun_sha_hmac was already a dependency of the install crate.
Level of scrutiny
Medium-high. The Rust source change is small (~30 lines) and mechanical, but it selects a hash function for a security-relevant cache identity and deletes a public method from bun_sys::File. I verified read_fill_buf has zero remaining callers repo-wide, that the removed Wyhash11 import was the file's only bun_wyhash reference, and that the SHA1 hasher's init/update/r#final/DIGEST API matches the call sites. The pread_all signature (&mut [u8], u64 → Maybe<usize>) matches the loop's usage.
Other factors
CI is green on the current head (Build #92633). Every prior round of feedback — mine (stale test prose, dead bun_wyhash Cargo dep, redundant isDockerEnabled() wrappers, comment-cop narration comments, duplicated mkProject/install helpers), coderabbitai's, and the comment-cop bot's — is addressed and resolved. No CODEOWNERS entries cover the touched paths. The PR went through significant scope reduction (transpiler-cache reverted at maintainer request; SQL source fix landed independently on main and was dropped in the rebase), so a human confirming the final scope matches maintainer intent is the remaining value-add.
…fix bun patch for non-npm deps and isolated hang (#38269) ### What does this PR do? Install-cache and `bun patch` robustness, consolidated from #37124, #37136 and #37145 (rebased and reshaped; #32749 from the same batch landed separately). **Git dependency cache folders are built in a staging dir and renamed on success.** `Repository::checkout` cloned straight into `<cache>/@g@<sha>` and checked out in place; `Repository::download` cloned the bare mirror straight into `<cache>/<hash>.git`. An install (or its git child — seen OOM-killed in CI) dying between steps left a folder at the trusted name: an empty `@G@` folder resolves as an *empty package* through the "git dependency without package.json" path (exit 0, `bun.lock` name falls back to the URL basename), and a half-cloned mirror fails every later `git fetch`. Both now build under a temporary sibling inside the cache dir (`CacheStaging`, same-filesystem so it's the same `renameat_concurrently` ladder tarball extraction uses) and are renamed into place only when complete; failures remove the staging dir. **Cache hits require the entry's completion marker.** One helper, `is_package_in_cache_at(cache_dir, folder, tag)`: npm folders must contain `package.json`, git checkouts must contain the `.bun-tag` written last, everything else stays a directory probe. Used by `checkout()`'s resolve-time hit, `determine_preinstall_state`, and the hoisted and isolated installers — previously every git hit was a bare directory probe (`.bun-tag` in the cache was written but never read), and `determine_preinstall_state` didn't probe `package.json` for npm either. Folders left by older versions are re-cloned instead of installed. Deletes the hoisted installer's unsafe in-place edit of the shared folder-name buffer and the isolated installer's append/truncate copy. Since the tag is now the marker, `checkout()` unlinks anything a repo checked in as `.bun-tag`, creates it `O_EXCL|O_NOFOLLOW`, and fails the checkout rather than publish an untaggable folder (a repo shipping a symlink named `.bun-tag` now gets a real tag; its target is still never written — test updated). Existing bare mirrors are not validated structurally; there's no marker for them. **`bun patch --commit` on git, github and tarball dependencies** failed with `Could not access '.../@gh@@@@1'` and wrote nothing (#18792, #17945): it loads its own lockfile but computed the cache path against the empty `manager.lockfile`. The loaded lockfile is moved into the manager before the path is computed (`install_with_manager` reloads it afterwards, as it already does for `bun update`; the double parse is left alone). Patch filenames additionally escape NTFS-reserved characters, which only these resolutions contain. Fixes #18792, fixes #17945. **Isolated linker hang** — a plain `bun install` in a workspace with a `patchedDependencies` entry for a git/github dependency hung forever: the installer treated every patched package as missing and re-enqueued a download the resolve phase had already completed, parking on a drained task list. It now probes the unpatched folder (computed with no patch hash) like everything else; also fixes removing an entry. **Tests:** github/git/tarball `patch --commit` flows; add → cold cache → remove → re-add of a patch under isolated for github, git and npm (the suspected stale-`.bun-tag-<hash>` skip on re-add did not reproduce, so these just pin the cycle); git checkout failure leaves only the mirror in the cache and an empty folder at the cache name is re-cloned; a pre-existing test pinned to the bogus `--commit` cache path now asserts the step that genuinely fails. ### How did you verify your code works? `bun-install` + `bun-install-registry` (427), `isolated-install` (65), `bun-install-patch`, `bun-patch` all pass locally; new tests fail on release bun (half-built `@G@` folder left behind; `--commit` error; isolated hang). Each fix was also driven by hand with the debug binary: `patch --commit` on a `file:` tarball fails on main's binary and succeeds here; a workspace with a patched local git dep installs, requires as patched, survives remove and re-add, and re-clones an emptied cache folder, with no `.tmp` residue. Windows rename/escaping legs are left to CI. Local gotcha: these files need `HOME` pointed at an empty dir if `~/.npmrc` sets `install-strategy=hoisted`. --- **Added after CI** (`25855c0fe97`): the npm add/remove/re-add test failed on Linux — the hardlink and copyfile backends overlay files onto an existing isolated store entry, so removing a patch kept every file the patched build had *added* (and its `.bun-tag-<hash>`); clonefile replaces the tree, which is why macOS passed and why the earlier "did not reproduce" was wrong — this is the staleness #37136 mentioned. The task now deletes the previous project-local package tree before rebuilding an entry (only reached when the entry needs a rebuild, so warm installs don't pay for it); the npm cycle test pins the hardlink backend. Also from review: `checkout()` uses `delete_tree` for a checked-in `.bun-tag`, so a directory under that name is replaced instead of failing the install (test added).
Fixes the
patchedDependenciescache-poisoning site from #32741, and adds regression coverage for the SQL prepared-statement collision.patchedDependencies
contents_hashThe shared-cache folder for a patched package is named
<pkg>@<ver>_patch_hash=<hex>, and the installed tree is verified by a.bun-patch-hash-<hex>tag file. Both were the Wyhash11(seed=0) of the patch file's raw bytes, so two projects with different patches that collide under Wyhash11 shared one cache folder and the second project observed the first project's patched package instead of applying its own patch:Fix: derive the u64 from SHA-1 instead of Wyhash11. The u64 interface (folder suffix, tag filename, in-memory
PatchedDep) is unchanged, and a constructed second preimage now costs 2^64 work. Existing node_modules re-patch once because the tag file name differs. There is no in-memory map to re-key here: the identity is a directory name derived from file contents, so the hash itself has to be collision resistant.Hash the whole file
The chunked read loop (pre-existing, inherited from the Wyhash11 version) called
File::read_fill_buf, which always reads from file offset 0. Each iteration therefore returned the same leading bytes, and the loop fed that first chunk into the hasher repeatedly until it had counted the file's size. Any two patch files with an identical first chunk hashed equal no matter what followed, which defeats the point of a collision-resistant hash. The loop now usespread_allat an explicit running offset, 64 KiB at a time. A second test covers it: two patches sharing an 80 KiB identical prefix and differing only in the final line must land in distinct cache entries (fails on the released binary, where project B receives project A's patched package).calc_hashwas the only caller ofbun_sys::File::read_fill_buf, so that helper is deleted in the same change.SQL prepared-statement cache (regression test only)
The MySQL/Postgres prepared-statement caches were keyed on
wyhash(signature.name)with identity equality, so two distinct queries whose names collided under that hash shared one server-side statement. That fix — re-keying the caches on thesignature.namebytes viaStringHashMapso the map's own equality distinguishes colliding queries — landed independently on main while this PR was in review. This PR keeps the regression test (test/js/sql/sql-statement-cache-hash-collision.test.ts): it builds wyhash-colliding query pairs for MySQL and Postgres and asserts each query returns its own result, plus a Postgres case where the colliding query fails at Parse. The tests pass against main's name-keyed caches and guard the invariant against a future regression to hash-keying.Rebase note
Rebased onto main. Main had independently landed the SQL prepared-statement name-keying fix (both MySQL and Postgres, via
StringHashMap<*mut Statement>), so all of this PR's SQL source changes were superseded and dropped in the rebase; only the patchcontents_hashsource change and the two regression tests remain. The earlier transpiler-cache changes were already reverted at Jarred's request before the rebase.Verification
The patch-hash test installs two projects with a shared
BUN_INSTALL_CACHE_DIRplus a non-colliding control project; it fails before the SHA-1 swap (USE_SYSTEM_BUN=1) and passes after. The SQL tests pass against main's name-keyed caches.no test proof · iteration 7 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/bun-install-patch.test.ts