Skip to content

paths: size the thread-local join output buffers to MAX_PATH_BYTES on Windows - #36340

Open
robobun wants to merge 8 commits into
mainfrom
farm/81bdda88/join-buf-windows-max-path
Open

paths: size the thread-local join output buffers to MAX_PATH_BYTES on Windows#36340
robobun wants to merge 8 commits into
mainfrom
farm/81bdda88/join-buf-windows-max-path

Conversation

@robobun

@robobun robobun commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Repro

On Windows:

Bun.serve({
  port: 0,
  routes: { "/": { index: "./index.html", files: [{
    input: "index.html", path: "C:\\" + "a".repeat(5000), loader: "html",
    isEntry: true, headers: { etag: "x", "content-type": "text/html" },
  }] } },
});
panic: range end index 5003 out of range for slice of length 4096

On any platform (bun:ffi has no upstream length guard):

require("bun:ffi").dlopen("/" + "a".repeat(5000) + ".so", { f: { args: [], returns: "void" } });
panic: range end index 5003 out of range for slice of length 4095

Both abort the process (panic = "abort").

Cause

join_abs_string / join_abs_string_z / join / join_z write their normalized output into the thread-local PARSER_JOIN_INPUT_BUFFER / JOIN_BUF, each a platform-agnostic [u8; 4096] since the Zig era. On Windows MAX_PATH_BYTES is 32767*3+1 = 98302, so any caller of FileSystem::abs() (and ~40 direct callers in the install/CLI/resolver/bake paths) with a path whose normalized length lands in [4096, 98302) slice-index panics in normalize_string_generic_tz. The Bun.serve route-manifest path passes Valid::path_string_length (bounds at MAX_PATH_BYTES, i.e. 98302 on Windows) and so reaches abs() with the full user string; bun:ffi's dlopen fallback has no length check at all.

Fix

