Skip to content

test scanner: skip paths exceeding MAX_PATH_BYTES instead of panicking - #32412

Closed
robobun wants to merge 3 commits into
mainfrom
farm/34634166/test-scanner-deep-path-panic
Closed

robobun wants to merge 3 commits into
mainfrom
farm/34634166/test-scanner-deep-path-panic

Conversation

@robobun

@robobun robobun commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Fixes Sentry BUN-3JDD (Panic: range end index 1041 out of range for slice of length 1023).

Repro

d=$(mktemp -d); cd "$d"
seg=$(printf 'a%.0s' {1..200})
# 22 levels → ~4400 byte abs path on Linux; 6 levels → ~1400 on macOS
for i in $(seq 1 22); do mkdir "$seg" && cd "$seg"; done
cd "$d" && bun test
panic: range end index 4236 out of range for slice of length 4095

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 fixed PathBuffer ([u8; MAX_PATH_BYTES]: 1024 on macOS, 4096 on Linux). _join_abs_string_buf concatenates the parts into an unbounded JoinScratch (heap-grows past MAX_PATH_BYTES), then calls normalize_string_buf with the fixed output buffer sliced to buf[leading_len..] (1023 / 4095 bytes after the leading /). normalize_string_generic_tz copies each segment with no bounds check at resolve_path.rs:1119, so the first segment that crosses the limit panics.

The Zig reference (normalizeStringGenericTZ) used an unchecked @memcpy here, 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.rs to the existing join_abs_string_buf_checked / abs_buf_checked variant, which returns None when 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 whole bun test run. The OS would reject such paths with ENAMETOOLONG anyway.

The initial scan path (bun test <huge-arg>) returns DoesNotExist on overflow, matching what the user would see from the failed open.

Verification

$ USE_SYSTEM_BUN=1 bun test test/cli/test/bun-test.test.ts -t "skips directories whose absolute"
(fail) ... panic: range end index 4249 out of range for slice of length 4095

$ bun bd test test/cli/test/bun-test.test.ts -t "skips directories whose absolute"
(pass) test file discovery (scanner) > skips directories whose absolute path exceeds MAX_PATH_BYTES instead of panicking

Existing scanner / path-ignore-pattern tests all pass.

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

robobun commented Jun 16, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:01 PM PT - Jun 16th, 2026

✅ @robobun, your commit f596e11ba7616727c772a2e44e84ad5f37529795 passed in Build #62850! 🎉


🧪   To try this PR locally:

bunx bun-pr 32412

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

bun-32412 --bun

@coderabbitai

coderabbitai Bot commented Jun 16, 2026 •

Copy link
Copy Markdown
Contributor

Review 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: Pro

Run ID: a778fa25-5a3a-432e-9124-c9f254a9f41f

📥 Commits

Reviewing files that changed from the base of the PR and between 1c57c41 and f596e11.

📒 Files selected for processing (1)
  • test/cli/test/bun-test.test.ts

Walkthrough

Scanner.rs replaces all unchecked absolute-path join calls with checked variants that return Option, mapping failures to ScanError::DoesNotExist in scan() or skipping entries during subdirectory traversal and next(). A regression test verifies that directory trees with paths exceeding MAX_PATH_BYTES are skipped gracefully without panicking.

Changes

Scanner checked path construction and regression test

Layer / File(s) Summary
Import updates and abs_buf_projected checked helper
src/runtime/cli/test/Scanner.rs
ZStr import made unconditional; join import switched to join_abs_string_buf_checked; abs_buf_projected changed to return Option<&[u8]> using the checked join.
scan() and subdirectory traversal with checked joins
src/runtime/cli/test/Scanner.rs
scan() maps checked join failure to ScanError::DoesNotExist; both non-windows and windows subdirectory loops reserve a NUL byte and skip entries whose checked join fails.
next() directory pruning and file filtering
src/runtime/cli/test/Scanner.rs
matches_path_ignore_pattern and file candidate processing both call checked abs_buf_projected; None results in early return or the candidate being ignored.
Regression test: deep path exceeding MAX_PATH_BYTES
test/cli/test/bun-test.test.ts
Adds isWindows import and a new test.skipIf(isWindows) block that builds a deep directory chain, runs bun test, and asserts exit code 0 with 1 pass and the shallow test output.

Possibly related PRs

  • oven-sh/bun#31850: Directly modifies the same abs_buf_projected usage and next()/scan() path-projection logic in Scanner.rs, making it a close predecessor to this change.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: switching the scanner from panicking on oversized paths to skipping them.
Description check ✅ Passed The description thoroughly addresses both required template sections with detailed problem analysis, root cause explanation, fix approach, and verification 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 test/cli/test/bun-test.test.ts Outdated
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.

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

@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

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 bun test positional argument of 997+ bytes on macOS (PATH_MAX 1024), since Scanner::scan joins argv into the fixed buffer unchecked. The scan() change in this PR covers that case too.

Branch farm/29eadf7b/test-scanner-long-positional has a minimal version of the same fix (checked join in scan() only, overflow mapped to DoesNotExist so it prints "had no matches" and exits 1) plus an argv-based test that runs on all platforms, in case it is useful when rebasing: a 5000-byte ./xaaa...test.ts positional, asserting "had no matches" and exit code 1. Verified failing on current main and passing with the fix.

@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #35863, which carries the same Scanner changes rebased onto current main and additionally bounds _join_abs_string_buf itself so the ~40 other callers no longer panic on over-length input.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Closing this one. #35863 now carries exactly this Scanner change rebased onto current main (the extra join_abs_string_buf hardening it used to add on top has been dropped), and the deep-tree scenario from this PR is covered there by two directory-walk cases in test/cli/test/bun-test.test.ts, next to the over-long positional argument cases from #35728.

@robobun robobun closed this Aug 15, 2026
Jarred-Sumner pushed a commit that referenced this pull request Aug 18, 2026
… 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>
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