paths: size the thread-local join output buffers to MAX_PATH_BYTES on Windows - #36340
paths: size the thread-local join output buffers to MAX_PATH_BYTES on Windows#36340robobun wants to merge 8 commits into
Conversation
…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.
|
Warning Review limit reached
Next review available in: 8 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
Comment |
There was a problem hiding this comment.
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.
|
Updated 8:53 AM PT - Jul 29th, 2026
✅ @robobun, your commit cd9b94943ca3c7ba00a820b1afda943a8c286532 passed in 🧪 To try this PR locally: bunx bun-pr 36340That installs a local version of the PR into your bun-36340 --bun |
…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.
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.
There was a problem hiding this comment.
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:
LazyJoinBufmirrorsLazyPathBuf(same file) — alloc/Drop pairing,'static mutcontract, andboxed_zeroedbound all check out.TL_JOIN_BUF_LENkeeps the 4096 floor on POSIX so no caller sees reduced capacity;JOIN_BUFgoing private has no other in-crate references.- FFI: keying the fallback message on
name.len() >= MAX_PATH_BYTESis sound — the firstDynLib::openreaches the loader iffname.len() < MAX_PATH_BYTES, sodlerror()is fresh regardless of whetherabs_buf_checkedreturnsNoneor 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
dlerroron the skipped-fallback path; macOS diagnostic regression when only the fallback short-circuits) are both resolved by cd9b949'sname.len() >= MAX_PATH_BYTESgate +abs_buf_checkedswitch. I re-tracedDynLib::openatsrc/sys/lib.rs:6107andjoin_abs_string_buf_checkedto confirm the invariant holds on macOS (MAX_PATH_BYTES = 1024). JOIN_BUFvisibility drop (pub(crate)→ private) andJOIN_BUF_LEN→TL_JOIN_BUF_LENrename 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_TYPEin 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.
Repro
On Windows:
On any platform (
bun:ffihas no upstream length guard):Both abort the process (
panic = "abort").Cause
join_abs_string/join_abs_string_z/join/join_zwrite their normalized output into the thread-localPARSER_JOIN_INPUT_BUFFER/JOIN_BUF, each a platform-agnostic[u8; 4096]since the Zig era. On WindowsMAX_PATH_BYTESis32767*3+1 = 98302, so any caller ofFileSystem::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 innormalize_string_generic_tz. TheBun.serveroute-manifest path passesValid::path_string_length(bounds atMAX_PATH_BYTES, i.e. 98302 on Windows) and so reachesabs()with the full user string;bun:ffi'sdlopenfallback 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 POSIXMAX_PATH_BYTES <= 4096so the capacity is unchanged; on Windows it becomes 98302. Back them with the existing lazy-heap pointer pattern (same asLazyPathBufa few lines down) so Windows keeps 8 bytes per buffer in.tlsinstead of ~96 KB of raw zeros (PE/COFF has no TLS-BSS; see #30219).Separately, bound the
bun:ffidlopenfallback atMAX_PATH_BYTESbefore callingabs(): a library name that long cannot exist on the host, so skipping straight to theERR_DLOPEN_FAILEDreport 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 withENAMETOOLONG, soMAX_PATH_BYTESis 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 = 1024there). This does not make the primitive overflow-safe for inputs that normalize above the host limit; #35863 covers that at the_join_abs_string_buflevel.Verification
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