Size both thread-local join buffers to max(MAX_PATH_BYTES, 4096) so the output buffer can hold any valid host path. On POSIX MAX_PATH_BYTES <= 4096 so the capacity is unchanged; on Windows it becomes 98302. Back them with the existing lazy-heap pointer pattern (same as LazyPathBuf a few lines down) so Windows keeps 8 bytes per buffer in .tls instead of ~96 KB of raw zeros (PE/COFF has no TLS-BSS; see #30219).

Separately, bound the bun:ffi dlopen fallback at MAX_PATH_BYTES before calling abs(): a library name that long cannot exist on the host, so skipping straight to the ERR_DLOPEN_FAILED report is the correct outcome.

Why this buffer size

join_abs_string's output is a normalized absolute path. The kernel rejects any path that long with ENAMETOOLONG, so MAX_PATH_BYTES is the tight upper bound for a path that is usable by the caller. The 4096 floor keeps the POSIX capacity exactly where it was so no unguarded caller sees a new panic surface on macOS (MAX_PATH_BYTES = 1024 there). This does not make the primitive overflow-safe for inputs that normalize above the host limit; #35863 covers that at the _join_abs_string_buf level.

Verification

USE_SYSTEM_BUN=1 bun test test/js/bun/ffi/ffi-error-messages.test.ts
  (fail) dlopen with a 5000-byte library path ...    (panic, exit 134/3)
  (fail) dlopen with a 100000-byte library path ...

bun bd test test/js/bun/ffi/ffi-error-messages.test.ts
  7 pass / 0 fail   (Linux, Windows)

bun bd test test/js/bun/http/bun-serve-html-manifest.test.ts
  5 pass / 0 fail   (Linux, Windows; the new case fails on Windows canary)

cargo test -p bun_paths resolve_path::tests
  2 pass   (buffer-length invariant + MAX_PATH_BYTES-8 round-trip)

bun run rust:check-all
  10 ok

no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/ffi/ffi-error-messages.test.ts test/js/bun/http/bun-serve-html-manifest.test.ts

robobun added 3 commits July 29, 2026 09:00
…Windows

The thread-local output buffers behind join_abs_string / join_abs_string_z /
join / join_z were a platform-agnostic [u8; 4096]. On Windows MAX_PATH_BYTES
is 98302, so any caller of FileSystem::abs() (or a direct join_abs_string
call) with a path whose normalized length landed in [4096, 98302) aborted the
process with a slice-index panic in normalize_string_generic_tz.

Size the buffers to max(MAX_PATH_BYTES, 4096) and back them with a lazily
heap-allocated pointer (the existing LazyPathBuf pattern) so the Windows .tls
section stays at 8 bytes per buffer instead of ~96KB of zeros.

Also guard the unbounded FileSystem::abs() fallback in bun:ffi dlopen, which
reached the same primitive with no upstream length check on any platform.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a686bce7-a8a4-4c5a-83f6-2bba7e6d630c

📥 Commits

Reviewing files that changed from the base of the PR and between 59242d6 and cd9b949.

📒 Files selected for processing (4)
  • src/paths/resolve_path.rs
  • src/runtime/ffi/ffi_body.rs
  • test/js/bun/ffi/ffi-error-messages.test.ts
  • test/js/bun/http/bun-serve-html-manifest.test.ts

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.

Beyond the inline nit, I also checked: the pub(crate) → private visibility drop on JOIN_BUF — no other in-crate users; the LazyJoinBuf unsafe pattern — mirrors the existing LazyPathBuf/lazy_path_buf a few lines down (same heap::into_raw/take pairing, same &'static mut contract, same Drop); and Box::new_zeroed() — already used elsewhere in-tree so the feature is enabled.

Extended reasoning...

The one finding is a diagnostic-quality nit on degenerate input. The core change touches a widely-used path primitive with new unsafe thread-local storage, so leaving for a human to confirm the TLS sizing decision (2×~96 KB lazily per thread on Windows) and the scoping vs #35863. The mechanical concerns I could verify statically are noted in the message.

Comment thread src/runtime/ffi/ffi_body.rs Outdated
Comment thread src/paths/resolve_path.rs Outdated
Comment thread src/paths/resolve_path.rs
Comment thread src/paths/resolve_path.rs Outdated
Comment thread src/paths/resolve_path.rs Outdated
Comment thread src/runtime/ffi/ffi_body.rs Outdated
@robobun

robobun commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 8:53 AM PT - Jul 29th, 2026

@robobun, your commit cd9b94943ca3c7ba00a820b1afda943a8c286532 passed in Build #85105! 🎉


🧪   To try this PR locally:

bunx bun-pr 36340

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

bun-36340 --bun

Comment thread src/paths/resolve_path.rs
…ary names

The previous name.len() guard ignored top_level_dir, so a relative library
name whose cwd+name exceeded the join buffer still reached the slice-index
panic. abs_buf_checked returns None on overflow regardless of how the length
is reached.

Also swap the open-coded Box::new_zeroed().assume_init() for the existing
bun_core::boxed_zeroed helper.
Comment thread src/runtime/ffi/ffi_body.rs Outdated
When the first DynLib::open reached the loader (name.len() < MAX_PATH_BYTES),
dlerror()/GetLastError() is fresh regardless of whether the fallback
short-circuited. Keying on last_err could discard a rich dlerror on macOS/BSD
when cwd + a short library name exceeded 1024 bytes.
Comment thread src/runtime/ffi/ffi_body.rs

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

Both earlier inline findings are addressed in cd9b949 and I didn't find anything new, but this reworks the thread-local output buffers behind join_abs_string/join (~40 indirect callers via FileSystem::abs()) with a new lazy-heap unsafe-pointer type and platform-dependent sizing — worth a maintainer sign-off on the approach.

What was reviewed:

  • LazyJoinBuf mirrors LazyPathBuf (same file) — alloc/Drop pairing, 'static mut contract, and boxed_zeroed bound all check out.
  • TL_JOIN_BUF_LEN keeps the 4096 floor on POSIX so no caller sees reduced capacity; JOIN_BUF going private has no other in-crate references.
  • FFI: keying the fallback message on name.len() >= MAX_PATH_BYTES is sound — the first DynLib::open reaches the loader iff name.len() < MAX_PATH_BYTES, so dlerror() is fresh regardless of whether abs_buf_checked returns None or the second open short-circuits.
  • Tests drain pipes concurrently, use test.concurrent.each, assert exit code last.
Extended reasoning...

Overview

Resizes the two thread-local join output buffers (PARSER_JOIN_INPUT_BUFFER, JOIN_BUF) from a fixed [u8; 4096] to max(MAX_PATH_BYTES, 4096), backing them with a new LazyJoinBuf heap-pointer type so Windows keeps 8 bytes in .tls instead of ~96 KB of zeros. Separately refactors the bun:ffi dlopen fallback to use abs_buf_checked into a pooled PathBuffer and to key its stale-dlerror fallback on name.len(). Adds Rust unit tests and two JS subprocess regression tests.

Security risks

None. This is defensive hardening: user-reachable slice-index panics (process abort under panic = "abort") become recoverable errors. No new attack surface; buffer capacity only grows.

Level of scrutiny

High. join_abs_string / join back FileSystem::abs() and ~40 direct callers across install/CLI/resolver/bake — a foundational primitive. The new LazyJoinBuf carries unsafe raw-pointer deref and a Drop impl. That said, it is a byte-for-byte copy of LazyPathBuf a few lines below (an accepted in-tree pattern), and the 4096 floor guarantees no POSIX caller sees reduced capacity.

Other factors

  • Two prior inline findings from me (stale dlerror on the skipped-fallback path; macOS diagnostic regression when only the fallback short-circuits) are both resolved by cd9b949's name.len() >= MAX_PATH_BYTES gate + abs_buf_checked switch. I re-traced DynLib::open at src/sys/lib.rs:6107 and join_abs_string_buf_checked to confirm the invariant holds on macOS (MAX_PATH_BYTES = 1024).
  • JOIN_BUF visibility drop (pub(crate) → private) and JOIN_BUF_LENTL_JOIN_BUF_LEN rename have no other in-crate references.
  • The PR description explicitly scopes out inputs that normalize above the host limit (deferred to #35863) — a maintainer should confirm that scoping is agreed.
  • CI build #85105 was still running at last timeline update; the Windows-specific behavior (98302-byte buffer, ERR_INVALID_ARG_TYPE in the manifest test) is only verifiable there.

Deferring because the blast radius (core path primitive, new unsafe TLS type, platform-dependent sizing) warrants a maintainer glance even though I found nothing wrong with the final revision.

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