Skip to content

mruby-regexp: bound the numeric \k backreference accumulator - #7019

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-backref-number-overflow
Aug 9, 2026
Merged

mruby-regexp: bound the numeric \k backreference accumulator#7019
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-backref-number-overflow

Conversation

@takumin

@takumin takumin commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

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.

Regexp.new("(a)\\k<4294967297>").match("aa")      # CRuby: RegexpError (too big number)
                                                  # mruby: ["aa", "a"]
Regexp.new("(a)\\k<-4294967297>").match("aa")     # CRuby: RegexpError
                                                  # mruby: ["aa", "a"]
Regexp.new("(a)(b)\\k<4294967298>").match("abb")  # CRuby: RegexpError
                                                  # mruby: ["abb", "a", "b"]

The range check is not bypassed; it is handed a number that is no longer the number in
the pattern. 4294967297 overflows on the tenth digit and wraps modulo 2^32 to 1,
which is in range for /(a)/, so the check passes and RE_BACKREF is emitted against
group 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 to
fall 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:

$ ./build/asan/bin/mruby -e 'p Regexp.new("(a)\\k<4294967297>").match("aa").to_a'
re_compile.c:823:17: runtime error: signed integer overflow: 429496729 * 10
  cannot be represented in type 'int'
["aa", "a"]

Fix

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 pattern that compiles today 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 (32), so n * 10 + 9 is at most 319.

Nothing else changes. re_exec.c is not involved: RE_BACKREF carries a resolved group
number in re_inst.a, so the matcher never sees the digits. The named branch resolves a
name through the capture table and does no arithmetic.

Test

Three cases in mrbgems/mruby-regexp/test/regexp.rb, next to the existing
Regexp - named backreference \k assertion. Each one binds to a group without this
change 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 == 1 and /(.)(.)\k<-1>\k<-2>/ reaches
n == c->num_captures - 1 == 2, which are exactly the two boundary values the new
comparison is written against.

Checked against CRuby 4.0.6. rake test passes, and the UBSan report above is gone on a
build_config/asan.rb build.

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

    • Invalid numeric regular-expression backreferences now raise RegexpError immediately instead of being interpreted as unintended capture groups.
    • Backreferences exceeding the supported capture-group range are handled consistently.
  • Tests

    • Added regression coverage for oversized numeric backreference values.

`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.
@takumin
takumin requested a review from matz as a code owner August 9, 2026 03:43
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2931709a-ed54-4d0b-9a75-101ef8fcb271

📥 Commits

Reviewing files that changed from the base of the PR and between fd6a182 and 0521da2.

📒 Files selected for processing (2)
  • mrbgems/mruby-regexp/src/re_compile.c
  • mrbgems/mruby-regexp/test/regexp.rb

📝 Walkthrough

Walkthrough

Numeric backreference parsing now rejects values beyond the defined capture-group range during digit accumulation. Regression tests cover positive and negative out-of-range \k references.

Changes

Numeric backreference validation

Layer / File(s) Summary
Backreference range check and regression coverage
mrbgems/mruby-regexp/src/re_compile.c, mrbgems/mruby-regexp/test/regexp.rb
The compiler raises RegexpError when a numeric backreference exceeds the available capture-group range. Tests cover positive and negative out-of-range values.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • mruby/mruby#7007: Both changes update numeric or named backreference parsing in re_compile.c to reject out-of-range values.

Suggested reviewers: matz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: bounding numeric \k backreference accumulation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

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

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