Skip to content

mruby-regexp: fix a heap-buffer-overflow read in MatchData#[] for an over-long group name - #7002

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:matchdata-aref-name-length
Aug 3, 2026
Merged

mruby-regexp: fix a heap-buffer-overflow read in MatchData#[] for an over-long group name#7002
matz merged 1 commit into
mruby:masterfrom
takumin:matchdata-aref-name-length

Conversation

@takumin

@takumin takumin commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

MatchData#[] reads past the named-capture arena when it is given a group name
longer than 65535 bytes.

matchdata_aref() compares the requested name against each named capture by
testing the lengths first and then calling memcmp(). The length test truncates
the requested length to uint16_t, but the memcmp() next to it uses the
untruncated length, so a name whose length is congruent to a stored name's
length modulo 65536 passes the guard and then reads far past the end of the
stored name.

md = /(?<abc>x)/.match("x")
md["abc" + "A" * 65536]   # requested length 65539, (uint16_t)65539 == 3 == "abc".length

The stored name lives in the pattern's named-capture arena, which holds exactly
the three bytes abc here, so memcmp() is handed a 3-byte allocation and a
65539-byte length.

==1515268==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x72ecb07e26d3
READ of size 65539 at 0x72ecb07e26d3 thread T0
    #0 MemcmpInterceptorCommon
    #1 memcmp
    #2 matchdata_aref       mrbgems/mruby-regexp/src/regexp.c:598
    #3 mrb_vm_exec          src/vm.c:2947

0x72ecb07e26d3 is located 0 bytes after 3-byte region [0x72ecb07e26d0,0x72ecb07e26d3)
allocated by thread T0 here:
    #0 realloc
    #4 mrb_malloc           src/gc.c:354
    #5 mrb_re_compile       mrbgems/mruby-regexp/src/re_compile.c:1292
    #6 regexp_init          mrbgems/mruby-regexp/src/regexp.c:116

No C API is involved: the reproducer is two lines of ordinary Ruby, so this is a
VM crash from valid Ruby code rather than a compatibility gap. On a build without
a sanitizer it is invisible, because memcmp() stops at the first differing
byte; how far past the allocation it reads depends on how many bytes of
unrelated heap happen to match, and the arena is only as large as the sum of the
pattern's group names.

Cause

name_len is an mrb_int taken from RSTRING_LEN() or mrb_sym_name_len(),
while re_named_capture::name_len is a uint16_t
(mrbgems/mruby-regexp/include/re_internal.h:63). The cast makes the two
comparable, and the truncated value is then never used again:

if (pat->named_captures[i].name_len == (uint16_t)name_len &&
    memcmp(pat->named_captures[i].name, name, name_len) == 0) {

Fix

Reject a length that cannot name a group before the loop runs:

-    if (pat) {
+    if (pat && name_len <= UINT16_MAX) {

That is the whole change. A stored name can never be longer than UINT16_MAX,
so nothing that could have matched is excluded, and the truncating cast inside
the loop becomes lossless. A name this long resolves to no group either way, so
it takes the IndexError path that #7000 put in place, both before and after
this change.

UINT16_MAX comes from <stdint.h>, which re_internal.h already includes.
The change adds no VM call: name-to-group resolution stays entirely in C, which
is where CONTRIBUTING.md asks that an internal-representation lookup live.

Test

The overread cannot be observed from Ruby, so the test pins the path rather than
the result, and fails only on a sanitizer build. Added to
mrbgems/mruby-regexp/test/regexp.rb:

assert("MatchData#[] - group name longer than a uint16 length") do
  md = /(?<abc>x)/.match("x")
  assert_raise(IndexError) { md["abc" + "A" * 65536] }
  assert_equal "x", md[:abc]
end

Verified on c78448d5b with clang address,undefined
(MRUBY_CONFIG=build_config/asan.rb):

  • Without the guard, rake test aborts on it with the heap-buffer-overflow
    above, pointing at regexp.c:598.
  • With the guard and nothing else changed, the same command runs to the end:
    2103 tests, 0 failures, 0 crashes, plus 78 bintests.
  • The default host build is green as well: 1918 tests, 0 failures, 0 crashes.

Not the same bug: the compile-side truncation

re_compile.c truncates a group name's length to uint16_t in two places, at
capture registration (:668) and at \k<name> resolution (:814). Those two
truncate consistently and the arena is built from the truncated lengths
(:1292), so no read leaves its allocation. A (?<name>...) or \k<name> whose
name exceeds 65535 bytes can resolve to the wrong group, but nothing reads out of
bounds, and a pattern that long is not worth a guard of its own. Only the
MatchData#[] site mixes a truncated length with an untruncated one.

Relationship to #7000

Independent of #7000, which rewrote the same function and has since merged. The
two changes sat in separate hunks and this one applies unchanged on top. What
#7000 changed here is only the observable result for such a name: nil before
it, IndexError after, and the overread is gone from either point on. Kept
separate on purpose, since CONTRIBUTING.md asks that a pull request not mix
several bugfixes and a memory-safety fix should be reviewable on its own.

Summary by CodeRabbit

  • Bug Fixes
    • Improved regular-expression named capture lookups for unusually long names.
    • Invalid names exceeding the supported length now raise IndexError instead of being incorrectly matched.
    • Valid named capture lookups continue to work as expected.

`matchdata_aref()` compared the requested name length truncated to
`uint16_t` but passed the untruncated length to `memcmp()`, so a name
whose length is congruent to a stored name's length modulo 65536 passed
the length test and then read far past the end of the named-capture
arena.

A stored name can never be longer than `UINT16_MAX`, so rejecting such a
request before the loop excludes nothing that could have matched and
makes the cast inside the loop lossless.

The overread is invisible without a sanitizer, so the test pins the path
rather than the result: the lookup raises `IndexError` for this name both
before and after the guard.
@takumin
takumin requested a review from matz as a code owner August 3, 2026 05:57
@coderabbitai

coderabbitai Bot commented Aug 3, 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: 240336c1-2def-48a1-8291-8f21a823bd72

📥 Commits

Reviewing files that changed from the base of the PR and between c78448d and ad453c3.

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

📝 Walkthrough

Walkthrough

MatchData#[] now rejects named-capture lookup names longer than UINT16_MAX before comparison. Regression coverage verifies IndexError handling and valid named-capture access.

Changes

Named-capture lookup validation

Layer / File(s) Summary
Validate lookup name length
mrbgems/mruby-regexp/src/regexp.c, mrbgems/mruby-regexp/test/regexp.rb
The lookup skips capture matching when the name exceeds uint16_t capacity and raises IndexError. Tests cover oversized names and valid named-capture access.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

  • mruby/mruby#7000: Both changes update MatchData#[] named-capture lookup and regression tests, but address different validation cases.

Suggested reviewers: matz, nattzn

🚥 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 specifically describes the heap-buffer-overflow fix in MatchData#[] for over-long group names.
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