Skip to content

install: fail the package with ENAMETOOLONG when the isolated linker walks an entry that does not fit the path buffer - #37424

Open
robobun wants to merge 4 commits into
mainfrom
farm/20f57712/isolated-linker-path-bounds
Open

install: fail the package with ENAMETOOLONG when the isolated linker walks an entry that does not fit the path buffer#37424
robobun wants to merge 4 commits into
mainfrom
farm/20f57712/isolated-linker-path-bounds

Conversation

@robobun

@robobun robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Repro

A file: dependency containing a directory chain longer than PATH_MAX (4096 bytes on Linux, 1024 on macOS), installed with the isolated linker:

mkdir -p proj/pkg && cd proj
echo '{ "name": "proj", "dependencies": { "pkg": "file:./pkg" } }' > package.json
echo '{ "name": "pkg", "version": "1.0.0" }' > pkg/package.json
( cd pkg; d=$(printf 'd%.0s' $(seq 1 200)); for i in $(seq 1 25); do mkdir "$d"; cd "$d"; done; echo hi > leaf.txt )
bun install --linker=isolated
panic: index out of bounds: the len is 4096 but the index is 4096
oh no: Bun has crashed. This indicates a bug in Bun, not your code.

Exit code 134, on bun 1.4.0-canary.1 (9008ae7). The same tree without leaf.txt crashes the same way, and so does the same tree inside a cached package (--backend=hardlink). The hoisted linker reports ENAMETOOLONG: failed copying files from cache to destination for package pkg and exits 1 for this tree; with this PR the isolated linker reports

ENAMETOOLONG: File name too long: failed to link package: pkg@pkg (link)

and exits 1 in all of these cases.

Cause

isolated_install/Hardlinker.rs and FileCopier.rs build the destination (and on Windows the source) of every entry by appending the walker's entry path to a bun_paths::Path in its default CheckLength::ASSUME mode, where append skips the length check and over-long input indexes past the pooled PathBuffer. walker_skippable opens each directory relative to its parent and keeps the entry path in a growable Vec, so it yields entries of any length; any folder dependency or cache entry with a deep enough tree crashes the install. The let _ = self.dest.append(..) on the unix side documented this as "fire-and-forget", but the Err arm is dead under ASSUME; the failure mode was the panic.

