mruby-regexp: bound the numeric \k backreference accumulator - #7019
Merged
Conversation
`compile_atom()` parses the numeric form of `\k` by accumulating digits into
an `int` with no bound. A number too large for the type wraps, and the wrapped
value is what the range check below it sees, so a backreference to a group
that does not exist compiles as a backreference to one that does.
```ruby
re = Regexp.new("(a)\\k<4294967297>") # CRuby: RegexpError (too big number)
re.match("aa") # mruby: matches, \k<4294967297> is group 1
```
The relative form wraps through the same accumulator:
```ruby
Regexp.new("(a)\\k<-4294967297>").match("aa") # CRuby: RegexpError
# mruby: matches
```
Which group a given number lands on is a property of the wrap, not of the
pattern: `(a)(b)\k<4294967298>` binds to group 2 for the same reason, and
`\k<10000000000000000000000>` raises only because its wrapped value happens to
fall outside the range. The accumulation is also signed integer overflow,
undefined behaviour under C99 6.5/5, and a UBSan build reports it.
Bound the accumulator inside the loop against `c->num_captures - 1`, the same
quantity the range check below it uses. The bound is tight and correct for
both forms: an absolute reference needs `group < c->num_captures`, and a
relative one needs `c->num_captures - n >= 1`. It rejects nothing the range
check would have accepted, so it carries that check's message and no existing
pattern changes its error.
Testing after the addition rather than before it keeps the check to one line,
and `n` cannot overflow between two iterations: it is at most
`c->num_captures - 1` on entry, itself at most `RE_MAX_CAPTURES - 1`, so
`n * 10 + 9` is at most 319.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughNumeric backreference parsing now rejects values beyond the defined capture-group range during digit accumulation. Regression tests cover positive and negative out-of-range ChangesNumeric backreference validation
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This was referenced Aug 9, 2026
This was referenced Aug 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
compile_atom()parses the numeric form of\kby accumulating digits into anintwith no bound. A number too large for the type wraps, and the wrapped value is what the
range check below it sees, so a backreference to a group that does not exist compiles as
a backreference to one that does.
The range check is not bypassed; it is handed a number that is no longer the number in
the pattern.
4294967297overflows on the tenth digit and wraps modulo 2^32 to1,which is in range for
/(a)/, so the check passes andRE_BACKREFis emitted againstgroup 1. Which group a given number lands on is a property of the wrap, not of the
pattern:
\k<10000000000000000000000>raises only because its wrapped value happens tofall outside the range.
The accumulation is also signed integer overflow, undefined behaviour under C99 6.5/5
independently of the wrong answer it produces here:
Fix
Bound the accumulator inside the loop against
c->num_captures - 1, the same quantitythe range check below it uses. The bound is tight and correct for both forms: an
absolute reference needs
group < c->num_captures, and a relative one needsc->num_captures - n >= 1. It rejects nothing the range check would have accepted, soit carries that check's message and no pattern that compiles today changes its error.
Testing after the addition rather than before it keeps the check to one line, and
ncannot overflow between two iterations: it is at most
c->num_captures - 1on entry,itself at most
RE_MAX_CAPTURES - 1(32), son * 10 + 9is at most 319.Nothing else changes.
re_exec.cis not involved:RE_BACKREFcarries a resolved groupnumber in
re_inst.a, so the matcher never sees the digits. The named branch resolves aname through the capture table and does no arithmetic.
Test
Three cases in
mrbgems/mruby-regexp/test/regexp.rb, next to the existingRegexp - named backreference \kassertion. Each one binds to a group without thischange and raises with it, so each one fails on master.
The in-range direction is already pinned by that neighbouring assertion:
/(a)\k<1>/has
n == c->num_captures - 1 == 1and/(.)(.)\k<-1>\k<-2>/reachesn == c->num_captures - 1 == 2, which are exactly the two boundary values the newcomparison is written against.
Checked against CRuby 4.0.6.
rake testpasses, and the UBSan report above is gone on abuild_config/asan.rbbuild.CodeRabbit raised this overflow while reviewing #7007, where it is pre-existing rather
than introduced; it goes out on its own per CONTRIBUTING.md's one bugfix per pull
request.
Summary by CodeRabbit
Bug Fixes
RegexpErrorimmediately instead of being interpreted as unintended capture groups.Tests