bunfig: stop panicking when the config path does not fit in a path buffer

Merged
RO
2mo ago
+270-47

Problem

  • bun x.cjs, bun -e 0 and every command in ALWAYS_LOADS_CONFIG (bun test, bun build, bun install, bun pm, ...) crash at startup when the working directory is 4084..4095 bytes long on Linux (1012..1023 on macOS, where MAX_PATH_BYTES is 1024):
    • cwd of exactly MAX_PATH_BYTES - 12 bytes: panic: index out of bounds: the len is 4096 but the index is 4096
    • longer: panic: range end index 4101 out of range for slice of length 4095 (end index = cwd length + 11), top frame bun_paths::resolve_path::normalize_string_generic_tz (src/paths/resolve_path.rs:1071), called from bun_bunfig::arguments::load_config
  • The same panics happen for --config=<value> when the value (absolute) or cwd + value (relative) is MAX_PATH_BYTES bytes or longer, and for bun install / bun pm ... when $XDG_CONFIG_HOME (or $HOME) is MAX_PATH_BYTES - 13 bytes or longer.
  • Cause, all in src/bunfig/arguments.rs:
    • load_config joined cwd + config name into a stack PathBuffer with the unchecked join_abs_string_buf (L213) and wrote the NUL at config_buf[len] (L217). A result of exactly MAX_PATH_BYTES bytes fits the join but the NUL write is out of bounds; anything longer overflows inside the join.
    • The absolute --config arm copied the argument into the buffer without a length check (L192).
    • get_home_config_path joined $XDG_CONFIG_HOME / $HOME + .bunfig.toml with the unchecked join_abs_string_buf_z (L24, L30), which writes the NUL itself.
  • Reproduced on the release build of main. The cwd case only needs bun -e 0 from a directory of that length: the path is built before anything is opened, so no bunfig.toml has to exist.

Fix

  • join_config_path builds dir/name with join_abs_string_buf_checked into buf[..len - 1] (reserving the NUL slot) and returns None when the result does not fit; the cwd join and get_home_config_path both use it. The absolute --config arm length-checks and uses resolve_path::z, the guard-then-z shape its other callers use.
  • A path that does not fit takes the existing "config failed to open" route in load_bunfig, factored into unreadable_config: auto-loaded configs (bunfig.toml in the cwd, the global .bunfig.toml) are skipped, as they already are on any read error; an explicit --config exits 1 with ENAMETOOLONG: <path>: File name too long (open()) / while reading config "<path>", the message a kernel ENAMETOOLONG on the same flag already produces.
  • Why this is correct: open() rejects any path of MAX_PATH_BYTES bytes or more (bun_sys::openat_a returns ENAMETOOLONG for the same condition before the syscall), so no file at such a path could have been loaded; the failure now takes the existing route instead of overflowing the buffer. join_abs_string_buf_checked normalizes before deciding, so a long --config value that normalizes to a path that fits still loads. For bun <file> / bun -e, run_command additionally loads bunfig.toml relative to the cwd when load_config loaded nothing, so a bunfig.toml in such a deep directory is still picked up there.
  • The cwd is no longer also passed as the first parts entry: join_abs_string_buf already uses it as the base, so the result is identical, and the copy would have made the checked join count the cwd twice in its size estimate.
  • Verified with test/config/bunfig/bunfig-errors.test.ts (15 new tests): on the unfixed build the 10 overflow cases fail with the panics above and the 5 boundary/normalization controls pass; with the fix all 20 tests in the file pass. Per site they cover the longest path that still loads (exactly MAX_PATH_BYTES - 1 bytes, asserted through the absolute path in the parse error), the MAX_PATH_BYTES case that failed on the NUL write, and a longer one, for bun -e and bun file.js in a deep cwd, relative and absolute --config, and $XDG_CONFIG_HOME / $HOME through bun pm cache. Skipped on Windows, where the buffer (~96 KiB) is longer than any path, argument or environment value the OS accepts.
  • Also green with the fix: test/config/bunfig/preload.test.ts, test/cli/install/bun-run-bunfig.test.ts, test/cli/bunfig-test-options.test.ts, test/cli/install/npmrc.test.ts, the global-bunfig tests in test/cli/install/minimum-release-age.test.ts; cargo clippy -p bun_bunfig and cargo fmt are clean.
  • Out of scope, reported separately: bun install / bun pm with $XDG_CONFIG_HOME / $HOME of MAX_PATH_BYTES - 7 bytes or longer still panic one step later in the .npmrc lookup (src/install/PackageManager.rs, same unchecked join), so the global-config tests here stay below that length; --cwd <over-long value> (src/runtime/cli/Arguments.rs) and process.chdir() into a directory of exactly MAX_PATH_BYTES - 1 bytes are the same bug class at other sites. Working directories of MAX_PATH_BYTES bytes or more fail in getcwd before this code runs; #38363 covers those.

Background

  • PathBuffer is bun's fixed stack buffer for path syscalls, MAX_PATH_BYTES long (the platform's PATH_MAX: 4096 on Linux, 1024 on macOS). Paths in it are NUL-terminated, so the longest path it holds is MAX_PATH_BYTES - 1 bytes.
  • resolve_path::join_abs_string_buf(cwd, buf, parts) is path.resolve into a caller buffer: it concatenates, normalizes and writes the result, assuming it fits. join_abs_string_buf_checked is the variant for unbounded input and returns None instead of writing when the normalized result is longer than buf. resolve_path::z copies a slice into a PathBuffer with a NUL and expects the caller to have checked the length.
  • bunfig loading: load_config loads bunfig.toml from the cwd for the commands in ALWAYS_LOADS_CONFIG and for bun <file> / bun -e (auto_loaded), or the --config value (not auto-loaded); install-family commands first load $XDG_CONFIG_HOME/.bunfig.toml, else $HOME/.bunfig.toml. load_bunfig ignores read errors for auto-loaded configs and exits with the error for explicit ones.

no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/config/bunfig/bunfig-errors.test.ts