Skip to content

install: stop aborting on -g when $BUN_INSTALL, $XDG_CACHE_HOME or $HOME does not fit the global directory path buffer - #38413

Open
robobun wants to merge 1 commit into
mainfrom
farm/abcaf0ad/global-dir-env-too-long
Open

robobun wants to merge 1 commit into
mainfrom
farm/abcaf0ad/global-dir-env-too-long

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • Every -g command (bun install -g, bun add -g, bun pm ls -g, bun pm bin -g, bun link, ...) aborts when $BUN_INSTALL, or when that is unset $XDG_CACHE_HOME or $HOME, is longer than the path buffer (4096 bytes on Linux, 1024 on macOS):
    panic: range end index 5000 out of range for slice of length 4095
    
    (SIGABRT, exit 134, top frame bun_paths::resolve_path::normalize_string_generic_tz, src/paths/resolve_path.rs:1071.) Repro on main: BUN_INSTALL=/$(head -c 5000 /dev/zero | tr '\0' a) bun pm ls -g.
  • The same length in $BUN_INSTALL_GLOBAL_DIR or $BUN_INSTALL_BIN already fails cleanly with error: An internal error occurred (ENAMETOOLONG), exit 1: those values are opened as given and openat_a (src/sys/lib.rs:6334) rejects any path of MAX_PATH_BYTES bytes or more.
  • Cause: open_global_dir and open_global_bin_dir in src/install/PackageManager/PackageManagerOptions.rs (lines 319, 332, 367 and 380 on main) join the environment value with install/global, .bun/install/global, bin or .bun/bin using the unchecked join_abs_string_buf into a stack PathBuffer. It normalizes straight into the buffer, so a result that does not fit is an out of bounds slice write instead of an error.

