Skip to content

install: do not abort when $XDG_CONFIG_HOME or $HOME is too long for the .npmrc path buffer - #38372

Open
robobun wants to merge 1 commit into
mainfrom
farm/18412571/npmrc-home-path-length
Open

install: do not abort when $XDG_CONFIG_HOME or $HOME is too long for the .npmrc path buffer#38372
robobun wants to merge 1 commit into
mainfrom
farm/18412571/npmrc-home-path-length

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • bun install, bun pm ..., bun publish and the other commands that go through PackageManager::init abort at startup when $XDG_CONFIG_HOME (or, when that directory has no .npmrc, $HOME) is MAX_PATH_BYTES - 7 bytes or longer (4089 on Linux, 1017 on macOS):
    • exactly MAX_PATH_BYTES - 7 bytes: panic: index out of bounds: the len is 4096 but the index is 4096
    • longer: panic: range end index 5000 out of range for slice of length 4095, top frame bun_paths::resolve_path::normalize_string_generic_tz (src/paths/resolve_path.rs:1071), called from bun_install::package_manager_real::init
  • Cause: src/install/PackageManager.rs:1887 and :1894 join the environment value and .npmrc into a stack PathBuffer with the unchecked join_abs_string_buf_z. It normalizes into the buffer without a size check and then writes the NUL terminator at result.len(), so a result of exactly MAX_PATH_BYTES - 1 bytes fails on the NUL write and anything longer overflows inside the normalization.
  • Reproduced on the release build of main (XDG_CONFIG_HOME=<short dir without .npmrc> HOME=/aaaa...<5000 bytes> bun pm cache in any directory with a package.json, exit 134). The values are environment input, so this should never be a crash.

Fix

  • New user_npmrc_path(dir, buf) builds dir/.npmrc with join_abs_string_buf_checked into buf[..MAX_PATH_BYTES - 1], writes the NUL itself, and returns None when the result does not fit. Both candidates ($XDG_CONFIG_HOME, then $HOME) use it; None leaves global_len at 0, which is the existing "no user-level .npmrc" path.
  • Why treating it as absent is right: a path that does not fit is MAX_PATH_BYTES bytes or longer, which open() rejects with ENAMETOOLONG (bun_sys::openat_a checks the same >= MAX_PATH_BYTES bound itself), and load_npmrc_config already skips user-level candidates that fail to open (src/ini/lib.rs:1232). So the outcome is exactly what a larger buffer would have produced, and no .npmrc that could be read before is skipped now: the longest directory that fits, MAX_PATH_BYTES - 8 bytes, still loads, and its .npmrc path is the longest one open() accepts.
  • The parts literal changes from ./.npmrc to .npmrc; the join normalizes both to the same path.
  • Verified with three new cases in test/cli/install/npmrc.test.ts ("user .npmrc lookup > $HOME longer than the path buffer"): the longest $HOME that fits (built as a real directory of exactly MAX_PATH_BYTES - 8 bytes) still has its .npmrc honored, $HOME one byte longer is skipped, $HOME longer than the whole buffer is skipped. On the unfixed build the last two fail with the two panics above; with the fix all 34 tests in the file pass. Skipped on Windows, where the buffer (~96 KiB) is longer than any environment value.
  • The tests drive the $HOME candidate (with $XDG_CONFIG_HOME pointing at a short directory) because the global .bunfig.toml lookup in src/bunfig/arguments.rs reads $XDG_CONFIG_HOME (or $HOME when it is unset) before this code with the same unchecked join and a longer file name, so an oversized value there still aborts earlier; bunfig: stop panicking when the config path does not fit in a path buffer #38370 fixes that site. Both candidates here share user_npmrc_path, and the existing lookup tests in the same describe cover the $XDG_CONFIG_HOME candidate when the path fits.
  • cargo clippy -p bun_install and rustfmt --check are clean.

