Skip to content

install: treat an out-of-range package id in bun.lockb as corruption - #31008

Merged
Jarred-Sumner merged 3 commits into
mainfrom
ali/lockb-corrupt-oob
May 19, 2026
Merged

Jarred-Sumner merged 3 commits into
mainfrom
ali/lockb-corrupt-oob

Conversation

@alii

@alii alii commented May 18, 2026

Copy link
Copy Markdown
Member

A bun.lockb damaged by a bad binary-file merge / interrupted write / disk-full / bit-rot made bun install (and --frozen-lockfile) hard-panic (exit 133). Released Bun tolerates the damaged lockfile, re-resolves, and completes the install + rewrites a good lockfile — a deliberate robustness guarantee the port regressed into a crash.

Package::clone indexed package_id_mapping[self.meta.id as usize] with self.meta.id deserialized verbatim from lockfile bytes and never range-checked; on corruption it's garbage and Rust's slice bounds check panics. Zig's ReleaseFast does the same unchecked write (silent UB) and recovers downstream via re-resolution.

Bounds-check self.meta.id against package_id_mapping.len(); out-of-range → return an InvalidLockfile error, which the installer already handles by falling back to a fresh resolution. Verified end-to-end against the saved corrupt lockfile: bun install (and --frozen-lockfile) now completes (81 packages installed, exit 0) exactly like released Bun instead of panicking. Regression test + corrupt-lockfile fixture added.

@robobun

robobun commented May 18, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 6:18 PM PT - May 18th, 2026

✅ @robobun, your commit 94894b08c80ed2283dc0db039c1f1e6b4bd3ace6 passed in Build #55926! 🎉


🧪   To try this PR locally:

bunx bun-pr 31008

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

bun-31008 --bun

@coderabbitai

coderabbitai Bot commented May 18, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Rate limit exceeded

@robobun has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 22 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: efda7ee5-df78-4d46-ac24-66e139ca7792

📥 Commits

Reviewing files that changed from the base of the PR and between dcd8ffc and 94894b0.

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

Walkthrough

This PR adds a bounds check for self.meta.id in Package<u64>::clone to return InvalidLockfile on out-of-range IDs and adds an integration test that stages a corrupted bun.lockb fixture to verify bun install exits successfully and creates node_modules instead of crashing.

Changes

Lockfile Corruption Handling

Layer / File(s) Summary
Package ID mapping bounds validation
src/install/lockfile/Package.rs
Package<u64>::clone validates self.meta.id against package_id_mapping.len() before indexing, returning Err(bun_core::err!("InvalidLockfile")) if out of bounds.
Integration test for corrupted lockfile handling
test/cli/install/bun-lockb.test.ts, test/cli/install/fixtures/corrupt-lockb/package.json
Harness imports tempDir; new test stages a corrupted bun.lockb with a test package.json, runs bun install --prefer-offline --no-progress, asserts exit code is 0 and not 133, and verifies node_modules exists.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding bounds checking for package IDs in corrupted lockfiles to prevent panics.
Description check ✅ Passed The description provides detailed context about the bug, the fix mechanism, and verification, but lacks explicit sections matching the template structure.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 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 `@test/cli/install/bun-lockb.test.ts`:
- Around line 123-137: The test currently discards child process output by
passing stdout/stderr: "ignore" to spawn, which hides valuable diagnostics;
change the spawn call used in this test (the spawn invocation that returns
exited and the code variable) to capture stdout/stderr (e.g., pipe or collect
streams) and surface them in assertions before or alongside the exit-code checks
so failures include stdout/stderr context; preserve the existing expectations
(expect(code).not.toBe(133), expect(code).toBe(0), and the node_modules exists
check) but reorder or augment the test to first assert or log captured
stdout/stderr when code !== 0 to make CI flakes debuggable.
🪄 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: ce939314-b71a-447e-96ba-1ebde475acc9

📥 Commits

Reviewing files that changed from the base of the PR and between 4c8a21b and db374b3.

⛔ Files ignored due to path filters (1)
  • test/cli/install/fixtures/corrupt-lockb/bun.lockb is excluded by !**/bun.lockb
📒 Files selected for processing (3)
  • src/install/lockfile/Package.rs
  • test/cli/install/bun-lockb.test.ts
  • test/cli/install/fixtures/corrupt-lockb/package.json

Comment thread test/cli/install/bun-lockb.test.ts Outdated
@alii
alii force-pushed the ali/lockb-corrupt-oob branch from db374b3 to dcd8ffc Compare May 18, 2026 20:09
@github-actions

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. Random crash parsing lockfile during runtime #16199 - Random crash parsing lockfile during runtime — segfault in Package.parseWithJSON at a low address, consistent with an out-of-range package ID producing a garbage pointer in the lockfile parsing path this PR hardens
  2. Bun crashes when I do bun install with a bun.lockb file in same path as package.json #23980 - Bun crashes when running bun install with a bun.lockb file — panic: attempt to use null value in package installation triggered by a corrupted lockfile, which the new bounds check on package_id_mapping would catch earlier

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #16199
Fixes #23980

🤖 Generated with Claude Code

Comment thread test/cli/install/bun-lockb.test.ts
@alii

alii commented May 18, 2026

Copy link
Copy Markdown
Member Author

@robobun fix review comments

@robobun

robobun commented May 18, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Review comments addressed in 4a8e403:

  • Regression test now generates bun.lockb against the local Verdaccio registry (no-deps/a-dep) and corrupts meta[1].id in-place — no more npmjs.org network dependency, no committed binary fixture.
  • stdout/stderr piped and asserted (including the Ignoring lockfile warning) before the exit code.
  • Moved the meta.id range check into Serializer::load so InvalidLockfile flows into the existing load-error fallback (warn + re-resolve) instead of aborting from clean_with_logger.

Gate verified: without the src fix the test panics with index out of bounds: the len is 3 but the index is 2147483647 at Package.rs:550; with the fix it re-resolves and installs both packages, exit 0.


CI: bun-lockb.test.ts passes on every lane. Remaining red on #55926 is the three darwin-*-test-bun lanes expiring waiting for an agent plus a known-flaky spawn-pipe-leak.test.ts RSS-threshold test on Windows (retried → passed) — none touch the lockfile parser. Diff is green; ready for a maintainer to re-run macOS / merge.

Comment thread src/install/lockfile/Package.rs Outdated
Comment thread test/cli/install/bun-lockb.test.ts Outdated
robobun added 2 commits May 18, 2026 22:16
… in test

Move the out-of-range meta.id check into Serializer::load where
InvalidLockfile flows into LoadResult::Err and triggers the existing
'Ignoring lockfile' re-resolve fallback, instead of propagating out of
clean_with_logger as an unrecoverable abort.

Rewrite the regression test to generate a bun.lockb against the local
Verdaccio registry (no-deps / a-dep) and surgically corrupt meta[1].id,
so re-resolution never touches npmjs.org. Pipe stdout/stderr and assert
on the 'Ignoring lockfile' warning + installed packages before the exit
code. Drop the unused fixtures/corrupt-lockb/ (bun.lockb was never
committed).

@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.

LGTM — thanks for moving the check into Serializer::load and reworking the test to generate/corrupt the lockfile against local Verdaccio; all prior feedback addressed.

Extended reasoning...

Overview

This PR adds load-time validation that every meta.id in a deserialized bun.lockb is within [0, packages.len()), returning InvalidLockfile if not so the existing LoadResult::Err → report_lockfile_load_error → re-resolve path kicks in instead of a later panic_bounds_check. A second, defense-in-depth check is kept in Package::clone with an updated comment that correctly notes errors at that point abort rather than recover. The regression test now generates a valid lockfile against the local Verdaccio registry (no-deps/a-dep), surgically overwrites meta[1].id with 0x7fffffff, and asserts the install warns "Ignoring lockfile", re-resolves, and exits 0.

Security risks

None. Both source changes are pure additive bounds checks on data already read from disk; they only fire on out-of-range values and return an error that the caller already handles. No new parsing surface, no new trust boundaries, no change to happy-path behaviour.

Level of scrutiny

Lockfile deserialization is a critical install path, but the change here is the most conservative possible: two if id >= len { return Err } guards. The happy path is untouched. The earlier revision had real problems (error returned from a non-recoverable call site, network-dependent test, missing fixture) — all of which I flagged and all of which were fixed in 4a8e403. The author's reply confirms gate testing: without the fix the test panics with index out of bounds: the len is 3 but the index is 2147483647; with it, exit 0.

Other factors

  • All three of my prior inline comments are resolved and the responses match the current diff.
  • The test's hardcoded binary offsets (header at 42/86/110, 88-byte Meta, column ordering) are brittle to future format changes, but the test self-validates with expect(meta[0].id).toBe(0) / expect(meta[1].id).toBe(1) before corrupting, so format drift will fail loudly at the sanity check rather than silently pass.
  • The three CI failures on 4a8e403 (macOS x64 build-cpp, v8-heap-snapshot.test.ts SIGKILL, spawn-stdout-iterate-leak.test.ts) are unrelated to this change.
  • No CODEOWNERS entry covers src/install/lockfile/.
  • No bugs found by the bug-hunting system on this revision.

@Jarred-Sumner
Jarred-Sumner merged commit 08f4ab4 into main May 19, 2026
77 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the ali/lockb-corrupt-oob branch May 19, 2026 03:49
Jarred-Sumner pushed a commit that referenced this pull request Sep 4, 2026
…#41366)

### 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.

<details><summary>Notes</summary>

- 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.
- #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.
- #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/`.

</details>

<!-- robobun:evidence:begin -->

---

**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

<!-- robobun:evidence:end -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants