Skip to content

Fix Bun.pathToFileURL crash with relative paths longer than PATH_MAX#29673

Merged
Jarred-Sumner merged 2 commits into
mainfrom
farm/630ecabe/pathToFileURL-long-path
Apr 24, 2026
Merged

Fix Bun.pathToFileURL crash with relative paths longer than PATH_MAX#29673
Jarred-Sumner merged 2 commits into
mainfrom
farm/630ecabe/pathToFileURL-long-path

Conversation

@robobun

@robobun robobun commented Apr 24, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Bun.pathToFileURL() crashed with an index-out-of-bounds panic when given a relative path that, when joined with cwd, exceeded 4096 bytes.

ResolvePath__joinAbsStringBufCurrentPlatformBunString passed the fixed 4096-byte threadlocal join_buf as the output buffer to joinAbsStringBuf. The inner normalizer writes the result into that buffer and does not bound-check against it, so long inputs overflowed:

panic(main thread): index out of bounds: index 5738, len 4095
resolver.resolve_path.normalizeStringGenericTZ
  src/resolver/resolve_path.zig:915
...
Bun::functionPathToFileURL
  src/bun.js/bindings/BunObject.cpp:793

The fix uses a stack-fallback allocator sized to cwd.len + input.len + 2 for the output buffer, so short paths stay zero-alloc and long paths heap-allocate. This matches Node.js, which happily returns a file URL for arbitrarily long paths.

How did you verify your code works?

Bun.pathToFileURL("a".repeat(6000)).href.length
// before: panic
// after:  6022 (same as Node.js)

Added tests in test/js/bun/util/fileUrl.test.js covering both a long flat segment and a long path that normalizes down via .. segments. Verified the new tests crash the unfixed debug build and pass with the fix. zig:check-all passes on all targets.

Found via Fuzzilli (fingerprint d8774a764480e01d).

@robobun

robobun commented Apr 24, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 2:19 PM PT - Apr 24th, 2026

@robobun, your commit 31a2c88 has 3 failures in Build #47802 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 29673

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

bun-29673 --bun

@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 950fef7e-dbdd-4fa4-8a12-f453b8267616

📥 Commits

Reviewing files that changed from the base of the PR and between c8a668f8fb08c8c660366b02c0ed5e8141feeda0 and 31a2c88.

📒 Files selected for processing (2)
  • src/resolver/resolve_path.zig
  • test/js/bun/util/fileUrl.test.js

Walkthrough

Replaced a fixed 4096-byte thread-local join buffer with a stack-fallback allocation sized from cwd + input length in path resolution; updated tests to cover extremely long relative paths and normalization with .. in pathToFileURL.

Changes

Cohort / File(s) Summary
Path resolution implementation
src/resolver/resolve_path.zig
Capture cwd locally; replace fixed thread-local 4096-byte join_buf with a std.heap.stackFallback/allocator-backed u8 buffer sized cwd.len + input_utf8_len + 2; allocate via bun.handleOom, defer free, pass buffer into joinAbsStringBuf, and retain cloning out_slice into a bun.String before return.
File URL tests
test/js/bun/util/fileUrl.test.js
Add two assertions: one verifies pathToFileURL preserves long relative-path suffixes (beyond PATH_MAX) in file: URLs; another verifies normalization of long relative paths containing .. segments resolves to the expected final path.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main fix: resolving a crash in Bun.pathToFileURL when handling relative paths longer than PATH_MAX.
Description check ✅ Passed The pull request description fully addresses both required template sections: it clearly explains what the PR does (the buffer overflow fix) and provides comprehensive verification details including test cases and build results.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

Comment thread src/resolver/resolve_path.zig Outdated
// The input is user-controlled and may be arbitrarily long. The
// threadlocal `join_buf` is only 4096 bytes, so use a stack-fallback
// allocator that heap-allocates for oversized inputs.
var sfa = std.heap.stackFallback(bun.MAX_PATH_BYTES * 2, bun.default_allocator);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be 32 KB of stack on Windows. Use a smaller max.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped to a fixed 4096-byte inline buffer in c8a668f8fb — same footprint as the previous threadlocal join_buf on all platforms, heap-allocates only when cwd + input exceeds that.

(The earlier CI failure was infrastructure — Image not found: linux-*-v30 agent provisioning errors, also affecting main.)

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Fix out-of-bounds panic in pathToFileURL with long relative paths #28324 - Fixes the same out-of-bounds panic in pathToFileURL with long relative paths
  2. fix OOB in pathToFileURL with long relative paths #28464 - Fixes the same OOB in pathToFileURL with long relative paths
  3. Fix out-of-bounds write in pathToFileURL with long relative paths #28585 - Fixes the same out-of-bounds write in pathToFileURL with long relative paths

🤖 Generated with Claude Code

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

LGTM — correct crash fix with tests; the stack-sizing nit is non-blocking (this runs on JS threads with multi-MB stacks) but worth a one-line follow-up.

Extended reasoning...

Overview

