install: fail instead of panicking when a local tarball or workspace: path does not fit the path buffer

Closed
RO
2mo ago
+210-63

Repro

mkdir probe && cd probe
bun -e 'require("fs").writeFileSync("package.json", JSON.stringify({ name: "probe", dependencies: { dep: "file:./" + "a".repeat(5000) + ".tgz" } }))'
bun install
panic: range end index 5022 out of range for slice of length 4095

bun install aborts (SIGABRT, exit 134). The same happens when the tarball dependency is declared by a workspace package instead of the root package, and for "dep": "workspace:./" + "a".repeat(5000) declared by either. Reproduced with bun 1.4.0-canary.1 on Linux; the buffer is 4096 bytes on Linux, 1024 on the other POSIX platforms and 32767 * 3 + 1 on Windows.

Cause

These specs are stored verbatim and nothing bounds their length. Three sites then join them into a stack PathBuffer with the unchecked join_abs_string_buf, whose normalizer indexes past the buffer when the result does not fit (resolve_path.rs:1067; the "length 4095" is the buffer minus the leading separator):

  • File::read_from_user_input (src/sys/file.rs), used by the local tarball task for dependencies of the root package.
  • enqueue_local_tarball (PackageManagerEnqueue.rs), which joins the workspace directory and the spec on the main thread for tarball dependencies of a workspace package.
  • The workspace: arm of Package::parse_dependency (lockfile/Package.rs), for workspace:<path> specs that do not name a workspace. The file: folder arm next to it already uses the checked join.

Fix

All three use join_abs_string_buf_checked, which still succeeds when an over-long spec normalizes down (x/../x/../...) and returns None only when the normalized path does not fit.

  • read_from_user_input returns ENAMETOOLONG, the same error openat_a already returns for a &[u8] path that does not fit a PathBuffer.
  • The workspace join happens before the task exists, so LocalTarballRequest's (tarball_path, normalize) pair becomes a TarballPath enum: Url (resolve tarball.url against the project root on the worker; this no longer interns a second copy of the same path), Absolute (pre-joined path) and TooLong, which the worker reports as Error::Sys(ENAMETOOLONG).
  • The workspace: arm reports Dependency "x" has an unsafe workspace path and fails the install, mirroring the folder arm's unsafe folder path.
  • openat_a (which both tarball sites end up in for a path of exactly MAX_PATH_BYTES, where the join fits but there is no room for the NUL) built its error from libc::ENAMETOOLONG. On Windows that is the MSVC value 38, and bun_sys::Error decodes the stored value against bun's own errno table, where 38 is ENOSYS; the boundary tests below reported ENOSYS extracting tarball on both Windows lanes. It and the identical guard in write_file_with_path_buffer now use the typed E::ENAMETOOLONG, like the rest of bun_sys. Linux and macOS are unaffected (libc's numbers and bun's table agree there).

Tarballs fail through the task's existing failure path, so both tarball cases print exactly what a too-long path rejected by the OS already prints today:

error: ENAMETOOLONG extracting tarball from dep
error: dep@file:./aaaa... failed to resolve

and bun install exits 1. A path that does not fit PATH_MAX can never be opened, so ENAMETOOLONG is the accurate error for the buffer case too; the kernel's answer is given one layer earlier instead of indexing out of bounds. Using the checked join rather than a length check on the spec keeps ..-heavy specs that normalize to a valid path working, which the positive tests pin down.

Rebase note

#38867 landed on main and generalized the workspace-relative join in enqueue_local_tarball into local_tarball_base_dir, which also covers tarballs declared by file: folder packages and filters out overrides/resolutions/catalogs. The rebase keeps that helper and routes its result through the checked join into TarballPath, so Absolute/TooLong now apply to folder-package tarballs as well as workspace ones. All 87 tests in bun-workspaces.test.ts pass after the rebase.

Not in this PR

  • #35863 (open) changes the shared primitive to return an empty path on overflow. That would turn these panics into a misleading ENOENT; this PR fixes the call sites so they report the real error either way. The two are independent.
  • folder_resolver::normalize_package_json_path has overflows of its own, reached by file: folder specs whose joined path is within 13 bytes of the buffer size and by any link: spec over 1024 bytes (it normalizes through a 1024 byte thread-local buffer). Different mechanism and function; reported separately, still panics after this PR. The workspace: change here gives those specs the behaviour folder specs have today, no more.
  • The same from_code_int(libc::...) mistake exists in a few Windows-compiled stubs in bun_sys (writev/readv/preadv, get_fcntl_flags, clonefile), where libc::ENOSYS / ENOTSUP decode as ELOOP / EKEYREJECTED. Reported separately; only the two ENAMETOOLONG sites the tests here exercise are changed.
  • Linking a package whose resolution string is over 512 bytes panics in PackageInstaller::install_package_with_name_and_resolution (reproducible with a valid tarball at a 618 byte relative path, no overflow involved). Also reported separately. The positive tests below use --lockfile-only, which stops after the tarball has been read and extracted, and check the lockfile instead of node_modules.

Tests

test/cli/install/bun-workspaces.test.ts, each case for a root dependency and for a workspace dependency:

  • a 100 kB tarball path (longer than the buffer on every platform) fails with ENAMETOOLONG extracting tarball from too-long, exit 1
  • a tarball path whose joined length is one byte below, exactly at and one byte above the buffer size (computed from the temp dir, which is the install's cwd): ENOENT for the one that fits (ENAMETOOLONG on Windows, which rejects anything over 32767 UTF-16 units before looking it up), ENAMETOOLONG for the other two. The workspace row above the limit is what pins TarballPath::TooLong: with the worker handling it like Url, that path fits when joined with the root and comes back ENOENT (verified by making exactly that change locally; the 100 kB case cannot tell the two apart because it overflows both joins)
  • a 100 kB x/../x/../... path that normalizes to an existing tarball resolves (local_tarball entry in the lockfile)
  • a 100 kB workspace: path fails with unsafe workspace path

On the unfixed build (USE_SYSTEM_BUN=1) 7 of the 12 crash with the panics above (both 100 kB tarball cases, root at and above the limit, workspace above the limit, both workspace: cases); the other 5 behave the same before and after. All 75 tests in the file pass with bun bd test on Linux, as do the workspace and tarball path tests in bun-install.test.ts and the local tarball tests in bun-add.test.ts. The 12 new tests also pass with a debug build on Windows x64 (before the openat_a change the two "exactly" rows failed there with ENOSYS, see the Windows lanes of the previous CI run). cargo check -p bun_install passes for x86_64-pc-windows-msvc and x86_64-apple-darwin; clippy is clean for bun_sys and bun_install.


no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/bun-workspaces.test.ts