Conversation
The test file scanner walks the directory tree by repeatedly joining parent directory paths with entry names into a fixed-size PathBuffer (MAX_PATH_BYTES: 1024 macOS, 4096 Linux). When a tree is deep enough that the absolute path exceeds this limit, the unchecked join panics in normalize_string_generic_tz with a slice index error. Use the existing checked join variant at each join site in the scanner and skip entries whose joined path would overflow. The OS would reject such paths with ENAMETOOLONG anyway, so skipping is the correct behavior rather than aborting the entire test run. Fixes Sentry BUN-3JDD.
|
Updated 1:01 PM PT - Jun 16th, 2026
✅ @robobun, your commit f596e11ba7616727c772a2e44e84ad5f37529795 passed in 🧪 To try this PR locally: bunx bun-pr 32412That installs a local version of the PR into your bun-32412 --bun |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
Walkthrough
ChangesScanner checked path construction and regression test
Possibly related PRs
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
On musl (Alpine), getcwd(3) fails once the cumulative path exceeds PATH_MAX. bash prints a cd warning for each subsequent level but mkdir/cd still succeed and exit 0. Filter the known warning before asserting stderr is clean, matching the existing handling in test/js/bun/glob/path-length.test.ts.
There was a problem hiding this comment.
LGTM — the musl stderr filter was applied in f596e11, and the checked-join swap looks correct.
Extended reasoning...
Overview
This PR fixes a Sentry-reported panic (range end index N out of range for slice of length 1023/4095) in the bun test file scanner. It touches two files: src/runtime/cli/test/Scanner.rs swaps every unchecked abs_buf / join_abs_string_buf call for the existing _checked variants that return Option, and handles None by skipping the entry (or returning ScanError::DoesNotExist for the initial scan argument). test/cli/test/bun-test.test.ts adds a regression test that builds a directory chain past MAX_PATH_BYTES and asserts bun test runs the shallow test instead of panicking.
Security risks
None. This is a defensive bounds-check change in the test-file discovery scanner. No auth, crypto, network, or permission logic is touched; the only behavioral change is that pathologically deep directory entries are silently skipped (the OS would have rejected them with ENAMETOOLONG anyway) instead of aborting the whole process.
Level of scrutiny
Low–medium. The transformation is mechanical: each call site goes from foo(...).len() / foo(...) to a let Some(...) = foo_checked(...) else { skip } pattern. The Windows branch was refactored to inline what abs_buf_z did (checked join + manual NUL byte + ZStr::from_raw), mirroring the non-Windows branch directly above it; reserving buf_len - 1 bytes for the join so the NUL fits at open_dir_buf[path2_len] is correct. abs_buf_checked and join_abs_string_buf_checked are pre-existing helpers in resolver/lib.rs and paths/resolve_path.rs, not new code.
Other factors
I previously flagged that the new test's setup-stderr assertion would fail on musl CI due to bash getcwd warnings; the author applied the same filter used in test/js/bun/glob/path-length.test.ts in f596e11 and resolved the thread. The bug-hunting pass on the latest revision found no issues. The Buildkite musl failure in the robobun comment is an LTO/llvm-link data-layout build infrastructure error, unrelated to the source changes here. No CODEOWNERS apply to these paths.
|
Issue #35728 reports the same panic ("range end index N out of range for slice of length 1023") reached from a different direction: a single Branch |
|
Superseded by #35863, which carries the same Scanner changes rebased onto current main and additionally bounds |
|
Closing this one. #35863 now carries exactly this Scanner change rebased onto current main (the extra |
… the path buffer (#35863) Fixes #35728 ### Problem - `bun test "/$(printf 'a%.0s' $(seq 1 5000)).test.ts"` aborts with `panic: range end index 5008 out of range for slice of length 4095` (exit 134) instead of reporting that the path matched nothing. The reporter hit it at 997 bytes on macOS, where the buffer is 1024 bytes. - `bun test` with no arguments aborts the same way when the tree it walks contains an entry (directory, test file, or symlink) whose absolute path is longer than the buffer; this is the scenario of #32412. - `Scanner` (`src/runtime/cli/test/Scanner.rs`) builds every absolute path it needs into a fixed `PathBuffer` with the unchecked `join_abs_string_buf` / `FileSystem::abs_buf`: the positional argument in `scan()`, each directory it descends into in the `dirs_to_scan` loop, and each directory and candidate file in `next()`. - For an entry that `readdir` reports as a symlink (or as unknown, which some filesystems do for everything), `next()` first asks the resolver for the entry's kind, and `RealFS::kind` (`src/resolver/lib.rs`) joins dir + name into its own `PathBuffer` with the same unchecked join. `normalize_string_buf` writes into the caller's buffer without a bounds check, so any result longer than the buffer panics at either layer. ### Fix - Every join in `Scanner.rs` goes through the existing `join_abs_string_buf_checked` / `FileSystem::abs_buf_checked`, which return `None` when the normalized result does not fit. - `scan()` maps `None` to `ScanError::DoesNotExist`, so a single over-long argument prints `Test filter "..." had no matches` and exits 1, and an over-long argument next to valid ones is ignored, exactly like a path that does not exist. This is correct because such a path cannot be opened either: the OS rejects it with `ENAMETOOLONG`. - The directory walk skips an entry whose path does not fit (not descended into, not matched, not added) and the rest of the run is unaffected. Directories are opened relative to the parent fd, so everything that does fit is still scanned when a tree continues past the limit. - `RealFS::kind` uses the checked join too and returns `Error::Sys(ENAMETOOLONG)` when the path does not fit. Its only callers, `Entry::kind` / `Entry::symlink`, already treat a failed stat as "unknown, assume file" (the same thing that happens when an entry is deleted between `readdir` and `stat`), and the scanner's file path then skips the entry through its checked join. Without this part, a symlink in the deepest directory still aborted with the scanner change alone (`range end index 4100 out of range for slice of length 4095`, in the `platform::Posix` join that only the resolver uses). - The POSIX `dirs_to_scan` join and `RealFS::kind` join into `buf[..len - 1]` / `buf[..len - 2]` because they write NULs after the path (same shape as `node_fs.rs`), so the set of paths that work is exactly the set that worked before; only the panicking inputs change behavior. The Windows `dirs_to_scan` branch passes the joined bytes to `open_dir_no_renaming_or_deleting_windows` directly (it transcodes to UTF-16 and never used the NUL), which leaves `FileSystem::abs_buf_z` without callers, so it is removed. - Scope: this PR covers the scanner, which is what #35728, #32412 and #38896 are about. The other `bun test` inputs that reach this buffer family (`--coverage-dir`, `--reporter-outfile`, bunfig `[test] root`, in `test_command.rs`) still abort on over-long values and are intentionally left to a separate change, as are `bun -c <path>` (`bunfig/arguments.rs`) and `Module._nodeModulePaths()` (`resolver_jsc.rs`); each has its own reporting path and test location. Per-site conversion to the checked helpers is how the rest of this family has been fixed (#37526, #37531). - Tests: six new cases in `test/cli/test/bun-test.test.ts` under "test file discovery (scanner)". Absolute (5000 and 100000 bytes) and relative over-long arguments report `had no matches` and exit 1; an over-long argument next to a valid one still runs the valid file; a tree whose deepest reachable directory contains an over-long test file, symlink and subdirectory runs only the shallow test file, scanned once without a bunfig and once with `pathIgnorePatterns` (which joins directories on a different path in `next()`). The three entries cover the file join, the resolver join and the directory joins. On the unfixed build all six abort (`range end index 5008 / 5036 / 100008 / 4108 / 4109 ...`); with the fix all six pass, and `bun-test.test.ts`, `path-ignore-patterns.test.ts`, `resolve.test.ts`, `require.test.ts` and `resolve-error.test.ts` pass (214 pass). `cargo check --target x86_64-pc-windows-msvc` for `bun_resolver` and `bun_runtime` and `cargo clippy` for both crates are clean. - The tests are skipped on Windows, where the buffer is `32767 * 3 + 1` bytes: larger than a command line or an NT path can deliver, so the overflow is not reachable there. ### Background - `PathBuffer` is `[u8; MAX_PATH_BYTES]`, the stack buffer bun uses for path syscalls: 4096 bytes on Linux, 1024 on macOS and the BSDs, 98302 on Windows. - `join_abs_string_buf(cwd, buf, parts)` resolves `parts` against `cwd` and normalizes the result into `buf`; it assumes the caller knows the result fits. `join_abs_string_buf_checked` is the variant for input of arbitrary length: it normalizes into heap scratch when the input might not fit and returns `None` if the normalized result is longer than `buf` (a long input that normalizes down through `..` still succeeds). `FileSystem::abs_buf` / `abs_buf_checked` are the same two functions with the project root as `cwd`. - The resolver's directory cache records each entry's kind from `readdir`'s `d_type`. Symlinks and `DT_UNKNOWN` entries have no kind yet, so `Entry::kind` stats them on first use through `RealFS::kind`, and on any error keeps the placeholder kind (file). The test scanner calls `Entry::kind` for every entry before deciding whether to queue or match it. - The scanner queues each subdirectory and opens it with `openat` relative to the parent's fd, so each syscall only sees one component; that is why a tree can legitimately be deeper than the buffer while still being walkable up to the limit. <details> <summary>Relationship to the other PRs and to the first version of this PR</summary> - #32412 (June) is the original of the Scanner change, for the deep-tree variant seen in Sentry; it was marked superseded by this PR but left open. Its scenario is covered by the directory-walk tests here (now including the resolver path, which neither version had), and it is closed. - #38896 by @deepshekhardas switches the `abs_buf_projected` sites to the checked join but leaves the two `dirs_to_scan` joins unchecked, so a deep tree still panics, and its 997 / 1200 byte arguments do not overflow the 4096-byte buffer on Linux (the "no matches" assertion fails there with or without the fix). Closed in favor of this PR. - The first version of this PR additionally made `join_abs_string_buf` itself return an empty slice on overflow. Main has since fixed the other callers of this family individually with the checked helpers, and an empty path silently handed to a caller that does not expect it is harder to reason about than `Option` at each site, so that part was dropped when the PR was rebased. </details>
Fixes Sentry BUN-3JDD (
Panic: range end index 1041 out of range for slice of length 1023).Repro
Stack:
bun test→Scanner::scan→abs_buf→join_abs_string_buf<Loose>→_join_abs_string_buf→normalize_string_buf→normalize_string_generic_tz→core::slice::index::slice_index_fail.Cause
The test scanner walks the tree by joining
[parent_abs_path, entry_name]into a fixedPathBuffer([u8; MAX_PATH_BYTES]: 1024 on macOS, 4096 on Linux)._join_abs_string_bufconcatenates the parts into an unboundedJoinScratch(heap-grows pastMAX_PATH_BYTES), then callsnormalize_string_bufwith the fixed output buffer sliced tobuf[leading_len..](1023 / 4095 bytes after the leading/).normalize_string_generic_tzcopies each segment with no bounds check atresolve_path.rs:1119, so the first segment that crosses the limit panics.The Zig reference (
normalizeStringGenericTZ) used an unchecked@memcpyhere, so this was pre-existing UB in the Zig build that Rust's bounds checking now catches as a panic.Fix
Switch every path join in
Scanner.rsto the existingjoin_abs_string_buf_checked/abs_buf_checkedvariant, which returnsNonewhen the normalized result does not fit. On overflow the scanner now skips that entry (directories are not descended into, files are not added) instead of aborting the wholebun testrun. The OS would reject such paths withENAMETOOLONGanyway.The initial scan path (
bun test <huge-arg>) returnsDoesNotExiston overflow, matching what the user would see from the failed open.Verification
Existing scanner / path-ignore-pattern tests all pass.