This PR fixes an index-out-of-bounds panic in Bun.pathToFileURL() when given a relative path that, joined with cwd, exceeds 4096 bytes. The change replaces the fixed 4096-byte threadlocal join_buf in ResolvePath__joinAbsStringBufCurrentPlatformBunString with a stack-fallback allocator sized to cwd.len + input.len + 2, and adds two regression tests in test/js/bun/util/fileUrl.test.js. ~12 lines of Zig + ~12 lines of test code.

Security risks

None introduced. The PR actually removes a latent hazard: the previous code wrote past the end of a fixed-size threadlocal buffer for long inputs (a panic in debug, but potentially silent memory corruption in release if bounds checks were elided). The new code allocates exactly enough space, clones the result into a bun.String, then frees the buffer — no UAF or leak.

Level of scrutiny

Low-to-moderate. This is a small, focused fuzzer-found crash fix in a single exported helper, following the same stackFallback + alloc pattern already used by joinAbsStringBufChecked and JoinScratch in the same file. The buffer-size math (cwd.len + input.len + 2) is correct since the normalized output cannot exceed the unnormalized concatenation. No CODEOWNERS apply to this path.

Other factors

The one inline nit (outer stackFallback(MAX_PATH_BYTES * 2) doubling the inner JoinScratch stack footprint to ~384KB on Windows) is valid as a style/convention concern — joinAbsStringBufChecked uses 1× for exactly this reason — but it is not a correctness issue: Bun.pathToFileURL is a JS host function called from threads with 4–18MB stacks, and on POSIX the extra is only ~8KB. The author can trivially drop the * 2 in a follow-up without blocking this crash fix. Tests cover both the long-flat-segment case and the long-path-that-normalizes-down case, and the PR description confirms they crash the unfixed build.

Comment thread src/resolver/resolve_path.zig 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.

LGTM — the 4096-byte stackFallback addresses the stack-size feedback while keeping the common case zero-alloc.

Extended reasoning...

Overview

This PR fixes an OOB panic in Bun.pathToFileURL() when given a relative path that, joined with cwd, exceeds 4096 bytes. The fix replaces the fixed 4096-byte threadlocal join_buf output buffer in ResolvePath__joinAbsStringBufCurrentPlatformBunString with a stackFallback(4096)-backed allocation sized to cwd.len + input.len + 2, so short paths remain zero-alloc and long paths heap-allocate. Two regression tests were added.

Feedback addressed

The first revision used stackFallback(bun.MAX_PATH_BYTES * 2), which Jarred flagged for excessive stack on Windows and which I separately flagged for stacking on top of JoinScratch's own inline buffer. Commit c8a668f switched to stackFallback(4096, ...), which is the same size as the original threadlocal buffer — so there is no stack-usage regression vs. the pre-PR code, and both review comments are resolved.

Security risks

None. The change converts an OOB write/panic on user-controlled input into a bounded heap allocation. bun.handleOom guards the allocation. The output buffer bound cwd.len + str.len + 2 is safe: joinAbsStringBuf normalizes with allow_above_root=false and no NT prefix, so the result cannot exceed the concatenated input length.

Level of scrutiny

Low-to-moderate. ~10 LOC in a single exported helper used only by Bun.pathToFileURL, plus tests. The pattern (stackFallback + alloc + defer free) matches joinAbsStringBufChecked in the same file. The maintainer already reviewed the approach and the requested change was applied.

Other factors

Bug-hunter found no issues on the current revision. Tests cover both the flat long-segment case and the ..-normalization case. CI is building.

robobun added 2 commits April 24, 2026 07:06
ResolvePath__joinAbsStringBufCurrentPlatformBunString used the 4096-byte
threadlocal join_buf as the output buffer for normalization. When the
input relative path (joined with cwd) exceeded 4096 bytes, normalizeStringGenericTZ
would index past the end of the buffer.

Use a stack-fallback allocator sized to cwd + input instead, matching
Node.js which returns a file URL for arbitrarily long paths.
bun.MAX_PATH_BYTES * 2 is ~192KB on Windows. Use 4096 bytes of inline
stack (same as the previous threadlocal join_buf) and heap-allocate
when cwd + input exceeds that.
@robobun robobun force-pushed the farm/630ecabe/pathToFileURL-long-path branch from c8a668f to 31a2c88 Compare April 24, 2026 07:10

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

LGTM — the 4096-byte stackFallback addresses the earlier stack-size feedback and keeps the same footprint as the old threadlocal buffer.

Extended reasoning...

Overview

This PR fixes an out-of-bounds panic in Bun.pathToFileURL() when the relative input + cwd exceeds 4096 bytes. The change is confined to one exported function in src/resolver/resolve_path.zig (replacing the fixed threadlocal join_buf with a stackFallback(4096)-backed buffer sized to cwd.len + input.len + 2) plus two regression tests in test/js/bun/util/fileUrl.test.js.

Security risks

None. The change removes an unchecked write past a fixed buffer (which previously panicked in debug and was UB-adjacent in release). The new sizing is a strict upper bound: normalization never produces output longer than its concatenated input, so cwd.len + input.len + 2 is safe. OOM is handled via bun.handleOom.

Level of scrutiny

