install: do not abort when $XDG_CONFIG_HOME or $HOME is too long for the .npmrc path buffer
Closed
RO
2mo ago+70+70+01234567890123456789-12-12-01234567890123456789
Problem
bun install,bun pm ...,bun publishand the other commands that go throughPackageManager::initabort at startup when$XDG_CONFIG_HOME(or, when that directory has no.npmrc,$HOME) isMAX_PATH_BYTES - 7bytes or longer (4089 on Linux, 1017 on macOS):- exactly
MAX_PATH_BYTES - 7bytes: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 framebun_paths::resolve_path::normalize_string_generic_tz(src/paths/resolve_path.rs:1071), called frombun_install::package_manager_real::init
- exactly
- Cause:
src/install/PackageManager.rs:1887and:1894join the environment value and.npmrcinto a stackPathBufferwith the uncheckedjoin_abs_string_buf_z. It normalizes into the buffer without a size check and then writes the NUL terminator atresult.len(), so a result of exactlyMAX_PATH_BYTES - 1bytes 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 cachein 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)buildsdir/.npmrcwithjoin_abs_string_buf_checkedintobuf[..MAX_PATH_BYTES - 1], writes the NUL itself, and returnsNonewhen the result does not fit. Both candidates ($XDG_CONFIG_HOME, then$HOME) use it;Noneleavesglobal_lenat 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_BYTESbytes or longer, whichopen()rejects withENAMETOOLONG(bun_sys::openat_achecks the same>= MAX_PATH_BYTESbound itself), andload_npmrc_configalready 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.npmrcthat could be read before is skipped now: the longest directory that fits,MAX_PATH_BYTES - 8bytes, still loads, and its.npmrcpath is the longest oneopen()accepts. - The
partsliteral changes from./.npmrcto.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$HOMEthat fits (built as a real directory of exactlyMAX_PATH_BYTES - 8bytes) still has its.npmrchonored,$HOMEone byte longer is skipped,$HOMElonger 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
$HOMEcandidate (with$XDG_CONFIG_HOMEpointing at a short directory) because the global.bunfig.tomllookup insrc/bunfig/arguments.rsreads$XDG_CONFIG_HOME(or$HOMEwhen it is unset) before this code with the same unchecked join and a longer file name, so an oversized value there still aborts earlier; #38370 fixes that site. Both candidates here shareuser_npmrc_path, and the existing lookup tests in the samedescribecover the$XDG_CONFIG_HOMEcandidate when the path fits. cargo clippy -p bun_installandrustfmt --checkare clean.
Background
PathBufferis bun's fixed stack buffer for path syscalls,MAX_PATH_BYTESlong (the platformPATH_MAX: 4096 on Linux, 1024 on macOS). Paths in it are NUL-terminated, so the longest path it can hold isMAX_PATH_BYTES - 1bytes, which is also the longest pathopen()accepts.resolve_path::join_abs_string_buf_z(cwd, buf, parts)ispath.resolveinto a caller buffer plus a NUL terminator; it assumes the result fits.join_abs_string_buf_checkedis the variant for input of unbounded length: it normalizes first and returnsNoneinstead of writing when the result is longer thanbuf, so it has to be given a buffer one byte short of the NUL slot the caller fills in.- User-level
.npmrclookup:PackageManager::inituses$XDG_CONFIG_HOME/.npmrcwhen that file exists, otherwise$HOME/.npmrc(#36289), and passes the result together with the project.npmrctoload_npmrc_config, which ignores candidates it cannot read.