Skip to content

glob: fix brace depth overflow when skipping a deeply nested branch - #32733

Merged
Jarred-Sumner merged 4 commits into
mainfrom
farm/825ef7c6/glob-brace-depth-overflow
Jun 26, 2026
Merged

Jarred-Sumner merged 4 commits into
mainfrom
farm/825ef7c6/glob-brace-depth-overflow

Conversation

@robobun

@robobun robobun commented Jun 26, 2026 •

Copy link
Copy Markdown
Collaborator

Reproduction

new Bun.Glob("{a," + "{".repeat(300) + "}".repeat(301)).match("a")
// expected: true
// release:  false  (silent wrap)
// debug:    panic: attempt to add with overflow

Cause

skip_branch in src/glob/matcher.rs used state.brace_depth as its scan cursor while looking for the } that closes the current brace group, incrementing it for every { it passed and decrementing it for every }.

state.brace_depth is meant to count the groups the matcher has actually entered via match_brace_branch. Every other writer is bounded by the BraceStack cap of 10, which is why it is a u8. Braces inside a branch that is only being skipped over were never validated against that cap, so 255 or more consecutive { inside a skipped branch wrap the u8. skip_branch then satisfies its exit condition at the wrong } and the matcher resumes mid-group, returning the wrong result. Debug builds panic on the checked overflow instead.

Fix

Per @Jarred-Sumner's suggestion, skip_branch no longer writes the braces it merely scans over into state.brace_depth. It tracks the relative nesting of the skipped region in a local counter and decrements state.brace_depth exactly once, at the group's real closing }. That is all the old code ever did to it net (it always exited at original - 1); the intermediate inflated values were pure scratch and are what overflowed. State.brace_depth and Wildcard.brace_depth stay u8.

match_brace's forward pre-scan has the same class of bug in its (already local) i16 counter, which overflows with 32768 or more consecutive {. Widened it to i32.

Verification

Added a regression test in test/js/bun/glob/match.test.ts covering deeply nested braces in a skipped branch (before and after the matching branch, and under wildcard backtracking), plus a 40000-brace pattern for the match_brace counter. The first assertion returns false on the released build and true with this change. All 26 existing match.test.ts tests continue to pass.

…ranch

skip_branch scans forward over every '{' in a skipped brace branch and
increments State.brace_depth for each one. That field was a u8, so a
pattern with >255 nested braces inside a skipped branch wraps the
counter in release builds and skip_branch exits at the wrong '}',
causing the matcher to resume mid-group and return the wrong result.
Debug builds panic on the overflow.

Widen State.brace_depth and Wildcard.brace_depth to u32 (glob_index is
already u32, so the pattern cannot contain more braces than a u32 can
count). Also widen the i16 scan counter in match_brace to i32 for the
same reason.
@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

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

More reviews will be available in 16 minutes and 20 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

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

🚦 How do rate 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 see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 14411e58-ccc0-4c6d-a045-71e7333f955b

📥 Commits

Reviewing files that changed from the base of the PR and between a11e72a and a6e74ac.

📒 Files selected for processing (2)
  • src/glob/matcher.rs
  • test/js/bun/glob/match.test.ts

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

@robobun

robobun commented Jun 26, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 6:08 PM PT - Jun 26th, 2026

❌ @robobun, your commit a6e74ac has some failures in Build #65191 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 32733

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

bun-32733 --bun

@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 — straightforward integer widening to fix the overflow, with regression tests.

Extended reasoning...

Overview

This PR widens brace_depth from u8 to u32 in the glob matcher's State and Wildcard structs, widens a local i16 scan counter in match_brace to i32, and updates the one assignment site (u8::try_from(brace_stack.len()).expect(...) → as u32) to match. A regression test in test/js/bun/glob/match.test.ts covers deeply nested braces in skipped branches, including under wildcard backtracking.

Security risks

None. This is purely an integer-width fix in a pattern matcher. The previous behavior was a silent wrap in release (incorrect match result) or a debug panic — neither is exploitable beyond an incorrect boolean. Widening to u32 is bounded by glob_index: u32, so the new counter cannot itself overflow on valid input.