Low-to-moderate. The diff is ~10 lines of mechanical buffer-management code following the same pattern already used by joinAbsStringBufChecked in the same file. The output is immediately cloned into a bun.String before the buffer is freed, so there's no lifetime hazard.

Other factors

  • Jarred's review comment ("use a smaller max") and my own earlier inline comment about doubled MAX_PATH_BYTES stack usage were both addressed in c8a668f8fb by switching to a fixed 4096-byte inline buffer — identical stack footprint to the previous threadlocal on all platforms.
  • The two new tests directly exercise the crash (6000-char segment) and the ..-normalization path (20000-char input that collapses to cwd/final).
  • CI failures are unrelated: socket.test.ts is a known Windows flake (just loosened on main in #29663), and the Linux build-cpp jobs failed on agent provisioning, not compilation.
  • The bug-hunting system found no issues on the current revision.

@robobun

robobun commented Apr 24, 2026

Copy link
Copy Markdown
Collaborator Author

CI on build 47802 — 272/275 passed. The 3 failures are pre-existing on main and reproduce on unrelated PRs:

  • test/integration/build-prefetch/prefetch.test.ts (x64-asan): JSC @assert(!dependency.isAsync) when evaluating scripts/build/fetch-cli.ts — new test from 2883530. Also failing on builds 47798, 47805, 47807, 47808.
  • test/js/bun/webview/webview-chrome.test.ts (darwin aarch64): 90s timeout. Also on 47798.
  • test/cli/install/bun-run-bunfig.test.ts (darwin 14 aarch64): agent realpath mismatch (/opt/homebrew/... vs /Users/administrator/...).

fileUrl.test.js (this PR's tests) passes on all platforms.

@Jarred-Sumner Jarred-Sumner merged commit ba8aec1 into main Apr 24, 2026
62 of 65 checks passed
@Jarred-Sumner Jarred-Sumner deleted the farm/630ecabe/pathToFileURL-long-path branch April 24, 2026 21:50
structwafel pushed a commit to structwafel/bun that referenced this pull request Apr 25, 2026
…ven-sh#29673)

## What does this PR do?

`Bun.pathToFileURL()` crashed with an index-out-of-bounds panic when
given a relative path that, when joined with cwd, exceeded 4096 bytes.

`ResolvePath__joinAbsStringBufCurrentPlatformBunString` passed the fixed
4096-byte threadlocal `join_buf` as the output buffer to
`joinAbsStringBuf`. The inner normalizer writes the result into that
buffer and does not bound-check against it, so long inputs overflowed:

```
panic(main thread): index out of bounds: index 5738, len 4095
resolver.resolve_path.normalizeStringGenericTZ
  src/resolver/resolve_path.zig:915
...
Bun::functionPathToFileURL
  src/bun.js/bindings/BunObject.cpp:793
```

The fix uses a stack-fallback allocator sized to `cwd.len + input.len +
2` for the output buffer, so short paths stay zero-alloc and long paths
heap-allocate. This matches Node.js, which happily returns a file URL
for arbitrarily long paths.

## How did you verify your code works?

```js
Bun.pathToFileURL("a".repeat(6000)).href.length
// before: panic
// after:  6022 (same as Node.js)
```

Added tests in `test/js/bun/util/fileUrl.test.js` covering both a long
flat segment and a long path that normalizes down via `..` segments.
Verified the new tests crash the unfixed debug build and pass with the
fix. `zig:check-all` passes on all targets.

Found via Fuzzilli (fingerprint `d8774a764480e01d`).

---------

Co-authored-by: robobun <robobun@users.noreply.github.com>
xhjkl pushed a commit to xhjkl/bun that referenced this pull request May 14, 2026
…ven-sh#29673)

## What does this PR do?

`Bun.pathToFileURL()` crashed with an index-out-of-bounds panic when
given a relative path that, when joined with cwd, exceeded 4096 bytes.

`ResolvePath__joinAbsStringBufCurrentPlatformBunString` passed the fixed
4096-byte threadlocal `join_buf` as the output buffer to
`joinAbsStringBuf`. The inner normalizer writes the result into that
buffer and does not bound-check against it, so long inputs overflowed:

```
panic(main thread): index out of bounds: index 5738, len 4095
resolver.resolve_path.normalizeStringGenericTZ
  src/resolver/resolve_path.zig:915
...
Bun::functionPathToFileURL
  src/bun.js/bindings/BunObject.cpp:793
```

The fix uses a stack-fallback allocator sized to `cwd.len + input.len +
2` for the output buffer, so short paths stay zero-alloc and long paths
heap-allocate. This matches Node.js, which happily returns a file URL
for arbitrarily long paths.

## How did you verify your code works?

```js
Bun.pathToFileURL("a".repeat(6000)).href.length
// before: panic
// after:  6022 (same as Node.js)
```

Added tests in `test/js/bun/util/fileUrl.test.js` covering both a long
flat segment and a long path that normalizes down via `..` segments.
Verified the new tests crash the unfixed debug build and pass with the
fix. `zig:check-all` passes on all targets.

Found via Fuzzilli (fingerprint `d8774a764480e01d`).

---------

Co-authored-by: robobun <robobun@users.noreply.github.com>
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