Skip to content

install: stop panicking on package labels longer than the stack buffers - #37469

Merged
Jarred-Sumner merged 1 commit into
mainfrom
farm/540c80a5/install-long-resolution-panic
Aug 13, 2026
Merged

Jarred-Sumner merged 1 commit into
mainfrom
farm/540c80a5/install-long-resolution-panic

Conversation

@robobun

@robobun robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Repro

mkdir probe && cd probe
cp <bun repo>/test/cli/install/bar-0.0.2.tgz .
bun -e 'require("fs").writeFileSync("package.json", JSON.stringify({ name: "foo", dependencies: { bar: "./" + "x/../".repeat(130) + "bar-0.0.2.tgz" } }))'
bun install
Resolved, downloaded and extracted [1]
panic: unreachable: Error

bun install aborts (SIGABRT, exit 134) after the tarball has been resolved and extracted. The spec normalizes to ./bar-0.0.2.tgz, which exists; a real 618 byte relative path behaves the same (that is how this was found, see #37462). The same abort happens for a remote tarball whose URL is longer than 512 bytes, a file: folder at a long path, and a workspace package whose version has a ~500 byte prerelease tag. --lockfile-only succeeds, so it is the link step. Reproduced with bun 1.4.0-canary.1 and main.

Top frame: Result::expect in PackageInstaller::install_package_with_name_and_resolution (src/install/PackageInstaller.rs:1340), called from install_package / hoisted_install::install_hoisted_packages.

Cause

While linking each package, the hoisted installer formats the package's version label into let mut resolution_buf = [0u8; 512] with buf_print(..).expect("unreachable"). For npm packages the label is the version; for tarball, folder and git packages it is the spec they were resolved from (stored verbatim), and for workspace packages it is the workspace's own version. Those are user supplied and have no length bound, so the overflow buf_print reports is reachable and the expect turns it into the panic. Both branches (workspace version and resolution) have it.

The isolated linker already builds this label in a Vec (Installer::package_patch_info) and is not affected by this panic. It fails such installs with ENAMETOOLONG instead, because the store directory name embeds the spec; that is a separate problem and not touched here.

Fix

print_package_version keeps the 512 byte stack buffer as the allocation-free path and, only when the label does not fit, formats it into a Vec that lives next to the buffer (the same shape as resolve_path::join_z_buf_spill). The linking loop runs once per installed package, so the common case still does not allocate.

Spilling is the only correct behavior for this label: it is compared against the installed package.json version and hashed as the version half of the name@version patchedDependencies key, so a truncated label would silently fail verification or miss a patch, and rejecting the package would refuse a valid install (nothing on disk is named after the label; the tarball cache folder is a hash of it). The new patchedDependencies test below checks the spilled label byte for byte by keying a patch with it.

bun patch formats the same labels into 1024 byte buffers with the same expect at four sites in src/install/PackageManager/patchPackage.rs, all reproducible the same way once the install succeeds (verified with a build that only had the installer change):

  • pkg_info_for_name_and_version (bun patch <name>@<version> compares the label of every package with that name; panicked with Resolution name too long),
  • the multiple-packages-with-this-name loops in prepare_patch and do_patch_commit (bun patch <path> when another package with the same name has a long label),
  • the name@label key in do_patch_commit, which is also the patch file name.

These format into a Vec (print_resolution_label, reused across a candidate loop). In do_patch_commit the key Vec is returned as the patch_key directly and the file name is built from it, replacing the re-slicing of the shared buffer; the bytes are unchanged (the key was already valid UTF-8, so the former BStr round trip was a copy). Committing a package whose label is this long still cannot succeed, since the patch file would be named after the label, but it now exits 1 with an error instead of aborting; today it fails before that at the diff step for tarball packages (#37124), which this does not change either way.

Tests

test/cli/install/bun-workspaces.test.ts, "packages whose version label is longer than 512 bytes" (hoisted linker): local tarball, remote tarball served from a local Bun.serve, workspace package with a long prerelease version (the other branch), and a patchedDependencies entry keyed by the long spec that must actually be applied. test/cli/install/bun-patch.test.ts, "packages whose label is longer than 1024 bytes": bun patch <name>@<label>, bun patch <path> plus --commit when a same-named package has a long label (the long one is listed first, so the loops format it), and --commit of a long-labeled package exiting 1 without touching package.json. The long specs use x/../ repeated so nothing long is ever created on disk.

All seven fail on the unfixed build (panic: unreachable: Error during the install) and pass with the fix; with only the installer change applied, the three bun patch tests fail at the bun patch step instead, so each patch site is covered on its own. Both files pass in full, as does bun-install-patch.test.ts; cargo clippy -p bun_install is clean and cargo check -p bun_install passes for x86_64-pc-windows-msvc and x86_64-apple-darwin.

The hoisted installer formats each package's version label (its version,
or the spec a tarball/folder/git package was resolved from) into a 512
byte stack buffer and treated overflow as unreachable, so any dependency
with a long enough spec aborted `bun install`. Keep the stack buffer for
the common case and spill into a Vec when the label does not fit.

`bun patch` and `bun patch --commit` formatted the same labels into 1024
byte buffers at four sites with the same failure mode; format them into
a Vec instead.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3cd03b2f-6026-4fee-a539-ad2977f5cbce

📥 Commits

Reviewing files that changed from the base of the PR and between da3851e and 26900a1.

📒 Files selected for processing (4)
  • src/install/PackageInstaller.rs
  • src/install/PackageManager/patchPackage.rs
  • test/cli/install/bun-patch.test.ts
  • test/cli/install/bun-workspaces.test.ts

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

@robobun

robobun commented Aug 11, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: reproduced on bun 1.4.0-canary.1 and main with the repro in the description (bun install of a tarball dependency whose spec is longer than 512 bytes aborts with panic: unreachable: Error after extraction; same for a long tarball URL, a file: folder at a long path and a workspace package with a long prerelease version). Fix and tests are in this PR; the seven new tests fail on the unfixed build and pass with it. CI is green (build #92169).

@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 — replaces reachable .expect() panics on fixed-size label buffers with a spill-to-Vec fallback, keeping the allocation-free fast path.

What was reviewed:

  • print_package_version spill pattern matches resolve_path::join_z_buf_spill; Arguments is Copy so re-formatting into the Vec after buf_print fails is sound, and resolution_spill is fresh at each call site.
  • do_patch_commit refactor is byte-for-byte equivalent for patch_key and string_hash; the .patch filename now built via .concat() also removes a latent slice-index panic at ~1018-byte labels.
  • Checked all four patchPackage.rs sites and both PackageInstaller.rs branches; no remaining fixed-buffer .expect() on the resolution label.
  • Tests cover each site independently, use x/../ so nothing long touches disk, follow harness conventions (VerdaccioRegistry, port: 0, concurrent pipe drain, Buffer.alloc).
Extended reasoning...

Overview

The PR fixes a user-reachable panic in bun install and bun patch when a package's version label (tarball spec, remote URL, folder path, git ref, or workspace prerelease tag) exceeds the fixed 512/1024-byte stack buffers it was formatted into with .expect("unreachable"). Two Rust files are touched: PackageInstaller.rs gains a print_package_version helper that tries the stack buffer first and spills into a caller-owned Vec on overflow; patchPackage.rs gains print_resolution_label (clear-and-reuse Vec) applied at four sites, plus a small refactor of do_patch_commit to build patch_key as a Vec directly and derive the patch filename from it. Seven new tests across two files exercise every changed site.

Security risks

None. The change replaces a panic with correct handling of long user-supplied strings; nothing new is written to disk with the long label (the tests deliberately use x/../ so paths normalize short), and the label is only compared/hashed. No parsing, auth, or trust boundary is touched.

Level of scrutiny

Medium. This is package-manager code on the install path, but the change is a targeted crash fix with a well-understood shape (buf-then-spill, matching resolve_path::join_z_buf_spill). The do_patch_commit refactor was checked for byte equivalence: the old code formatted name@resolution into a stack buffer, hashed it, appended .patch in place, then copied the prefix into a fresh Vec for patch_key; the new code formats directly into the patch_key Vec, hashes it, and builds the filename via [patch_key, b".patch"].concat(). The bytes and hash input are identical, and the in-place suffix write (which itself would have panicked at ~1018 bytes) is gone.

Other factors

  • core::fmt::Arguments is Copy, so passing args to both buf_print and write_fmt is sound.
  • resolution_spill is declared fresh before the workspace/non-workspace branch and only one arm runs, so the helper never sees a non-empty spill.
  • print_resolution_label clears its Vec first, so reuse across candidate loops is correct and allocation-free after the first grow.
  • The .expect("formatting into a Vec is infallible") calls are true invariants: Vec<u8>: io::Write cannot fail, and the Display impls only propagate writer errors.
  • Tests follow repo conventions: added to existing files, test.concurrent, Buffer.alloc(n, fill) instead of .repeat(), local Bun.serve({ port: 0 }) for the remote-tarball case, VerdaccioRegistry for npm packages, pipes drained concurrently, content asserted before exit codes. The PR description confirms each test fails on the unfixed build and that the patch tests fail independently with only the installer fix applied.
  • No prior review comments; no CODEOWNER-gated paths.

@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 3:56 AM PT - Aug 11th, 2026

✅ @robobun, your commit 26900a1f1843a7f5bec01d6817e610a428c306fd passed in Build #92169! 🎉


🧪   To try this PR locally:

bunx bun-pr 37469

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

bun-37469 --bun

@Jarred-Sumner
Jarred-Sumner merged commit 2362578 into main Aug 13, 2026
53 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/540c80a5/install-long-resolution-panic branch August 13, 2026 01:44
@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

The bun patch tests added here share one install cache in CI (BUN_INSTALL_CACHE_DIR overrides the bunfig cache) and two of them install the same spec concurrently, which fails one of them on Windows; #38255 gives each project its own cache.

Jarred-Sumner pushed a commit that referenced this pull request Oct 1, 2026
)

### Problem

- `bun pm ls --all` (and `bun list --all`) aborts with `panic:
buf_print: buffer too small` (exit 134) as soon as it reaches a package
whose resolution is longer than 512 bytes, for example a dependency
installed from a tarball URL with a ~600 character path. Plain `bun pm
ls` prints the same package fine.
- Reproduced with bun 1.4.0-canary.1 and main: serve a `.tgz` from a
local `Bun.serve`, depend on it as `http://127.0.0.1:PORT/aaaa...(600
chars)/bar-0.0.2.tgz`, `bun install` (succeeds), `bun pm ls --all`
prints the root line and `├── ` and panics.
- Cause: the `--all` tree printer
(`print_node_modules_folder_structure`,
`src/runtime/cli/package_manager_command.rs`) formatted each resolution
into `let mut resolution_buf = [0u8; 512]` with `buf_print_infallible`,
which panics on overflow. Two sites: the header line of a package that
owns a nested `node_modules` folder (formerly line 764) and the line of
every other package (formerly line 890).
- A tarball, folder or git resolution repeats the user supplied spec
verbatim, so there is no length a stack buffer can be sized to. #37469
fixed the same class of panic in the installer and `bun patch`; this
printer was left out.

### Fix

- Pass the resolution's `Display` formatter
(`resolutions[id].fmt(string_bytes, PathSep::Auto)`) straight to
`prettyln!` at both sites and delete the buffer and the now unused
`buf_print_infallible` import.
- Correct because the formatted bytes were only ever printed: the buffer
added no information, and removing it removes the only length limit on
the path.
- Output is unchanged for every label that used to fit. The old code
formatted the same `Display` impl into the buffer and re-printed it
through `BStr`, and `BStr` prints the valid UTF-8 that `Display`
produces byte for byte, so the only observable difference is on labels
that used to panic.
- This is already how the other two `pm ls` paths print the label: the
top-level listing (`bun pm ls`) and `--all --trusted`
(`print_trusted_dependencies_flat`) both pass the formatter directly,
which is why they never had the problem.
- Verified with `test/cli/install/bun-pm.test.ts`, "should list all
dependencies when a resolution is longer than 512 bytes": the root
depends on two tarballs with ~620 byte URLs plus `baz@0.0.5`; one
tarball (`moo`) depends on versions of `bar` and `baz` that conflict
with the root's, so it owns a nested `node_modules` and its label goes
through the header site, while the other tarball (`bar`) goes through
the per-package site. The test asserts the full tree. It fails on the
unfixed binary with the panic above and passes with the fix; the rest of
`bun-pm.test.ts` and
`test/regression/issue/24502/bun-pm-ls-all-invalid-package-id.test.ts`
still pass. `cargo fmt --check` and `cargo clippy -p bun_runtime` are
clean.

### Background

- A package's resolution is the lockfile's record of what it was
installed from: a version for registry packages, otherwise the spec
itself (tarball URL or path, folder path, git reference).
`Resolution::fmt` returns a `Display` formatter that renders it as the
`@...` label `bun pm ls` prints after each package name.
- `bun pm ls --all` walks the lockfile's hoisted tree one `node_modules`
folder at a time. Each folder's header names the package that owns it,
and each package inside the folder that does not own a folder of its own
gets a line of its own; those are the two print sites.
- `buf_print_infallible` (`src/bun_core/fmt.rs`) formats into a caller
supplied slice and panics if the output does not fit; it is meant for
buffers whose contents have a known bound. The Zig version of this code
used `std.fmt.bufPrint` with `try`, so the same overflow used to surface
as an error instead of a panic, but `--all` failed either way.
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.

2 participants