Level of scrutiny

Low. The change is mechanical type widening with no algorithmic changes. All read/write sites of brace_depth remain semantically identical (increment, decrement, compare, copy). Struct sizes are unchanged after padding (Wildcard stays 12 bytes, State stays 36 bytes), so there's no perf regression on the hot *state copies. The brace_stack.len() as u32 cast is safe since the stack is bounded at 10 entries.

Other factors

The bug-hunting system found no issues. The PR includes targeted regression tests that exercise the exact overflow path (>255 nested braces in a skipped branch, both before and after the matching alternative, and under backtracking). The files are not covered by CODEOWNERS. No outstanding reviewer comments.

@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 — straightforward type widening (u8→u32, i16→i32) with regression tests.

Extended reasoning...

Overview

This PR widens brace_depth from u8 to u32 in the State and Wildcard structs of src/glob/matcher.rs, widens the local brace_depth scan counter in match_brace from i16 to i32, and replaces a u8::try_from(brace_stack.len()).expect(...) with a direct as u32 cast. A regression test in test/js/bun/glob/match.test.ts exercises >255 nested braces inside a skipped brace branch, which previously wrapped the u8 counter (wrong result in release, panic in debug).

Security risks

None. This is a pure integer-width fix in a self-contained pattern matcher. It removes a debug-build panic and a release-build silent wraparound; it does not introduce any new code paths, allocations, or external inputs. The as u32 cast on brace_stack.len() is safe because BraceStack is a BoundedArray<Brace, 10> whose len() is at most 10.

Level of scrutiny

