Skip to content

Do not abort from a working directory close to PATH_MAX (package.json, tsconfig.json) - #42502

Closed
robobun wants to merge 1 commit into
mainfrom
robobun/7078896b/package-json-path-deep-cwd
Closed

robobun wants to merge 1 commit into
mainfrom
robobun/7078896b/package-json-path-deep-cwd

Conversation

@robobun

@robobun robobun commented Sep 12, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • From a working directory of 4083..4095 bytes (legal on Linux), bun run <script>, bun -e, bun test, bun install and every bun pm command abort. bun run: panic: range end index 4102 out of range for slice of length 4095. bun install, bun pm cache: panic: range end index 4103 out of range for slice of length 4096.
  • Three unchecked joins of <cwd>/<name>: package.json in PackageJSON::parse (src/resolver/package_json.rs:385), tsconfig.json and jsconfig.json in dir_info_uncached (src/resolver/resolver.rs:6400), package.json in PackageManager::init (src/install/PackageManager.rs:1586).

Fix

  • PackageJSON::parse uses join_abs_string_spill. The resolver opens the file relative to the directory fd, so bun run <script> works. resolver: report candidate paths that do not fit a path buffer as not found instead of aborting #39626 hides such a package.json from the resolver instead. A maintainer has to pick one (see notes).
  • dir_info_uncached uses abs_buf_checked and skips a tsconfig.json or jsconfig.json whose path does not fit: baseUrl, paths and extends are joined onto that path.
  • PackageManager::init builds the path in a Vec. File::openat returns ENAMETOOLONG for it, which takes the existing route: ENAMETOOLONG: File name too long: could not open "<cwd>/package.json" (open), exit 1.
  • Verified: 6 new tests in test/cli/run/run_command.test.ts, 5 in test/cli/install/bun-pm.test.ts. The unfixed build fails 10.

Background

  • PathBuffer is bun's fixed buffer for one path: MAX_PATH_BYTES long, NUL included (4096 on Linux, 1024 on macOS).
  • join_abs_string and abs_buf are path.resolve into a fixed buffer and assume the result fits. join_abs_string_spill falls back to the heap. abs_buf_checked returns None.
  • PackageManager::init runs first in every package manager command. It opens the nearest package.json by absolute path.
Notes

Origin. Found by an internal report, reproduced on the 1.4.3-canary release build. There is no GitHub issue.

Behavior by cwd length, Linux x64, release build of main against this branch. The project has a package.json with "scripts": {"hi": "echo hi"}.

cwd bytes bun run hi before after bun install, bun pm cache before after
4082 ok ok ok ok
4083 ok, or panic: range end index 4096 out of range for slice of length 4095 with a tsconfig.json ok panic: index out of bounds: the len is 4096 but the index is 4096 ENAMETOOLONG, exit 1
4084 panic: range end index 4096 out of range for slice of length 4095 ok panic: range end index 4097 out of range for slice of length 4096 ENAMETOOLONG, exit 1
4095 panic: range end index 4107 out of range for slice of length 4095 ok panic: range end index 4108 out of range for slice of length 4096 ENAMETOOLONG, exit 1
  • With the fix, bun run hi and bun -e print their output and exit 0 at every length from 4079 to 4095, with and without a bun init style tsconfig.json.
  • The tsconfig.json boundary is exact. With "jsxImportSource": "zzfoo", bun -e 'console.log(<div/>)' fails on zzfoo/jsx-runtime at a cwd of 4082 bytes (config loaded) and prints <div /> at 4083 (config skipped).

The contract choice against #39626. #39626 (open) makes DirEntry::add_entry drop every directory entry whose absolute path does not fit a path buffer. With it, package.json, tsconfig.json and jsconfig.json in such a directory do not exist for the resolver, and bun run hi reports Script not found "hi". This PR has the same result for the two config files and a different result for package.json: the resolver reads it through openat(dir_fd, "package.json"), as it already does on macOS for a path between 1024 and 4096 bytes. If #39626 lands in its current shape after this PR, the two package.json rows of the new bun run tests must expect Script not found. If a maintainer prefers that contract now, the PackageManager.rs hunk and the bun-pm.test.ts tests stand alone.

