Skip to content

mruby-regexp: reject an empty group name - #7021

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-empty-group-name
Aug 9, 2026
Merged

mruby-regexp: reject an empty group name#7021
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-empty-group-name

Conversation

@takumin

@takumin takumin commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

compile_atom() accepts (?<> as a named capture whose name is zero bytes long.
CRuby rejects an empty group name at compile time; mruby registers it, and the
group is then reachable through every named capture API under the name "".

re = Regexp.new("(?<>x)")   # CRuby: RegexpError (group name is empty)
re.named_captures           # mruby: {"" => 1}
re.match("x")[""]           # mruby: "x"

The backreference form goes the same way once such a group exists:

Regexp.new("(?<>x)\k<>").match("xx")   # mruby: matches   CRuby: RegexpError

\k<> on its own raises in mruby, but for the wrong reason: it resolves to no
group and falls out of the range check as undefined group name reference. It
stops raising as soon as the pattern has an empty named group to resolve to,
which is the case above.

Cause

The scan in the (?<name> branch stops immediately when the next byte is >,
cap_name_len is 0, and nothing tests it. cap_name is non-NULL, so the
registration block stores the entry like any other. The \k<name> branch scans
the same way.

The arena guard leaves a dangling pointer

mrb_re_compile() copies capture names into an arena the regexp owns, because
until that point they point into the pattern source, which in /x mode is a
buffer the function frees before returning. The copy is guarded on the total
length of all names, so for a pattern whose named groups are all empty named,
total is 0, the arena is never allocated, and every
pat->named_captures[i].name keeps pointing at c.stripped:

Regexp.new("(?<>x) ", Regexp::EXTENDED).named_captures   # mruby: {"" => 1}

Nothing dereferences it today, because every reader passes the matching
name_len of 0 to mrb_str_new() or memcmp(), so this is a latent condition
rather than a reproducible fault. Rejecting the empty name in the parser closes
it: total is now zero only when there are no named captures at all, which is
what the guard's comment says.

Change

Two checks, one in each branch, using CRuby's group name is empty message:

$ ./build/host/bin/mruby -e 'Regexp.new("(?<>x)")'
-e:0: group name is empty: /(?<>x)/ (RegexpError)

(?<=...) and (?<!...) are taken by the lookbehind branch, which tests
c->p[2], so neither reaches the new check.

Testing

rake test passes: 1943 tests, 0 failures, 0 crashes. Behaviour checked against
CRuby 4.0.6.

Summary by CodeRabbit

  • Bug Fixes

    • Invalid regular expressions with empty named capture-group names or named backreferences now correctly raise RegexpError.
    • Extended-mode patterns now apply the same validation consistently.
    • Existing lookbehind behavior remains unchanged.
  • Tests

    • Added regression coverage for invalid empty names and valid lookbehind expressions.

`compile_atom()` accepted `(?<>` as a named capture whose name is zero
bytes long. mruby registered it and the group became reachable through
every named capture API under the name `""`.

```ruby
re = Regexp.new("(?<>x)")   # CRuby: RegexpError (group name is empty)
re.named_captures           # mruby: {"" => 1}
re.match("x")[""]           # mruby: "x"
```

The backreference form followed once such a group existed:

```ruby
Regexp.new("(?<>x)\k<>").match("xx")   # mruby: matches   CRuby: RegexpError
```

`\k<>` on its own already raised, but as `undefined group name reference`
rather than for the empty name, and it stopped raising as soon as the
pattern had an empty named group to resolve to.

Reject the empty name where each is parsed, with CRuby's message.

This also closes a latent pointer problem below it. `mrb_re_compile()`
copies capture names into an arena the regexp owns, guarded on the total
length of all names. For a pattern whose named groups were all empty
named that total was 0, the arena was never allocated, and every
`named_captures[i].name` kept pointing into `c.stripped`, which the
function frees before returning:

```ruby
Regexp.new("(?<>x) ", Regexp::EXTENDED).named_captures   # mruby: {"" => 1}
```

Nothing dereferenced it, since every reader passed the matching
`name_len` of 0, so it was a latent condition rather than a fault. With
no empty name reaching registration, the guard is zero only when there
are no named captures at all, which is what its comment says.
@takumin
takumin requested a review from matz as a code owner August 9, 2026 04:59
@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: 327bd592-8b31-4c97-b2e0-2d69234ec3e4

📥 Commits

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

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

📝 Walkthrough

Walkthrough

Regexp compilation now raises RegexpError for empty named capture-group and named backreference names. Regression tests cover extended-mode preprocessing and preserve valid lookbehind behavior.

Changes

Regexp validation

Layer / File(s) Summary
Empty named-reference rejection
mrbgems/mruby-regexp/src/re_compile.c, mrbgems/mruby-regexp/test/regexp.rb
Named capture and backreference parsing reject empty names. Tests verify RegexpError, extended-mode cases, and valid lookbehind patterns.

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

Possibly related PRs

  • mruby/mruby#7007: Both changes update named capture and backreference name parsing and add related regression tests.

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 primary change: rejecting empty group names in mruby-regexp.
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