Background

  • PathBuffer is bun's fixed stack buffer for path syscalls, MAX_PATH_BYTES long (the platform PATH_MAX: 4096 on Linux, 1024 on macOS). Paths in it are NUL-terminated, so the longest path it can hold is MAX_PATH_BYTES - 1 bytes, which is also the longest path open() accepts.
  • resolve_path::join_abs_string_buf_z(cwd, buf, parts) is path.resolve into a caller buffer plus a NUL terminator; it assumes the result fits. join_abs_string_buf_checked is the variant for input of unbounded length: it normalizes first and returns None instead of writing when the result is longer than buf, so it has to be given a buffer one byte short of the NUL slot the caller fills in.
  • User-level .npmrc lookup: PackageManager::init uses $XDG_CONFIG_HOME/.npmrc when that file exists, otherwise $HOME/.npmrc (install: fall back to $HOME/.npmrc when $XDG_CONFIG_HOME is set #36289), and passes the result together with the project .npmrc to load_npmrc_config, which ignores candidates it cannot read.

…E does not fit the path buffer

PackageManager::init joined $XDG_CONFIG_HOME and $HOME with .npmrc into a
stack PathBuffer using the unchecked join, so a value of MAX_PATH_BYTES - 7
bytes or longer aborted every install-family command with an index out of
bounds panic. Build the candidate with join_abs_string_buf_checked and treat
a path that does not fit like a missing file, which is what opening it would
have reported (ENAMETOOLONG) anyway.
@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: 14 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: 24f49ce8-88d9-4973-a785-0c66abf340e9

📥 Commits

Reviewing files that changed from the base of the PR and between abe3007 and d76ead5.

📒 Files selected for processing (2)
  • src/install/PackageManager.rs
  • test/cli/install/npmrc.test.ts

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

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 3:07 AM PT - Aug 14th, 2026

@robobun, your commit d76ead521fb4fa4ac4dc0ea417b3a7a553d0ac18 passed in Build #95513! 🎉


🧪   To try this PR locally:

bunx bun-pr 38372

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

bun-38372 --bun

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Status: fix and tests pushed, waiting on CI.

Reproduced on the release build of main (any directory with a package.json):

mkdir -p /tmp/xdg   # no .npmrc in it
XDG_CONFIG_HOME=/tmp/xdg HOME="/$(head -c 5000 /dev/zero | tr '\0' a)" bun pm cache
# panic: range end index 5000 out of range for slice of length 4095   (exit 134)

With this branch the same command prints the cache directory and exits 0. bun bd test test/cli/install/npmrc.test.ts: 34 pass; on the unfixed build the two new oversized cases fail with the panics quoted in the description and the longest-fitting case passes on both.

An oversized $XDG_CONFIG_HOME (or $HOME with $XDG_CONFIG_HOME unset) still aborts on this branch, one step earlier, in the global .bunfig.toml lookup; that site is #38370.

@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 — targeted fix that swaps the unchecked path join for the existing checked variant, converting a panic on oversized $XDG_CONFIG_HOME/$HOME into the same "no user .npmrc" path that an unopenable file already takes.

What was reviewed:

  • Verified join_abs_string_buf_checked semantics against src/paths/resolve_path.rs: the slow path leaves buf untouched on None, so a failed XDG candidate does not corrupt the buffer before the HOME candidate runs.
  • Checked NUL-terminator arithmetic: buf[..MAX_PATH_BYTES-1] reserves the last byte, and buf[len] = 0 with len ≤ MAX_PATH_BYTES-1 is always in bounds; the downstream ZStr::from_buf(&buf[..], global_len) at line 1916 still sees the NUL it expects.
  • Confirmed the ./.npmrc.npmrc change is behavior-preserving (both normalize identically).
  • Tests cover the exact-fit boundary, one-past, and far-past cases; the boundary test builds a real 4088-byte directory to prove no readable .npmrc is newly skipped.
Extended reasoning...

Overview

This PR fixes a panic in PackageManager::init (src/install/PackageManager.rs) when $XDG_CONFIG_HOME or $HOME is long enough that joining /.npmrc overflows the fixed PathBuffer. The fix extracts a small helper user_npmrc_path that uses the pre-existing resolve_path::join_abs_string_buf_checked instead of the unchecked _z variant, reserving one byte for the NUL terminator and returning None when the result doesn't fit. Both call sites (XDG and HOME) now go through the shared helper. Three new test cases in test/cli/install/npmrc.test.ts cover the boundary, one-byte-over, and far-over cases.

Security risks

None. The change converts a crash on attacker-controllable environment input into a graceful "file not found" — strictly a DoS-hardening improvement. No new codepaths read or write user data, no auth/crypto/permissions surface is touched, and the outcome for an oversized path is identical to what open() would return anyway (ENAMETOOLONG), which load_npmrc_config already handles by skipping.

Level of scrutiny

Low-to-medium. This is a small (~25 net lines of Rust), mechanical substitution of one join helper for its checked sibling — a pattern that already exists in-tree for exactly this purpose. I traced through join_abs_string_buf_checked in src/paths/resolve_path.rs to confirm it doesn't scribble on buf when returning None (it uses a separate scratch vec), so the XDG-then-HOME sequence can't leave stale data that gets misread. The NUL-terminator bookkeeping is correct: max len is MAX_PATH_BYTES - 1, so buf[len] = 0 never indexes past the buffer, and ZStr::from_buf's debug assertions (len < buf.len(), buf[len] == 0) hold.

Other factors

  • The PR description is unusually thorough: it names both panic messages, cites exact line numbers, explains why treating oversized as absent is semantically correct (matches openat_a's own >= MAX_PATH_BYTES check), and documents why the tests drive $HOME rather than $XDG_CONFIG_HOME (a sibling bug in arguments.rs fires earlier on that path — tracked separately in #38370).
  • Tests follow harness conventions: tempDir with using, it.concurrent, Buffer.alloc instead of .repeat(), combined {stdout, stderr, exitCode} assertions via the existing publishDryRun/usesRegistry helpers, and a skipIf(isWindows) with a stated reason.
  • The boundary test ("longest $HOME that fits") is a real regression guard — it proves the fix didn't shrink the set of readable .npmrc files by one byte.
  • No CODEOWNERS entry covers src/install/. No prior human reviews or outstanding comments on the PR.

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. bun test: stop panicking on a path argument or tree entry longer than the path buffer #35863 - Fixes the same normalize_string_generic_tz overflow panic at the primitive level (_join_abs_string_buf, including the _z/NUL-slot variant), which would make the abort at the .npmrc join site in PackageManager::init unreachable without a call-site change.

🤖 Generated with Claude Code

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Not a duplicate of #35863, though the two are related.

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