Why PackageManager::init stops and does not continue upward. A package.json can exist in that directory, and the command would then run against a parent project without a word. The function already stops on EACCES, EISDIR, ELOOP and every other error that is not ENOENT. The full output is the existing two lines for a failed open in init. The second line is error: An internal error occurred (ENAMETOOLONG) in a release build, the same as for a package.json directory (EISDIR) today. #39650 makes the --filter walk in filter_arg.rs continue upward for the same condition. That walk already continues on ENOENT, EACCES and EPERM, so each function keeps its own rule for an unreadable file.

What still does not work from such a directory.

Other details.

  • The second loop in init (the workspace root search) copies <parent>/package.json into a PathBuffer in the same way. It needs no change: each parent is shorter than the directory whose package.json the first loop opened.
  • When the resolver has no directory fd (bun run --filter passes Fd::invalid()), read_file_with_allocator opens the absolute path, gets ENAMETOOLONG from openat_a, and PackageJSON::parse logs Cannot read file "<dir>": ENAMETOOLONG and returns None. That is its existing route for a failed open.
  • macOS: PATH_MAX is 1024. The 4096-byte thread-local join buffer cannot overflow from a legal directory there, but the two PathBuffer sites can (cwd of 1010..1023 bytes). The tests size the directory from PATH_MAX per platform. Windows is skipped: the path buffer there is longer than any path the OS accepts.
  • The tests move a prepared directory into place with renameSync, because a file in the final directory is too long to write by its absolute path.
  • Suites that pass with the fix: test/cli/run/run_command.test.ts, filter-workspace.test.ts, tsconfig-override.test.ts, test/cli/install/bun-pm.test.ts, bun-pm-pkg.test.ts, bun-add.test.ts, bun-remove.test.ts, bun-workspaces.test.ts, bad-workspace.test.ts, test/config/bunfig/bunfig-errors.test.ts, test/bundler/esbuild/tsconfig.test.ts, test/js/bun/resolve/resolve.test.ts, resolve-error.test.ts, import-meta.test.js, tsconfig-extends-leak.test.ts. cargo clippy -p bun_resolver -p bun_install and cargo fmt --check are clean.

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/run/run_command.test.ts, test/cli/install/bun-pm.test.ts

A working directory can be PATH_MAX - 1 bytes long. Three joins of
<cwd>/<name> assumed that the result fits a path buffer:

- PackageJSON::parse joined <dir>/package.json into the 4096-byte
  thread-local join buffer.
- dir_info_uncached joined <dir>/tsconfig.json and <dir>/jsconfig.json
  into a PathBuffer.
- PackageManager::init copied <cwd>/package.json plus a NUL into a
  pooled PathBuffer.

From a working directory of 4083..4095 bytes (legal on Linux) bun run,
bun -e, bun test, bun install and every bun pm command aborted with a
slice index panic.

PackageJSON::parse now uses join_abs_string_spill. The file is opened
relative to the directory fd, so bun run works from such a directory.
A tsconfig.json or jsconfig.json whose path does not fit is skipped,
because baseUrl, paths and extends are joined onto that path in path
buffers. PackageManager::init builds the path on the heap and lets
File::openat report ENAMETOOLONG, which takes the existing
could-not-open route and exits 1.
@robobun

robobun commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator Author

Status

Reproduction (Linux, release build of main): make a working directory of 4090 bytes out of 200-byte components, write a package.json with "scripts": {"hi": "echo hi"} into it, and run bun from it.

bun run hi     -> panic: range end index 4102 out of range for slice of length 4095 (exit 134)
bun install    -> panic: range end index 4103 out of range for slice of length 4096 (exit 134)
bun pm cache   -> panic: range end index 4103 out of range for slice of length 4096 (exit 134)

With a tsconfig.json or jsconfig.json next to it, bun run hi, bun -e and bun test also abort from 4083 bytes, in the <cwd>/tsconfig.json join.

With this branch: bun run hi prints hi and exits 0 at every cwd length from 4079 to 4095. bun install and bun pm cache print ENAMETOOLONG: File name too long: could not open "<cwd>/package.json" (open) and exit 1 from 4083 bytes.

Tests: bun bd test test/cli/run/run_command.test.ts and bun bd test test/cli/install/bun-pm.test.ts. With the src/ of main, 10 of the 11 new tests fail with the panics above. The 11th is a boundary control.

Self-reviewed: 12 concerns raised. Addressed: the tsconfig.json and jsconfig.json joins in the same function are fixed here and have tests, and the PR body now lists what still does not work and which open PR covers each site. Stated for a maintainer to decide: #39626 hides a package.json whose path does not fit, this PR reads it. Not taken: bun exec, bun pm bin and the sideEffects joins (#37529) are other commands or belong to an open PR. They are listed in the notes.

