Skip to content

mruby-regexp: do not truncate a POSIX bracket class name length - #7024

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-posix-class-name-truncation
Aug 9, 2026
Merged

mruby-regexp: do not truncate a POSIX bracket class name length#7024
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-posix-class-name-truncation

Conversation

@takumin

@takumin takumin commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

compile_charclass() passes the length of a POSIX bracket class name to
posix_class_bits() through a uint16_t cast. The cast is silent, so a name
whose true length is congruent to a valid name's length modulo 65536 is
accepted, and the pattern compiles as the class named by the first few bytes
rather than raising RegexpError.

name = "alpha" + "A" * 65536      # 65541 bytes, (uint16_t)65541 == 5
re = Regexp.new("[[:#{name}:]]")
# CRuby: RegexpError
# mruby: compiles, and behaves as /[[:alpha:]]/
re.match?("q")                    # mruby: true
re.match?("1")                    # mruby: false

The name that was written is not a POSIX bracket class name. mruby compares
only its first five bytes, finds "alpha", and silently accepts the other
65536. The negated form goes down the same branch:

name = "digit" + "A" * 65536
Regexp.new("[[:^#{name}:]]")      # mruby: compiles as /[[:^digit:]]/

Only lengths congruent to a valid name's length slip through. A name of
exactly 65536 bytes truncates to 0, no NAME_IS test matches a zero length,
and the pattern raises as it should.

Cause

name is set right after [: and any ^, and the scan runs to the first :
or ], so c->p - name is a ptrdiff_t over the pattern source with no upper
bound; nothing in mrb_re_compile() limits the pattern length. The length is
then narrowed at the call site:

if (!posix_class_bits(bits, name, (uint16_t)(c->p - name))) {
  compile_error(c, "invalid POSIX bracket class");
}

posix_class_bits() takes the length as uint16_t, and its NAME_IS macro is

#define NAME_IS(s) (len == sizeof(s) - 1 && memcmp(name, s, len) == 0)

so the truncated value decides both which literal can match and how many bytes
are compared. With the truncation, len == 5 and memcmp(name, "alpha", 5)
succeeds, posix_class_bits() returns TRUE, and the compile_error() never
fires. The : and ] are then consumed and the bits are merged into the
class, so the trailing filler bytes leave no trace in the compiled pattern.

This stays in bounds. memcmp() compares only the truncated number of bytes,
and the name lives in the pattern source, so nothing reads past an allocation.
The bug is a compatibility one: a pattern CRuby rejects compiles here, and it
compiles as something the pattern does not say.

CRuby 4.0.6 raises for both patterns above, with invalid POSIX bracket type
followed by the whole pattern. That is Onigmo's parse_posix_bracket(): on a
prefix hit it requires the next two bytes to be :], which they are not here.

Fix

Change posix_class_bits()'s len to size_t and drop the cast. NAME_IS
then compares the true length, every over-long name fails the
len == sizeof(s) - 1 test, posix_class_bits() falls through to
return FALSE, and the compile_error() already at the call site raises with
no new call site and the same message [[:bogus:]] produces. The
short-circuit in NAME_IS keeps memcmp() bounded by the literal, so the
body of the function needs no other edit.

Guarding the ptrdiff_t against UINT16_MAX before the cast would work too,
but removing the narrowing is preferable to making it loud, and it costs one
parameter type.

Tests

Two cases in assert("Regexp - POSIX bracket classes"), next to the existing
assert_raise(RegexpError) { Regexp.new("[[:bogus:]]") }, covering the plain
and negated over-long names. The block's existing assertions cover the
regression side: a valid name must keep compiling.

rake test passes on x86_64-linux with no new compiler warning.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed handling of unusually long POSIX character-class names in regular expressions.
    • Invalid oversized class names now correctly raise RegexpError instead of being silently truncated or compiled incorrectly.
    • Applies to both standard and negated character classes.
  • Tests

    • Added regression coverage for oversized POSIX character-class names.

`compile_charclass()` passed the length of a POSIX bracket class name to
`posix_class_bits()` through a `uint16_t` cast. The cast is silent, so a
name whose true length is congruent to a valid name's length modulo 65536
compared equal to that valid name, and the pattern compiled as the class
named by the first few bytes instead of raising.

```ruby
name = "alpha" + "A" * 65536      # 65541 bytes, (uint16_t)65541 == 5
re = Regexp.new("[[:#{name}:]]")  # CRuby: RegexpError
re.match?("q")                    # mruby: true
re.match?("1")                    # mruby: false
```

The negated form goes down the same branch:

```ruby
name = "digit" + "A" * 65536
Regexp.new("[[:^#{name}:]]")      # mruby: compiles as /[[:^digit:]]/
```

`NAME_IS` is `len == sizeof(s) - 1 && memcmp(name, s, len) == 0`, so the
truncated value decides both which literal can match and how many bytes
are compared. Nothing reads out of bounds: `memcmp()` stops at the
truncated length and the name lives in the pattern source. Only lengths
congruent to a valid name's length slip through; a name of exactly 65536
bytes truncates to 0, no `NAME_IS` test matches, and the pattern raises.

Take the length as `size_t` instead. Every over-long name then fails the
`len == sizeof(s) - 1` test, `posix_class_bits()` returns `FALSE`, and
the `compile_error()` already at the call site raises the same
`invalid POSIX bracket class` that `[[:bogus:]]` raises.
@takumin
takumin requested a review from matz as a code owner August 9, 2026 10:49
@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: 58d45d4b-8e5b-412c-a0eb-b5bc0ab682ec

📥 Commits

Reviewing files that changed from the base of the PR and between 4461d87 and 5634ac6.

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

📝 Walkthrough

Walkthrough

The regexp compiler now passes POSIX bracket-class name lengths as size_t. Regression tests verify that oversized positive and negated class names raise RegexpError.

Changes

POSIX class length handling

Layer / File(s) Summary
Preserve class-name lengths and test oversized inputs
mrbgems/mruby-regexp/src/re_compile.c, mrbgems/mruby-regexp/test/regexp.rb
The compiler uses size_t for POSIX class name lengths. Tests cover oversized positive and negated class names and expect RegexpError.

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

Possibly related PRs

  • mruby/mruby#7002: Addresses oversized-name truncation in MatchData#[] named-capture lookup.
  • mruby/mruby#7007: Updates regexp name-length handling and adds 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 main fix for POSIX bracket class name length truncation.
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