Repository navigation
Conversation
A `**` only acts as a globstar when it is a whole path segment; anywhere
else (`a**`) it means `*`. The matcher checked the "starts a segment"
half of that rule with `glob_index.saturating_sub(glob_start) < 3`,
where glob_start is the start of the brace branch currently being
matched. When a branch fails and the matcher backtracks into a globstar
that encloses the group, glob_index lands before glob_start, the
subtraction saturates to 0, and every `**` re-matched on the way back
to the group qualifies, so `**/a**/{x,y}` matched "ab/ay" as
`**/` + `a` + an empty `**/` + `y`.
The check also ran only after skip_globstars() had already merged any
`/**` segments following the `**` into it, so `x**/**` became a single
trailing globstar and matched "x" and "xy".
Check the `**`'s actual neighbourhood (pattern start, a preceding `/`,
or the start of a brace branch on the brace stack) before coalescing,
and treat a `**` that fails it exactly like `*`. The `!` prefix is
sliced off up front so the pattern start is index 0, which removes the
glob_start parameter.
|
Warning Review limit reached
Next review available in: 13 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 (2)
Comment |
|
Status: fix pushed, waiting for CI. Reproduced on the current release ( Related: #38996 fixes the |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. The is_segment_start rule and the !-prefix slicing check out, and the test coverage is thorough. Given it's a semantic change to the core glob backtracking matcher (used across the bundler, install, shell, and fs.glob) and the noted overlap with #38996, a human look would still be worthwhile.
What was reviewed:
- Traced the
b'*'arm before/after: gatingskip_globstarsonis_segment_startfixes both the brace-backtrack case andx**/**, and the removedglob_index - 3check is fully subsumed. - Checked
is_segment_startfor false hits from stale/sequential brace frames —branch_idxis a unique glob offset inside the group, so a**outside it can't collide;glob[glob_index-1]can't underflow. - Verified re-slicing
globpast!keeps all indices consistent (state starts at 0) and preserves!**/…behavior. - Confirmed
**band top-levela**take the same effective path as before;scan()is untouched.
Extended reasoning...
Overview
This PR fixes a correctness bug in src/glob/matcher.rs where a non-segment ** (e.g. a**) was incorrectly promoted to a globstar when re-scanned via backtracking from inside a brace branch, causing patterns like **/a**/{x,y} to spuriously match ab/ay. The fix replaces the glob_start-relative check with is_segment_start(), which decides from the **'s own position (index 0, preceded by /, or a live branch_idx on the brace stack), and moves that check ahead of skip_globstars so x**/** no longer collapses into a trailing globstar. The ! prefix is now sliced off the pattern up front so index 0 is the true pattern start, which lets the glob_start parameter be removed entirely. 61 new assertions in match.test.ts cover the failing shapes, positive controls, real-globstar controls, and each-ingredient-removed controls.
Security risks
None. This is pure in-memory pattern matching over caller-supplied byte slices; no I/O, no allocation changes, no new panics. The only index arithmetic added is glob[glob_index as usize - 1], guarded by glob_index != 0.
Level of scrutiny
Medium-high. The diff is small and the reasoning in the PR description is unusually rigorous (mechanism traced to the saturating subtraction, differential fuzz against picomatch over 4000×40 inputs, existing 1582-assertion suite plus the ported node fs.glob suite passing). But glob_match_impl is a hand-written backtracking matcher whose brace/globstar interaction is subtle, and it sits under Bun.Glob, the bundler, bun install, the shell, and fs.glob. A behavior change here fans out widely.
Other factors
- I traced the new rule against the concern in
skip_branch's doc comment that the brace stack "also holds already-exited sequential groups": those frames'branch_idxvalues point inside their own group, so a**after the group cannot collide with them — no false positives. - The
**bcase (segment-start but not segment-end) still enters theis_globstarblock and fails the trailing-/check exactly as before; the top-levela**case now takes the single-*arm twice instead of the+= 2short path but ends at the sameglob_indexwith equivalent wildcard state. - The PR explicitly flags overlap with #38996 and a pre-existing brace-boundary false negative left for a follow-up; a maintainer should coordinate the merge order.
- No prior human review on the timeline; only a rate-limited CodeRabbit placeholder.
|
Nothing actionable came out of the automated review above (no line comments). On its one open point, merge order with #38996: both PRs now link to each other, and the tests in each stay valid regardless of which lands first; the second one only needs a rebase of the few shared lines in the |
|
Closing #38996 in favour of this PR: it contains the same matcher change for the Two things from #38996 that may be worth folding in here, since they are not in the current test block:
For the reviewer's benefit, the differential run from #38996 applies to the ordering change this PR shares: 8612 patterns over an 8-token grammar x 128 paths (1,102,336 results) against the release build; the only results that changed were for patterns with a non-segment |
Problem
new Bun.Glob("**/a**/{x,y}").match("ab/ay")returnstrue. The segmentayis neitherxnory;**/a*/{x,y}(which is whata**is documented to mean) correctly returnsfalse, and so do picomatch, micromatch and bash. Same for**/a**/{x}vsab/ax,**/a**/{x,y}/cvsab/ay/c,**/{a,b}**/{x,y}vsq/bz/bx, and!**/a**/{x,y}gives the inverse wrong answer. Found by a Bun.Glob-vs-picomatch differential fuzzer.**that is not a whole segment, and a brace group after it.a**/{x,y},*/a**/{x,y}and**/a**/yare all correct againstab/ay.src/glob/matcher.rs,b'*'arm ofglob_match_impl: whether a**starts a segment was checked withstate.glob_index.saturating_sub(glob_start) < 3 || glob[glob_index - 3] == b'/', whereglob_startis the index the currentglob_match_implcall began at: 0 at top level, but the start of the branch when matching a brace alternative (so that{**/a,**/b}works). When a branch fails, it backtracks into the enclosing globstar, which re-runs the part of the pattern that precedes the group inside the branch's call. Every index there is belowglob_start, the subtraction saturates to 0, and every**re-matched on the way back qualifies as a globstar.**/a**/{x,y}is then matched againstab/ayas**/=ab/,a=a, an empty**/, andy=y.skip_globstarshad folded any/**segments following the**into it, sox**/**was matched as one trailing globstar and matchedxandxy(glob: stop a non-segment**from swallowing the**segments after it in match() #38996 fixes this shape on its own; see the note below).Fix
is_segment_start()decides from the**'s own neighbourhood whether it begins a segment: it is at index 0 of the pattern, follows a/, or is thebranch_idxof a frame onbrace_stack. That result gates bothskip_globstarsand the globstar path; a**that fails it goes through the*arm twice, which is exactly whata*does. Only the "followed by/or end of pattern" half of the rule is still checked after the collapse.**that is a whole segment, and which position a**sits at does not depend on which brace branch is being tried, so the answer must not depend onglob_start. The brace stack holds exactly the branches we are currently inside (match_brace_branchpushes before matching the branch plus the rest of the pattern and pops after), sobranch_idxidentifies a branch-leading**wherever we arrive at it from, including by backtracking, and a literal,or{(a,**/b) does not qualify. For a**that does begin a segment nothing changes: a pattern-start**is index 0 (the!prefix is sliced off up front, which is what let theglob_startparameter go away), a branch-leading**is on the stack, and every**reached by the collapse is preceded by/.test/js/bun/glob/match.test.ts, the newdescribe("a \` that is not a whole segment behaves like `*`"). 11 of its 61 assertions fail on the unfixed binary (7 brace-backtracking shapes, 4x/shapes); the other 50 are controls that pass before and after: the same patterns against paths they should match, real globstars across the same backtrack (/a//{x,y},/{/x,y},/a/{b/**/x,y}`), and each of the three ingredients removed.match.test.ts(31 tests, 1582 assertions, including the ported bash/micromatch star, globstar and brace suites) and the portednodefs.globsuite (448 tests) pass with the debug build; all 61 new assertions agree with brace-expansion + picomatch.**, anda**/**b/{a,b}**/a,**shapes) x 40 paths. For every pattern P, fixed(P) equals unfixed(P with each non-segment**spelled*), and the 1558 patterns with no non-segment**are unchanged; 21 results across 12 patterns changed, all spurious matches of a non-segment**.scan()is unaffected: it matches one/-separated component at a time, so a component never backtracks into another one.**from swallowing the**segments after it in match() #38996: that PR moves the start-of-segment check ahead of the collapse but keeps it expressed asglob_index <= glob_start, which is what this bug needs replaced. Whichever lands second rebases to a small delta; on top of glob: stop a non-segment**from swallowing the**segments after it in match() #38996 this PR is theis_segment_startrule plus the brace-backtracking tests.**whose segment boundary is brace syntax rather than/(a/{**,b},{foo/**,bar}/baz) is still matched as*. That is a pre-existing false negative and is tracked separately.Background
src/glob/matcher.rs, ported from theglob-matchcrate) is a backtracking matcher over the raw pattern bytes; braces are not expanded up front. Every*records awildcardresume point; a qualifying**also records aglobstarresume point, and a later failure resumes there to let the globstar eat one more segment and re-match everything after it.match_brace, which callsmatch_brace_branchper alternative; that pushes aBrace { open_brace_idx, branch_idx, close_brace_idx }frame and recursively runsglob_match_implstarting at the branch, over the rest of the whole pattern. The recursive call inherits the resume points, so a failure inside the branch can resume at a globstar that precedes the group and walk back through the pattern prefix while the branch's frame is still on the stack.**that is a whole path segment; any other**(a**,**b) is documented to behave like*(thea**c,foo**bar,foo**cases inmatch.test.ts).skip_globstarsis the optimization that folds**/**/**into one globstar so the matcher does not backtrack through equivalent segments.