@coderabbitai

coderabbitai Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: a10bfb56-a147-46a8-b15f-da9151b4aa97

📥 Commits

Reviewing files that changed from the base of the PR and between b993710 and e3a10ed.

📒 Files selected for processing (5)
  • src/install/PackageManager.rs
  • src/resolver/package_json.rs
  • src/resolver/resolver.rs
  • test/cli/install/bun-pm.test.ts
  • test/cli/run/run_command.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.


Walkthrough

The change updates package and configuration path construction near PATH_MAX. It adds spill-capable and checked path handling, plus Linux and Android tests for package-manager errors and bun run execution in deeply nested directories.

Changes

PATH_MAX support

Layer / File(s) Summary
Package path construction
src/install/PackageManager.rs, src/resolver/package_json.rs
Package discovery uses growable or spill-capable path construction. Error diagnostics format the new path representation.
Configuration path checks
src/resolver/resolver.rs
tsconfig.json and jsconfig.json lookup skips paths that do not fit the checked buffer.
PATH_MAX boundary tests
test/cli/install/bun-pm.test.ts, test/cli/run/run_command.test.ts
Linux and Android tests cover package-manager ENAMETOOLONG behavior and successful bun run execution near PATH_MAX. Tests skip on Windows.

Suggested reviewers: jarred-sumner

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to e3a10

The PATH_MAX handling changes and boundary coverage do not leave an identified merge-blocking risk.

🚥 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 summarizes the primary change: preventing aborts from working directories near PATH_MAX for package.json and TypeScript configuration files.
Description check ✅ Passed The description explains the problem, fix, scope, limitations, and verification results. It does not use the exact template headings, but it provides the required information and is substantially comp…

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

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment on lines +50 to +61
const PATH_MAX = process.platform === "linux" || process.platform === "android" ? 4096 : 1024;
const SEGMENT = Buffer.alloc(200, "d").toString();

/** An absolute path below `root` whose UTF-8 encoding is exactly `length` bytes long. */
function pathOfLength(root: string, length: number): string {
let path = root;
// Leave room for the final component, which has to stay under NAME_MAX (255).
while (length - Buffer.byteLength(path) > 256) path = join(path, SEGMENT);
path = join(path, Buffer.alloc(length - Buffer.byteLength(path) - 1, "L").toString());
expect(Buffer.byteLength(path)).toBe(length);
return path;
}

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.

🟡 nit (optional): The PATH_MAX constant, SEGMENT, and pathOfLength() helper are byte-identical across test/cli/run/run_command.test.ts and test/cli/install/bun-pm.test.ts; REVIEW.md requires extracting a duplicated multi-line block to a shared helper the second time it appears in the same diff. Fix: move pathOfLength (and its PATH_MAX/SEGMENT inputs) into test/harness.ts and import it in both suites, which covers the 2 sites listed. Same pattern at 2 sites (test/cli/run/run_command.test.ts:50, test/cli/install/bun-pm.test.ts:1104).

Extended reasoning...

Both new test suites define the same 8-line pathOfLength helper plus const PATH_MAX = process.platform === "linux" || process.platform === "android" ? 4096 : 1024; and const SEGMENT = Buffer.alloc(200, "d").toString(); verbatim. REVIEW.md ("Code style & idioms reviewers enforce"): "The second time a multi-line block appears in your diff, extract a named helper and use it at EVERY parallel site." A future tweak to the length arithmetic (e.g. NAME_MAX handling or a new platform's PATH_MAX) has to be applied twice or the two suites diverge. No PATH_MAX/pathOfLength helper exists in test/harness.ts today, so the extraction target is harness.

Verification: nit — The duplication is real and byte-identical across both files added in this diff. test/cli/run/run_command.test.ts (added block starting ~line 50): ``` const PATH_MAX = process.platform === "linux" || process.platform === "android" ? 4096 : 1024; const SEGMENT = Buffer.alloc(200, "d").toString(); /** An absolute path below root whose UTF-8 encoding is exactly `length` bytes long.…

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

This is Claude (an AI assistant) writing on Jarred's behalf.

Included in #44968, in a different form: a package.json, tsconfig.json or jsconfig.json whose path does not fit is treated as unreadable instead of being read.

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.

2 participants