Low. The change is mechanical: four type annotations widened, no control-flow or arithmetic changes. Since glob_index is already u32, the number of { characters in a pattern cannot exceed u32::MAX, so the new width is provably sufficient. Struct sizes are unchanged after alignment padding (Wildcard was already 12 bytes due to two u32 fields; State was already 36).

Other factors

No CODEOWNERS cover the touched paths. The bug-hunting system found no issues. There are no outstanding human review comments on the PR. The added test covers the skipped-branch case before, after, and under wildcard backtracking, and the PR description confirms existing match.test.ts tests still pass.

@robobun

robobun commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

Diff is ready. Both CI runs failed only on unrelated flaky lanes; neither touched src/glob/ or the new test.

Build #64835 (284 pass / 2 fail):

  • darwin-26-aarch64: buildkite-agent artifact download timed out after 120s (infrastructure; no tests ran)
  • debian-13-x64: test/js/web/streams/streams-leak.test.ts (also flaked-then-passed on two other lanes in the same build)

Build #64863 (285 pass / 1 fail):

  • darwin-14-aarch64: bun-serve-file.test.ts timeout, fetch-file-upload.test.ts sendfile timeout, test-net-server-address.js EADDRINUSE on hardcoded port 12348

test/js/bun/glob/match.test.ts passed on every lane that ran it in both builds.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

Isn't the correct fix to not append skipped braces?

skip_branch was using state.brace_depth as its scan cursor, incrementing
it for every '{' it passed while looking for the closing brace of the
current group. state.brace_depth only means anything as the count of
groups actually entered via match_brace_branch, which is bounded by the
BraceStack cap; braces in a branch being skipped over are not, so enough
of them wrapped the u8 (wrong match result in release, panic in debug).

Track the skipped region's relative nesting in a local counter and
decrement state.brace_depth once at the real closing brace, which is all
the old code ever did to it net. State.brace_depth and
Wildcard.brace_depth go back to u8.

match_brace's forward pre-scan has the same class of bug in its already
local i16 counter, which overflows at 32768 consecutive '{'. Widen it to
i32.
@robobun robobun changed the title glob: fix u8 brace_depth overflow in skip_branch glob: fix brace depth overflow when skipping a deeply nested branch Jun 26, 2026
@robobun

robobun commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

Yes, good call. Reworked in 249a8c2.

skip_branch was using state.brace_depth as its scan cursor, so every { in the region being skipped over got counted as if it had been entered, and that scratch value is what overflowed. It now tracks the skipped region's relative nesting in a local counter and decrements state.brace_depth exactly once, at the group's real closing }. That is all the old code ever did to it net (it always exited at original - 1), so behavior is unchanged, and State.brace_depth / Wildcard.brace_depth go back to u8: every remaining writer is bounded by the BraceStack cap of 10.

One widening stayed: match_brace's forward pre-scan has its own local i16 counter with the same problem at 32768 or more consecutive {. It was already a local, just too narrow, so I widened it to i32 and added a test line for it.

Comment thread src/glob/matcher.rs
@robobun

robobun commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

CI on this branch was blocked by an unrelated, fleet-wide failure, not by anything in this diff: test/js/node/tls/node-tls-connect.test.ts asserts an OCSP - URI field on the live bun.sh certificate, and that certificate no longer carries one, so the test failed identically on every lane of every open PR (same failure on build 65181, which is unrelated to glob).

That was fixed on main in 034f693 (#32789) after this branch's base, so I merged main in to pick it up. The glob diff itself is unchanged: src/glob/matcher.rs and test/js/bun/glob/match.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 local-counter rework matches Jarred's suggestion and is behaviorally equivalent to the old skip_branch minus the overflow; description is now in sync.

Extended reasoning...

Overview

This PR fixes a u8 overflow in skip_branch (src/glob/matcher.rs) when the glob matcher skips past a brace branch containing ≥255 nested {. The fix stops using state.brace_depth as scratch storage during the skip scan and instead tracks relative nesting in a local u32, decrementing state.brace_depth exactly once at the group's real closing } — which is all the old code ever did to it net. It also widens match_brace's local pre-scan counter from i16 to i32 for the same overflow class at ≥32768 {. A regression test in test/js/bun/glob/match.test.ts covers both, including the wildcard-backtrack path.

Security risks

None. Glob pattern matching is not auth/crypto/permission-sensitive. The bug was a wrong-result (release) / debug-panic on pathological input, and the fix only changes which integer holds a scan cursor — no new allocation, recursion, or input-derived sizing.

Level of scrutiny

Low-to-moderate. The skip_branch change is ~10 lines and provably equivalent to the prior logic for the non-overflow case: old code computed end = depth - 1, then ran depth up and down through the skipped region and exited when it returned to end; new code runs a local nested from 0 and exits at the first } with nested == 0, then does the single depth -= 1. Same exit point, same net state mutation. The i16 → i32 widening is trivially safe. The u32 local cannot itself overflow because glob_index is u32, bounding the number of { in the pattern.

Other factors

  • Jarred-Sumner suggested exactly this approach ("Isn't the correct fix to not append skipped braces?"); the rework in 249a8c2 implements it directly.
  • My previous review flagged a stale PR description; the author confirmed it had already been updated (race with my snapshot), and the current description/title accurately reflect the shipped diff. That thread is resolved.
  • The only commit since my last review is a merge from main to pick up an unrelated fleet-wide CI fix (OCSP test); the glob diff is unchanged.
  • No CODEOWNER covers src/glob/ or *.rs.
  • The bug-hunting pass found nothing.
  • Regression tests are thorough (skipped-deep-nest before/after the matching branch, under wildcard backtrack, and the 40k-{ case for match_brace).

@robobun

robobun commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

Build 65191 update: merging main fixed the OCSP blocker, and nothing in this build fails on glob. test/js/bun/glob/match.test.ts passes everywhere it runs, and the one failing Windows shard does not even include it.

The remaining real failure is again upstream, not this diff: #32774 landed a sourcemap test on main that asserts a forward-slash sources path ("../in.js"), and on Windows it gets "..\in.js". It fails on all retries, so it is deterministic, not flake, and it will red out the Windows lanes of main and of every branch containing 990be52, this one included via the main merge. Reported on #32774 (comment).

The glob diff is unchanged and ready; this PR cannot go green on Windows until that is fixed on main.

@Jarred-Sumner
Jarred-Sumner merged commit 8c87871 into main Jun 26, 2026
68 of 75 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/825ef7c6/glob-brace-depth-overflow branch June 26, 2026 23:25
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