Skip to content

install: key the lockfile string pool by the bytes, not a stored hash - #41366

Merged
Jarred-Sumner merged 1 commit into
mainfrom
robobun/b7df888e/lockb-name-hash
Sep 4, 2026
Merged

Jarred-Sumner merged 1 commit into
mainfrom
robobun/b7df888e/lockb-name-hash

Conversation

@robobun

@robobun robobun commented Sep 4, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • bun add can panic in Lockfile::clean_with_logger: panic: range end index 391 out of range for slice of length 383. The frames are lockfile::StringBuilder::append_with_hash, Dependency::clone_with_different_buffers and Package::clone.
  • StringBuilder::count (src/install/lockfile.rs) skips a string whose hash is already pooled. append_with_hash looked up a hash the caller passed. Package::clone passed the name_hash stored in the lockfile, and Package::from_npm passed hashes stored in the manifest cache. If a stored hash does not match its bytes, the append writes bytes that were never reserved.

Fix

  • Remove append_with_hash. append hashes the bytes it writes, so the count and the append always use the same key. The six callers move to append.
  • Recompute each package's name_hash from its name when a bun.lockb loads, as loading bun.lock already does. A valid file does not change.
  • Verified: two new tests. bun-lockb.test.ts changes a stored hash in bun.lockb. bun-install-registry.test.ts changes the name hash in a cached manifest. Bun 1.4.1 panics on both. The notes list the other suites.

Background

  • The lockfile keeps its strings in one buffer. StringBuilder counts the bytes it needs, reserves that much, and then appends.
  • The string pool maps the hash of a string to its place in the buffer. A pooled string is not counted or written again.
  • bun.lockb is the binary lockfile, and each .npm file in the cache is a serialized manifest. Both loaders copy their hashes from disk.
Notes
  • Crash report: one event, Windows x64, Bun 1.4.0. It has no text_lockfile feature, so that run did not load a bun.lock. The origin of its bad hash is not proven. The stored hashes in bun.lockb and in the manifest cache are the two sources found, and the tests construct both.
  • A build that logs each mismatched hash found none in the install suites. bun.lockb files written by Bun 1.0.36, 1.1.38, 1.2.0, 1.3.0 and 1.3.14 have none either.
  • install: treat an out-of-range package id in bun.lockb as corruption #31008 rejects a bun.lockb with an out-of-range package id. This change repairs name_hash instead, because the name fully determines it. package_index is keyed by it, and the debug build's verify_data asserts it.
  • Excluded: the name_hash of dependency rows in bun.lockb. Dependency strings are counted and appended by their bytes, so those rows cannot reach this panic.
  • install: validate bun.lockb slice descriptors and ids at load time #32753 also edits the end of bun.lockb.rs load. The two changes touch different lines.
  • Self-review: it asked for the key to be fixed in StringBuilder, which covers from_npm, and for the manifest cache test. Both are in this PR.
  • Suites run with the debug build: bun-lockb, bun-install-registry, bun-lock, migrate-bun-lockb-v2, bun-add, bun-add-catalog, bun-install, bun-workspaces, overrides, nested-overrides, catalogs, bun-update, migration/.

no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/install/bun-lockb.test.ts, test/cli/install/bun-install-registry.test.ts

…lockb name hashes

lockfile::StringBuilder::count skips a string whose hash is already in
the pool. append_with_hash looked the string up by a hash the caller
passed. Package::clone passed the name_hash stored in the lockfile, and
Package::from_npm passed hashes stored in the manifest cache. When a
stored hash did not match its bytes, count skipped the string and the
append wrote it anyway, past the reserved bytes:
"range end index N out of range for slice of length M".

Remove append_with_hash. append hashes the bytes it is given, so count
and append always use the same key, and the ExternalString hash it
returns matches the bytes.

bun.lockb also copied each package's name_hash from disk. Recompute it
from the name on load, as loading bun.lock already does.
@robobun

robobun commented Sep 4, 2026 •

Copy link
Copy Markdown
Collaborator Author

Reproduced on Bun 1.4.1 in two ways:

  • Change one package's name_hash in a bun.lockb, then run bun add no-deps@1.0.0: panic: range end index 153 out of range for slice of length 140.
  • Change the name hash in a cached .npm manifest, remove the lockfile, then run bun install --prefer-offline: panic: range end index 88 out of range for slice of length 75.

Both new tests fail on main in the debug and release builds. They pass on this branch in the debug build.

CI, build 110139: no lane reports a failure in the two new tests. The failures are in code this PR does not change:

  • test/cli/run/run_command.test.ts (ubuntu 25.04 aarch64) also fails on main.
  • test/js/third_party/grpc-js/test-server.test.ts (debian 13 x64-asan) and test/js/node/net/node-net-server.test.ts (darwin aarch64) each failed one network test, once, in a parallel batch.
  • The other failures passed on retry.

Status: ready for review.

@github-actions github-actions Bot added the claude label Sep 4, 2026
@robobun

robobun commented Sep 4, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 5:17 AM PT - Sep 4th, 2026

❌ @robobun, your commit b3ce903 has 3 failures in Build #110139 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 41366

That installs a local version of the PR into your bun-41366 executable, so you can run:

bun-41366 --bun

@coderabbitai

coderabbitai Bot commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 2e9b7855-dc83-445c-bec2-77da55c3904c

📥 Commits

Reviewing files that changed from the base of the PR and between 4661e49 and b3ce903.