Fix

  • The four joins go through one helper, make_open_dir_under, which uses join_abs_string_buf_checked and returns Error::Sys(ENAMETOOLONG) when the joined path does not fit; otherwise it opens the path exactly as before.
  • Why this is the right answer: a path that does not fit the buffer is at least MAX_PATH_BYTES bytes, which openat_a rejects with ENAMETOOLONG anyway, so the helper reports one layer earlier what a larger buffer would have reported. The error is the same Error::Sys(ENAMETOOLONG) value that make_open_path produces for an oversized $BUN_INSTALL_GLOBAL_DIR / $BUN_INSTALL_BIN, so every caller (PackageManager::init, setup_global_dir, the link/unlink commands) prints the message above and exits 1 without any caller changes.
  • Nothing that worked before changes: the checked join measures the normalized result, so a value that is long as written but normalizes to a path that fits is still used, and a joined path of exactly the buffer size still gets ENAMETOOLONG from openat_a as it does today. Same pattern as install: stop panicking on workspaces entries longer than the path buffer #37531 (workspaces entries) and lockfile/Package.rs (folder dependencies).
  • Tests: test/cli/install/bun-pm.test.ts, describe("global directories longer than the path buffer"), POSIX only (on Windows the buffer holds more than an environment variable can carry). Each case sets every variable the two lookups read, so it reaches exactly the arm it names; $XDG_CONFIG_HOME points at a directory with an .npmrc so the $HOME cases reach this code regardless of the separate .npmrc (install: do not abort when $XDG_CONFIG_HOME or $HOME is too long for the .npmrc path buffer #38372) and bunfig (bunfig: stop panicking when the config path does not fit in a path buffer #38370) joins:
    • bun pm bin -g with an 8 KiB value in each of the six arms (global directory and global bin directory, from $BUN_INSTALL, $XDG_CACHE_HOME, $HOME): ENAMETOOLONG, exit 1
    • bun install -g with an 8 KiB $BUN_INSTALL: same
    • global directory path of exactly the buffer size and one byte above it: ENAMETOOLONG, exit 1
    • a 100 kB $BUN_INSTALL that normalizes to a short directory: bun pm bin -g prints that directory's bin, exit 0
  • Without the fix (USE_SYSTEM_BUN=1, and a debug build with src/ stashed) 8 of the 10 cases fail with the panics above (range end index 8192, and 4096 for the one byte above case); the exactly-the-buffer-size and normalized cases pass both ways and pin down the boundary. With the fix the whole file passes (28 tests), as do the -g cases of bun-install-registry.test.ts; cargo clippy -p bun_install and rustfmt are clean.
  • Related: install: stop panicking when the cache directory setting does not fit the path buffer #38390 fixes the same class of bug for the cache directory and also appends tests to bun-pm.test.ts, so whichever lands second needs a trivial rebase. install: skip non-absolute $BUN_INSTALL when locating global dirs #32515, install: load bunfig before resolving globalDir for -g installs #35454 and install: stop resolving global install dirs from $XDG_CACHE_HOME #36492 touch these two functions for other reasons and keep the unchecked join, so this is independent of them.

Background

  • PathBuffer is bun's fixed stack buffer for building syscall paths, MAX_PATH_BYTES long (the platform PATH_MAX: 4096 on Linux, 1024 on macOS, about 96 KiB on Windows). openat_a copies a path into one and adds the NUL, so the longest path it accepts is MAX_PATH_BYTES - 1 bytes.
  • resolve_path::join_abs_string_buf(base, buf, parts) is path.resolve into a caller buffer. It assumes the normalized result fits and indexes buf accordingly. join_abs_string_buf_checked is the variant for input of unbounded length: it normalizes first (into heap scratch when the input is large) and returns None instead of writing when the result is longer than buf.
  • Global directories: for -g commands PackageManager::init opens the global directory (where the global package.json and node_modules live) and setup_global_dir opens the global bin directory (where bin links are created). The first choice for each is a directory named outright ($BUN_INSTALL_GLOBAL_DIR / bunfig globalDir, $BUN_INSTALL_BIN / bunfig globalBinDir), opened as given; the remaining choices are derived by joining $BUN_INSTALL, else $XDG_CACHE_HOME, else $HOME with a fixed suffix, which is the code changed here.
Probes on the release build of main
$ LONG=/$(head -c 8192 /dev/zero | tr '\0' a)
$ BUN_INSTALL=$LONG bun pm bin -g
panic: range end index 8192 out of range for slice of length 4095        (exit 134)
$ XDG_CACHE_HOME=$LONG bun pm bin -g                                       # BUN_INSTALL unset
panic: range end index 8192 out of range for slice of length 4095        (exit 134)
$ BUN_INSTALL_GLOBAL_DIR=<dir with package.json> BUN_INSTALL=$LONG bun pm bin -g   # bin dir arm
panic: range end index 8192 out of range for slice of length 4095        (exit 134)
$ BUN_INSTALL_GLOBAL_DIR=$LONG bun pm bin -g                               # opened as given
error: An internal error occurred (ENAMETOOLONG)                         (exit 1)
$ BUN_INSTALL_BIN=$LONG BUN_INSTALL_GLOBAL_DIR=<dir> bun pm bin -g         # opened as given
error: An internal error occurred (ENAMETOOLONG)                         (exit 1)

# Boundary, global directory arm (value + "/install/global"), Linux:
joined length 4096 -> error: An internal error occurred (ENAMETOOLONG)   (exit 1)
joined length 4097 -> panic: range end index 4096 out of range for slice of length 4095
joined length 4098 -> panic: range end index 4097 out of range for slice of length 4095

# Control: with both directories named outright, an 8 KiB $HOME, $XDG_CACHE_HOME or
# $BUN_INSTALL is harmless ($XDG_CONFIG_HOME holding an .npmrc), so the test cases
# above isolate these two functions.

… $BUN_INSTALL, $XDG_CACHE_HOME or $HOME do not fit the path buffer

open_global_dir and open_global_bin_dir joined the environment value with
the unchecked join_abs_string_buf, which indexes past the PathBuffer when
the joined path is longer than MAX_PATH_BYTES, so every -g command
aborted with "panic: range end index N out of range for slice of length
4095". Join through join_abs_string_buf_checked and fail with
Error::Sys(ENAMETOOLONG), the error an oversized $BUN_INSTALL_GLOBAL_DIR or
$BUN_INSTALL_BIN already produces.
@coderabbitai

coderabbitai Bot commented Aug 14, 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: 41 minutes

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: 59675b7a-4f50-451d-b08d-2552a1faf03f

📥 Commits

Reviewing files that changed from the base of the PR and between 01aa7cd and 244a51e.

📒 Files selected for processing (2)
  • src/install/PackageManager/PackageManagerOptions.rs
  • test/cli/install/bun-pm.test.ts

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

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Status: fix and tests pushed (244a51e), waiting on CI.

Reproduced on the release build of main with BUN_INSTALL=/$(head -c 8192 /dev/zero | tr '\0' a) bun pm bin -g (and the $XDG_CACHE_HOME / $HOME arms, for both the global directory and the global bin directory): panic: range end index 8192 out of range for slice of length 4095, exit 134. With this branch every arm reports error: An internal error occurred (ENAMETOOLONG) and exits 1, the same as an oversized $BUN_INSTALL_GLOBAL_DIR does today.

The new cases in test/cli/install/bun-pm.test.ts fail 8/10 on the unfixed build (the two that pass both ways pin the boundary and the normalized-length behavior) and pass with the fix.

@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 — swaps the unchecked path join for the checked variant and folds the four identical blocks into one helper, matching the established pattern.

What was reviewed:

  • make_open_dir_under mirrors the existing join_abs_string_buf_checked usage in lockfile/Package.rs and WorkspaceMap.rs; the Error::Sys(ENAMETOOLONG) return matches what make_open_path already produces for the direct-path arms.
  • Tests: all six env-var arms covered, boundary at exactly/one-above buffer size, and a positive control (100 KB value that normalizes short) pinning that the checked join measures the normalized result — hermetic, concurrent, POSIX-gated with reason.
  • Checked that the 100 KB x/../ fill divides evenly so it normalizes back to ${dir}, and that returning the disposed tempDir path from runGlobal is only used for string comparison.
Extended reasoning...

Overview

Two files. In src/install/PackageManager/PackageManagerOptions.rs, the four call sites in open_global_dir and open_global_bin_dir that joined an environment value ($BUN_INSTALL / $XDG_CACHE_HOME / $HOME) with a fixed suffix using the unchecked join_abs_string_buf are replaced by a single private helper make_open_dir_under that uses join_abs_string_buf_checked and returns crate::Error::Sys(ENAMETOOLONG) when the normalized result does not fit the PathBuffer. The open call (Dir::cwd().make_open_path(...).map(|d| d.into_raw()).map_err(Into::into)) is byte-identical to what each site did before. In test/cli/install/bun-pm.test.ts, a new describe.concurrent.skipIf(isWindows) block adds ten cases exercising every arm plus the exact boundary and a normalization control.

Security risks

None. The change strictly reduces attack surface: an environment-variable-driven out-of-bounds slice write (panic in release, but still a bounds violation) is replaced by an early ENAMETOOLONG error. No new inputs are read, no new syscalls are made, and the error value is the same one the adjacent $BUN_INSTALL_GLOBAL_DIR / $BUN_INSTALL_BIN arms already produce via openat_a.

Level of scrutiny

Low-to-medium. The Rust change is a mechanical refactor plus a one-function swap (join_abs_string_buf → join_abs_string_buf_checked), and that swap follows precedent already in the crate at lockfile/Package.rs:1735 and lockfile/Package/WorkspaceMap.rs:218,446 (referenced in the PR as #37531). The helper is private, the callers are unchanged, and crate::Error::Sys(bun_errno::SystemErrno::ENAMETOOLONG) is used verbatim elsewhere in src/install/bin.rs. The success path is provably identical because join_abs_string_buf_checked returns the same slice join_abs_string_buf would have when it fits.

Other factors

The tests are unusually thorough for this class of fix: they set every variable both lookups read (including $XDG_CONFIG_HOME with an .npmrc to keep the separate bunfig/npmrc joins from interfering), so each case reaches exactly the arm it names; they cover both entry points (bun pm bin -g and bun install -g); they pin the boundary at buffer-size and buffer-size+1; and the normalization control proves no regression for long-but-normalizable values. They follow harness conventions (tempDir, Buffer.alloc(n, fill), concurrent stdout/stderr/exited drain, describe.concurrent, exit-code asserted last). The PR description documents that 8/10 fail on main and all pass with the fix. No prior reviews or outstanding comments on the timeline.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 2:05 AM PT - Aug 14th, 2026

@robobun, your commit 244a51e is building: #95655

@robobun

robobun commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 5:05 AM PT - Aug 14th, 2026

❌ @robobun, your commit 244a51e has some failures in Build #95655 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 38413

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

bun-38413 --bun

This branch has not been deployed

No deployments
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.

1 participant