Smaller problems in the same loops (the hoisted counterparts are fixed in #37400):

  • A directory entry whose creation fails (make_path in the Hardlinker, CreateDirectoryExW + make_path in the Windows FileCopier) was ignored and the install reported success without the directory.
  • In the unix FileCopier a failing fstat of the source did continue after the destination had already been created and truncated, leaving an empty file in a package that installed "successfully".
  • When the unix FileCopier could not create an entry's destination (which is how it runs into this tree, the kernel rejecting the relative path), it printed the error and exited the process from the install worker instead of failing the package like every other error in these loops. This is the path the copy fallback for folder dependencies and --backend=copyfile take.

Fix

  • bun_paths: Path::into_checked() reinterprets a path as CheckLength::CHECK (the same no-op move as into_sep), and CheckLength::CHECK is public so the checked OS-unit types can be named. The store paths are still built with the existing ASSUME-only PathLike helpers (their inputs are bounded); the conversion happens where the unbounded walker paths get appended. This is the approach cli: report an error for over-PATH_MAX --cpu-prof-dir / --heap-prof-dir instead of panicking #36881 took for the profiler output paths.
  • Hardlinker and FileCopier hold checked paths; an entry that does not fit fails the package with ENAMETOOLONG (tagged link / copyfile), which is what the kernel returns for the same path and what the hoisted linker reports. The EXDEV/EACCES/EPERM fallback to copying still works, since those errors still come out of linkat.
  • The Windows Hardlinker additionally checks that cwd + destination + the \??\ prefix + NUL fit before join_string_buf_w_same / add_nt_path_prefix_if_needed, which copy into PATH_MAX_WIDE buffers unchecked: the checked dest only bounds the cwd-relative part, and a package's own paths are shorter than their destinations by the store prefix, so a package that exists on disk can still have destinations that do not fit (panic: range end index 32785 out of range for slice of length 32767 without the check). bun_paths::windows::NT_OBJECT_PREFIX is public for that.
  • Directory creation errors and the fstat error propagate, and the unix FileCopier returns the error from creating the destination instead of exiting; every caller (both Installer sites and bun patch) already reports a returned error. make_path treats an existing directory as success (that is also the path taken when CreateDirectoryExW fails with "already exists"), so reinstalling over an existing tree is unchanged; what propagates is a directory that really does not exist afterwards. The make_path calls that are immediately followed by a retried link/copy whose error is propagated are left as they were: the retry decides. On unix the copy backend never creates directories on its own (same as the hoisted copyfile backend), so a tree of only over-long directories still installs with it; only the hardlink backend reports that case.

Verification

test/cli/install/isolated-install-long-paths.test.ts builds the trees with node:fs (chunks of four directories assembled bottom-up with rename, so nothing handed to the filesystem is near PATH_MAX) and covers:

  • a folder dependency with only directories past PATH_MAX (crash before);
  • the destination path one byte short of PATH_MAX installing (and being readable) and exactly PATH_MAX failing, so the >= in the checked append is pinned from both sides; the failing entry there is the file;
  • a cached package with the tree, reinstalled with --backend=hardlink (crash before) and --backend=copyfile (process exit from the worker before, now the package error);
  • a deep tree that fits installing on every platform (4443 characters on Windows);
  • on Windows, an entry whose store-relative destination fits but whose absolute destination does not, placed in the middle of the window between the two prefixes; it panics as quoted above with the check removed and fails with ENAMETOOLONG with it (verified both ways on a Windows debug build).

Against the release binary the four failure cases fail and the two fitting cases pass; with the debug build all pass on Linux, and the two Windows cases pass on a Windows debug build. Also run with the debug build: isolated-install.test.ts (62 pass on Linux; on Windows the backend and folder dependency cases, while three unrelated registry tests fail identically on main on that VM), bun-install-hardlink-fallback.test.ts (copy fallback for folder dependencies, both linkers) and bun-patch.test.ts (FileCopier via bun patch, Linux and Windows). cargo check -p bun_install passes for the Linux, x86_64-pc-windows-msvc and x86_64-apple-darwin targets.

…ntry does not fit the path buffer

The isolated linker's Hardlinker and FileCopier append each walker entry
to CheckLength::ASSUME paths, so an entry longer than the remaining
buffer panics in Buf::append instead of failing the package. Build them
on length-checked paths (Path::into_checked) and report the overflow as
ENAMETOOLONG, guard the cwd + dest join on Windows the same way, and
propagate the errors from creating a directory entry and from fstat on a
source file that were previously discarded.
@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced on bun 1.4.0-canary.1 (9008ae7) with the repro in the description (panic: index out of bounds: the len is 4096 but the index is 4096, exit 134, with and without the leaf file, and in a cached package with --backend=hardlink). With this branch these fail with ENAMETOOLONG: File name too long: failed to link package: pkg@pkg (link) and exit 1. test/cli/install/isolated-install-long-paths.test.ts: against the release binary the 4 failure cases fail and the 2 fitting cases pass; with the debug build 6 pass on Linux (Windows case skipped) and the 2 Windows cases pass on a Windows debug build. Hoisted counterpart: #37400.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The installer now converts paths to checked-length types. File copying and hardlinking propagate ENAMETOOLONG, directory, copy, and fstat errors. New tests cover deep isolated-install paths and overflow cases.

Changes

Checked install path validation

Layer / File(s) Summary
Checked path construction contracts
src/paths/Path.rs, src/paths/lib.rs
CheckLength, CheckLength::CHECK, Path::into_checked, and windows::NT_OBJECT_PREFIX are publicly accessible.
Hardlinker path validation
src/install/isolated_install/Hardlinker.rs, src/install/isolated_install/Installer.rs
Hardlinker uses checked paths and propagates path-capacity and directory errors on Windows and POSIX. Installer call sites pass checked paths.
FileCopier error propagation
src/install/isolated_install/FileCopier.rs, src/install/isolated_install/Installer.rs, src/install/PackageManager/patchPackage.rs
FileCopier validates path appends and propagates copy, directory, and POSIX fstat errors. Relevant call sites pass checked paths.
Deep-path installation coverage
test/cli/install/isolated-install-long-paths.test.ts
Tests cover fitting deep paths and Windows and POSIX overflow errors for hardlink and copyfile backends.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the isolated linker failure change and the ENAMETOOLONG behavior.
Description check ✅ Passed The description explains the reproduction, cause, fix, and verification, covering the template requirements despite using different section headings.

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/install/isolated_install/FileCopier.rs`:
- Around line 171-179: Update the retry branch in FileCopier to propagate the
result from bun_sys::make_path::make_path::<u16> instead of discarding it with
let _. Return immediately when directory creation fails, and only retry
bun_sys::copy_file::copy_file after successful creation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 978ae1e1-0c41-43ec-b71f-8e3a9b799756

📥 Commits

Reviewing files that changed from the base of the PR and between 9fcdea8 and 1b4890a.

📒 Files selected for processing (7)
  • src/install/PackageManager/patchPackage.rs
  • src/install/isolated_install/FileCopier.rs
  • src/install/isolated_install/Hardlinker.rs
  • src/install/isolated_install/Installer.rs
  • src/paths/Path.rs
  • src/paths/lib.rs
  • test/cli/install/isolated-install-long-paths.test.ts

Comment thread src/install/isolated_install/FileCopier.rs Outdated

@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.

I reviewed this PR and didn't find any bugs. Because it changes error-propagation behavior in the isolated installer's link/copy loops, adds public surface to bun_paths (into_checked, CheckLength::CHECK, NT_OBJECT_PREFIX), and touches Windows-specific unchecked-buffer arithmetic, a human look would still be worthwhile.

What was reviewed:

  • into_sep::<AUTO>into_checked() at the four Installer.rs call sites keeps separator behavior (OsAutoAbsPath/OsAutoPath/dest_subpath are already PathSeparators::AUTO).
  • Checked append returns Err before mutating, so the post-body set_length(saved_len) restore is correct even when only the first of two appends succeeds.
  • Windows needed_len bound (Σ(part.len()+1) + NT_OBJECT_PREFIX.len()) covers the join separator, NUL, and \\??\\ prefix that join_string_buf_w_same + add_nt_path_prefix_if_needed write unchecked.
  • make_path treating existing directories as success means the newly-propagated directory errors won't break re-install over an existing tree.
Extended reasoning...

Overview

The PR fixes a panic (index out of bounds: the len is 4096 but the index is 4096) in the isolated linker when a folder dependency contains a directory chain longer than PATH_MAX. It touches src/install/isolated_install/{Hardlinker,FileCopier,Installer}.rs, src/install/PackageManager/patchPackage.rs, src/paths/{Path.rs,lib.rs}, and adds test/cli/install/isolated-install-long-paths.test.ts. The mechanism is: switch the per-entry destination/source paths from CheckLength::ASSUME (which panics on overflow inside Buf::append's slice indexing) to CheckLength::CHECK via a new Path::into_checked() reinterpret, then surface the append failure as ENAMETOOLONG for the package. Alongside, three previously-swallowed errors (directory make_path in both linkers, fstat in the unix FileCopier) now propagate, and the Windows Hardlinker gains a manual pre-check before two unchecked pooled-buffer writes.

Security risks

None identified. The change tightens validation of on-disk directory-walk input (turning a crash into a recoverable per-package error) and does not touch auth, network, or lockfile parsing. Making NT_OBJECT_PREFIX and CheckLength::CHECK pub is crate-internal API surface, not user-facing.

Level of scrutiny

Medium-high. The isolated linker is a production-critical install path, the change is cross-platform with distinct Windows and POSIX loop bodies, and it deliberately changes behavior on three previously-silent error paths. The bun_paths additions (into_checked, public CheckLength) are small but establish a pattern other call sites may adopt. This is not a mechanical fix — the PR reasons carefully about which make_path results to propagate vs. leave best-effort, and adds hand-computed buffer-length arithmetic for the Windows NT-path join.

Other factors

  • The Installer.rs call sites previously used .into_sep::<{ PathSeparators::AUTO }>(); I checked that OsAutoAbsPath, OsAutoPath, and dest_subpath are all already PathSeparators::AUTO, so replacing with .into_checked() (which preserves SEP_OPT) does not lose the /\\ normalization the Windows FileCopier depends on.
  • Path::append in CHECK mode returns Err before calling buf_append_input, so when the first append succeeds and the second fails, the post-body set_length(saved_len) correctly restores both paths before the error return.
  • The Windows needed_len bound is conservative-correct: join_string_buf_w_same needs Σ part.len() + (parts-1) separators, add_nt_path_prefix_if_needed prepends 4 units and NUL-terminates; Σ(part.len()+1) + 4 covers that with at most one unit of slack.
  • The propagated make_path errors should not regress re-installs because make_path returns success on already-exists; the ENOENT-retry make_path calls that are immediately followed by a re-attempted link/copy remain let _ = since the retry's error already propagates.
  • The test builds >PATH_MAX trees via chunked rename (so no single syscall exceeds the limit), covers file-leaf, directory-only, and fitting-tree cases, uses test.concurrent, drains all pipes, and skips the overflow cases on Windows with a stated reason. robobun confirmed 3 pass on the debug build vs 1 pass / 2 fail on release.

Given the behavior changes on error paths in a critical installer loop and the new bun_paths public surface, deferring to a human reviewer rather than auto-approving.

@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 6:10 AM PT - Aug 11th, 2026

@robobun, your commit 7d99e1d4f139f164e1a9515c499e56908dbf95a8 passed in Build #92255! 🎉


🧪   To try this PR locally:

bunx bun-pr 37424

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

bun-37424 --bun

…r; cover the copy backend, the PATH_MAX boundary and the Windows absolute path

The unix FileCopier printed the error and exited the process when the
destination of an entry could not be created after creating its parent;
every caller already reports a returned error as the package failing.

Tests now also cover the hardlink and copyfile backends walking a cached
package, the destination path one byte below and exactly at PATH_MAX, and
on Windows an entry whose cwd-relative store path fits but whose absolute
destination does not, which is what the join guard in the Hardlinker is
for.
Comment thread src/install/isolated_install/FileCopier.rs Outdated
Comment thread src/install/isolated_install/FileCopier.rs Outdated
Comment thread src/install/isolated_install/FileCopier.rs Outdated
Comment thread src/install/isolated_install/FileCopier.rs Outdated
Comment thread src/install/isolated_install/Hardlinker.rs Outdated
Comment thread src/install/isolated_install/Hardlinker.rs Outdated
Comment thread src/paths/Path.rs Outdated
Comment thread src/paths/Path.rs Outdated

@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.

I reviewed this PR and didn't find any bugs. Because it changes error-propagation semantics in the isolated linker (directory make_path and fstat failures now fail the package where they were previously silent), adds public surface to bun_paths, and has eight open comment-cop flags on new/re-indented comments that haven't been addressed yet, a human look is still worthwhile.

What was reviewed:

  • into_checked() preserves SEP_OPT, so replacing .into_sep::<AUTO>() with .into_checked() at the Installer/patchPackage call sites type-checks only because the source paths were already AUTO — no separator-normalization change.
  • The saved-length restore in Hardlinker/FileCopier still runs when append fails mid-way (both lengths are captured before the labeled block, set_length runs unconditionally after).
  • The Windows needed_len bound counts one separator per part plus the \\??\\ prefix, which covers both the join and the NT-prefix write; slightly conservative but safe.
  • make_path treating existing directories as success means the new directory-error propagation does not break reinstalling over an existing store tree.
Extended reasoning...

Overview

Converts the isolated linker's per-entry path builders from CheckLength::ASSUME to CheckLength::CHECK so an over-long walker entry fails the package with ENAMETOOLONG instead of panicking on an out-of-bounds buffer index. Touches Hardlinker.rs and FileCopier.rs (both POSIX and Windows arms), the four Installer.rs call sites plus patchPackage.rs, and adds Path::into_checked() / public CheckLength::CHECK / public windows::NT_OBJECT_PREFIX to bun_paths. Also tightens three previously-swallowed errors in the same loops: directory make_path, Windows CreateDirectoryExW fallback, and the unix fstat before fchmod. Replaces a Global::exit(1) in the unix FileCopier retry path with ? propagation. New test file exercises the PATH_MAX boundary (fits / one-over), the directory-only crash, and both hardlink/copyfile backends via a poisoned cache entry.

Security risks

None identified. The change hardens a user-reachable panic (folder-dependency contents are user-controlled) into a recoverable per-package error, which is strictly safer. No new untrusted input parsing, no path-traversal surface added.

Level of scrutiny

High. This is native install-path code with divergent Windows/POSIX branches, and it changes whether an install reports success: previously-ignored make_path/fstat failures now fail the package. That is the correct behavior per the repo's "never signal success on a failure" rule, and the PR description argues it matches the hoisted linker, but it is a user-visible behavior change to bun install that a maintainer should sign off on. The bun_paths additions are small but public API.

Other factors

  • Eight unresolved comment-cop (github-actions) inline comments flag paragraph-length comments added or re-indented in this PR (FileCopier.rs:14/139/164/222, Hardlinker.rs:15/148, Path.rs:84/865). Several are legitimate why-comments or public-API docs, but the author has not yet responded to or trimmed them.
  • CodeRabbit's one finding (retry-arm make_path result discarded) was answered and withdrawn; the reasoning — the retried copy_file/link is authoritative — is sound and consistent with the sibling ENOENT arms.
  • Test coverage is thorough (boundary-exact, both backends, Windows absolute-path case), uses tempDir/bunEnv/test.concurrent, and the description confirms it fails on the release binary and passes on the debug build.
  • cargo check across linux/windows/darwin targets is reported passing, and robobun confirmed the repro.

@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.

I reviewed this PR and didn't find any bugs. Because it changes error-propagation behavior in the isolated installer (previously-swallowed make_path/fstat failures now fail the package, and the unix FileCopier retry-fail path changes from Global::exit(1) to ?), adds public surface to bun_paths, and has cross-platform #[cfg] divergence, a human look would still be worthwhile. There are also several unresolved comment-cop findings on the touched files.

What was reviewed:

  • into_checked() reuses the same field-move as into_sep(); the dropped .into_sep::<AUTO>() calls in Installer.rs were no-ops (source paths are already AUTO, enforced by the FileCopier::init signature).
  • Checked-append + set_length ordering in both loops — saved lengths are captured before the fallible append, so a failed append still restores correctly.
  • Windows needed_len bound covers both pooled buffers (join into buf1, NT-prefix into buf2; both are PATH_MAX_WIDE).
  • The retained let _ = make_path(..) sites all have a retried operation immediately after whose result propagates.
Extended reasoning...

Overview

Fixes a panic (index out of bounds: the len is 4096 but the index is 4096) in the isolated linker when a walked package entry's path exceeds PATH_MAX. The fix threads CheckLength::CHECK through the Hardlinker/FileCopier path types so append returns Err(MaxPathExceeded) instead of indexing past the pooled buffer, and surfaces that as ENAMETOOLONG on the package. Along the way it also propagates previously-discarded make_path (directory-entry) and fstat errors, replaces a Global::exit(1) in the unix FileCopier retry-fail path with ?, and adds a Windows-only pre-check for the absolute-destination join into the NT-prefixed buffer. bun_paths gains Path::into_checked(), and CheckLength / CheckLength::CHECK / windows::NT_OBJECT_PREFIX become pub. A new test file covers the fitting case, the PATH_MAX boundary (both sides), the copyfile backend via a mutated cache entry, and the Windows absolute-destination overflow.

Security risks

None identified. The change tightens error handling on paths that come from the local filesystem (folder dependencies / cache entries); no new parsing of untrusted input, no auth/crypto/permissions.

Level of scrutiny

Medium-high. This is production install-path code that runs on every --linker=isolated install, with #[cfg(windows)]/#[cfg(not(windows))] divergence in every touched loop. The error-propagation changes are behavior changes: directory-creation and fstat failures that used to be silently dropped now fail the package, and the unix FileCopier's retry-fail path no longer hard-exits the process. Those are the right calls per the PR description and the hoisted-linker precedent (#37400), but they're the kind of "previously best-effort now hard-fail" semantics a maintainer should sign off on. The bun_paths additions are minimal (one method + visibility), but new public API on a foundational crate.

Other factors

  • Test coverage is thorough (fitting case, one-byte-under/at PATH_MAX, both backends, Windows absolute path) and the description documents USE_SYSTEM_BUN=1 failure and cross-target cargo check.
  • The dropped .into_sep::<AUTO>() at the two FileCopier::init call sites in Installer.rs is safe: FileCopier::init takes AbsPathAutoOs/PathAutoOs (both SEP = AUTO), so a mismatched source SEP would fail to compile — the old conversion was already the identity.
  • The one CodeRabbit finding was discussed and withdrawn; the reasoning (retried copy_file is authoritative) is sound and consistent with the sibling ENOENT retry arms.
  • Eight comment-cop inline comments from github-actions remain unresolved on the touched files (posted after the "shorten comments" commit). They appear to target pre-existing multi-line comments the diff brushed rather than the one-liners this PR added, but they are outstanding automated feedback the author may want to address or dismiss.

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