📒 Files selected for processing (7)
  • src/install/lockfile.rs
  • src/install/lockfile/CatalogMap.rs
  • src/install/lockfile/OverrideMap.rs
  • src/install/lockfile/Package.rs
  • src/install/lockfile/bun.lockb.rs
  • test/cli/install/bun-install-registry.test.ts
  • test/cli/install/bun-lockb.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 1 remains after this review.


Walkthrough

Changes

Lockfile string construction now computes hashes from string values. Binary lockfile loading recomputes package name hashes. Regression tests cover corrupted cached manifest and lockfile hashes.

Lockfile hash integrity

Layer / File(s) Summary
Hash-computing string append
src/install/lockfile.rs, src/install/lockfile/CatalogMap.rs, src/install/lockfile/OverrideMap.rs, src/install/lockfile/Package.rs
StringBuilder::append computes hashes directly. Lockfile name and version construction uses this method.
Lockfile hash normalization
src/install/lockfile/bun.lockb.rs
The loader recomputes each package name_hash from its decoded package name.
Corrupted hash regression coverage
test/cli/install/bun-install-registry.test.ts, test/cli/install/bun-lockb.test.ts
Tests corrupt cached manifest and binary lockfile hashes, then verify successful installation and correct hash handling.

Suggested reviewers: jarred-sumner

Merge Risk: ⚪ Minimal · up to b3ce9

The lockfile hash handling change has no remaining identified merge-blocking risk.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the primary change: the lockfile string pool now uses string bytes instead of stored hashes.
Description check ✅ Passed The description explains the problem, fix, verification steps, background, scope, and test coverage. It does not use the exact template headings, but it provides the required information and is comple…

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. Because it changes the string-pool keying invariant that every lockfile clean/clone depends on and adds a recompute pass to the binary lockfile loader, a human familiar with the install subsystem should also take a look.

What was reviewed:

  • Confirmed the removed lockfile::StringBuilder::append_with_hash has no remaining callers; the surviving append_with_hash hits are on the separate semver::string::Builder type, and every one of those already passes string_hash(bytes) computed on the line above.
  • Checked the new recompute loop in bun.lockb.rs runs after buffers::load populates string_bytes, so name.slice(string_bytes) reads the loaded buffer.
  • Verified package.name_hash = package_name.hash in from_npm now receives the freshly-computed hash via T::from_pooled(_, hash), not the stale manifest hash.
  • Both regression tests self-validate their hardcoded binary offsets (asserting the read hash matches before corrupting it) and assert positive outcomes (installed package, saved hash), not merely absence of a panic.
Extended reasoning...

Overview

This PR fixes a lockfile string-builder crash where the two-phase count/append passes could diverge when a caller-supplied hash didn't match the hash of the bytes being appended. lockfile::StringBuilder::append_with_hash is removed and its body folded into append, which now always derives the pool key from SemverStringBuilder::string_hash(slice). Six call sites in CatalogMap.rs, OverrideMap.rs, and Package.rs are migrated. bun.lockb.rs's load gains a post-load pass that recomputes each package's name_hash from its name bytes so a stale stored hash cannot desync package_index or later pooling. Two regression tests reproduce both identified sources of a bad stored hash (a corrupted bun.lockb and a corrupted .npm manifest cache entry) and assert the install succeeds and re-saves the correct hash.

Security risks

None identified. The change tightens an invariant (pool key is always derived from the bytes written) rather than loosening it, and the recompute pass only overwrites a derived field with the value it must have. No new untrusted-input parsing surface is added; the tests operate against the local Verdaccio registry via the existing suite fixtures.

Level of scrutiny

Moderate-to-high. The lockfile string pool is load-bearing for every install — a wrong key here silently produces out-of-bounds writes or mis-pooled strings across the whole dependency graph. The fix itself is structurally simple and clearly correct (make the count key and the append key the same by construction), but the added recompute-on-load loop runs for every package on every bun.lockb open, and the removal of append_with_hash shifts where hashes are computed at several call sites. Someone who owns the install code should confirm the per-load rehash cost is acceptable and that no other consumer of the stored name_hash (e.g. package_index population, verify_data) runs before the recompute point.

Other factors

The PR description is unusually thorough — it names the excluded case (dependency-row name_hash, which is counted/appended by bytes and cannot reach this path), lists suites run against the debug build, and documents that no organic mismatch was observed in lockfiles from five prior Bun releases. The remaining append_with_hash callers in yarn.rs, pnpm.rs, npm_lock.rs, bun.lock.rs, CatalogMap.rs, and OverrideMap.rs are on the separate semver::string::Builder type and every one computes string_hash(bytes) immediately before the call, so they cannot exhibit the count/append divergence. The two changed OverrideMap.rs/CatalogMap.rs sites now hash the same slice twice (once for the local name_hash variable, once inside append); that is a minor redundancy, not a correctness concern. No CODEOWNERS entry covers src/install/, and the timeline shows no outstanding third-party objections.

@robobun

robobun commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator Author

On the two questions for a human reviewer:

  • Order: load in src/install/lockfile/bun.lockb.rs recomputes the hashes right after buffers::load. The same function fills package_index after that, in the get_or_put_id loop. Lockfile::verify_data runs after load returns (src/install/lockfile.rs:710). Nothing reads a package's name_hash before the recompute.
  • Cost: one Wyhash11 of each package name per bun.lockb load. Nothing else is added to that path.

@Jarred-Sumner
Jarred-Sumner merged commit 86b2e06 into main Sep 4, 2026
10 of 12 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the robobun/b7df888e/lockb-name-hash branch September 4, 2026 22:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants