Skip to content

mruby-regexp: relocate the lookaround offsets with the code they name - #7213

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:lookaround-offset-relocation
Aug 17, 2026
Merged

mruby-regexp: relocate the lookaround offsets with the code they name#7213
matz merged 1 commit into
mruby:masterfrom
takumin:lookaround-offset-relocation

Conversation

@takumin

@takumin takumin commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

enum re_opcode gives four opcodes an absolute code index in offset: the lookarounds hold the end of their sub-pattern there, the same way RE_JMP, RE_SPLIT and RE_SPLITNG hold a jump target. Two places in re_compile.c move compiled code and have to carry those indices along, and each carried its own list of which opcodes to fix. Both lists named the three jumps and stopped.

The defect

A lookaround that goes through either relocator keeps an index into where its sub-pattern used to be. The stale index lands on the sub-pattern's own RE_MATCH, which ends the outer match at that point, so the pattern answers about a match it never made.

Three shapes reach a relocator:

  • * or ? around a group, where insert_inst() puts a SPLIT in front of it and shifts the rest down
  • {n,m}, where emit_atom_copy() copies the group and re-points the copy at itself
  • alternation, where compile_alt() inserts a SPLIT at the first branch once every branch is compiled

The answers flip in both directions, and a MatchData survives the failure looking successful: the match reports a position while m[0] is nil. Each line is begin(0) and m[0], against CRuby 4.0.6:

/(?:(?=a)b)*x/.match("a")    # CRuby: nil      mruby: 0, nil
/(?:(?=a)a){2}/.match("aa")  # CRuby: 0, "aa"  mruby: nil
/(?=a)a|z/.match("ax")       # CRuby: 0, "a"   mruby: 0, nil
/(?:(?!b)b)*x/.match("ax")   # CRuby: 1, "x"   mruby: 0, nil
/(?:(?=a)ab)+/.match("ab")   # CRuby: 0, "ab"  mruby: 0, "ab"

The last line is the same group with nothing to relocate, and it has always agreed.

The fix

Ask one function, op_holds_code_index(), which opcodes hold a code index, rather than keeping the question answered separately in each relocator. The two lists disagreeing with the opcode set is the defect, and a list per relocator invites it back the next time an opcode gains an index.

RE_LB_WIDTH stays out, since it carries a character count in a, as do RE_SAVE and RE_BACKREF, whose offset is a slot number and a case-fold flag.

The ambiguity insert_inst() already resolves for a target equal to the insertion point needs no new arm. A lookaround's sub-pattern ends ahead of it, so it is always the forward-reference case, and leaving the end where it is puts the inserted instruction after the sub-pattern rather than inside it, which is what the quantifier wrapping the whole group is asking for.

Tests

mrbgems/mruby-regexp/test/regexp_syntax.rb gains one assertion block covering the three relocation shapes, one case each, plus the group with nothing to relocate as the control.

build config total OK KO crash skip
host build_config/default.rb 2103 2054 0 0 49
host bintest build_config/default.rb 106 106 0 0 0
full-debug build_config/ci/gcc-clang.rb 2327 2324 0 0 3
bintest build_config/ci/gcc-clang.rb 2327 2316 0 0 11
bintest bintest build_config/ci/gcc-clang.rb 117 117 0 0 0
cxx_abi build_config/ci/gcc-clang.rb 2327 2316 0 0 11
byte-string build_config/ci/gcc-clang.rb 2257 2208 0 0 49
ascii-case build_config/ci/gcc-clang.rb 2323 2310 0 0 13
asan build_config/asan.rb 2327 2324 0 0 3
asan bintest build_config/asan.rb 79 79 0 0 0

The asan build reports no AddressSanitizer or UBSan diagnostic.

Environment

Details
OS Ubuntu 24.04.4 LTS
Kernel Linux 7.0.0-28-generic x86_64
CPU AMD Ryzen 9 5950X 16-Core Processor
C compiler gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
C compiler (asan) Homebrew clang version 22.1.8
binutils GNU ld (GNU Binutils) 2.47.20260726
CRuby ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [x86_64-linux]

Compile lines for mrbgems/mruby-regexp/src/re_compile.c, taken from rake --verbose with -MMD -c, -I and -o removed. full-debug and asan are -O0, since enable_debug() appends -g3 -O0 after the toolchain default of -g -O3.

# host (build_config/default.rb)
gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRBGEM_MRUBY_REGEXP_VERSION=0.0.0 -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DMRB_USE_COMPLEX -DMRB_USE_BIGINT -DMRB_USE_DEBUG_HOOK

# full-debug (build_config/ci/gcc-clang.rb)
gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -g3 -O0 -DMRB_GC_STRESS -DMRB_USE_DEBUG_HOOK -DMRBGEM_MRUBY_REGEXP_VERSION=0.0.0 -DMRB_DEBUG -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER

# bintest (build_config/ci/gcc-clang.rb)
gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRB_GC_FIXED_ARENA -DMRBGEM_MRUBY_REGEXP_VERSION=0.0.0 -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER -DMRB_USE_DEBUG_HOOK

# cxx_abi (build_config/ci/gcc-clang.rb), compiled by gcc -x c++, g++ only links
gcc -g -O3 -Wall -Wundef -Wwrite-strings -x c++ -std=gnu++03 -DMRB_GC_FIXED_ARENA -DMRBGEM_MRUBY_REGEXP_VERSION=0.0.0 -DMRB_USE_CXX_EXCEPTION -DMRB_USE_CXX_ABI -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER

# byte-string (build_config/ci/gcc-clang.rb)
gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRBGEM_MRUBY_REGEXP_VERSION=0.0.0 -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER

# ascii-case (build_config/ci/gcc-clang.rb)
gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRB_USE_ASCII_CASE -DMRBGEM_MRUBY_REGEXP_VERSION=0.0.0 -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER

# asan (build_config/asan.rb)
clang -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -Wzero-length-array -fsanitize=address,undefined -g3 -O0 -DMRBGEM_MRUBY_REGEXP_VERSION=0.0.0 -DMRB_DEBUG -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER

Summary by CodeRabbit

  • Bug Fixes

    • Improved regular expression handling for lookaround patterns, alternations, and repeated groups.
    • Fixed cases where inserting or copying pattern instructions could produce incorrect matches or match boundaries.
  • Tests

    • Added regression coverage for lookaround endpoint preservation and related matching scenarios.

`enum re_opcode` gives four opcodes an absolute code index in `offset`: the
lookarounds hold the end of their sub-pattern there, the same way `RE_JMP`,
`RE_SPLIT` and `RE_SPLITNG` hold a jump target. Both relocators carried
their own list of which opcodes to fix, and both lists named the three jumps
and stopped: `insert_inst()` shifting code down to make room, and
`emit_atom_copy()` re-pointing a copied atom at itself.

A lookaround that went through either one kept an index into where its
sub-pattern used to be. The stale index lands on the sub-pattern's own
`RE_MATCH`, which ends the outer match at that point, so the pattern answers
about a match it never made. Three shapes reach a relocator: `*` or `?`
around a group, which inserts a `SPLIT` in front of it; `{n,m}`, which
copies it; and alternation, where `compile_alt()` inserts a `SPLIT` at the
first branch once every branch is compiled.

The answers flip in both directions, and a `MatchData` survives the failure
looking successful. Each line below is `begin(0)` and `m[0]`, against CRuby
4.0.6:

```ruby
/(?:(?=a)b)*x/.match("a")    # CRuby: nil      mruby: 0, nil
/(?:(?=a)a){2}/.match("aa")  # CRuby: 0, "aa"  mruby: nil
/(?=a)a|z/.match("ax")       # CRuby: 0, "a"   mruby: 0, nil
/(?:(?!b)b)*x/.match("ax")   # CRuby: 1, "x"   mruby: 0, nil
/(?:(?=a)ab)+/.match("ab")   # CRuby: 0, "ab"  mruby: 0, "ab"
```

The last one is the same group with nothing to relocate, and it has always
agreed.

Ask one function which opcodes hold a code index, rather than keeping the
question answered separately in each relocator: the two lists disagreeing
with the opcode set is the defect, and a list per relocator invites it back
the next time an opcode gains an index. `RE_LB_WIDTH` stays out, since it
carries a character count in `a`, as do `RE_SAVE` and `RE_BACKREF`, whose
`offset` is a slot number and a case-fold flag.

The ambiguity `insert_inst()` already resolves for a target equal to the
insertion point needs no new arm. A lookaround's sub-pattern ends ahead of
it, so it is always the forward-reference case, and leaving the end where it
is puts the inserted instruction after the sub-pattern rather than inside
it, which is what the quantifier wrapping the whole group is asking for.
@takumin
takumin requested a review from matz as a code owner August 17, 2026 00:14
@coderabbitai

coderabbitai Bot commented Aug 17, 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: 01662ebd-c31b-4178-a814-dbce22b31cc9

📥 Commits

Reviewing files that changed from the base of the PR and between bd41ddb and 40fab4b.

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

Included review availability: Your plan includes up to 8 reviews per rolling hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The regexp compiler centralizes relocation of instruction indices for jumps, splits, and lookaround endpoints. Atom copying uses the same relocation rules. Regression tests cover relocated lookaround patterns and match boundaries.

Changes

Regexp code-index relocation

Layer / File(s) Summary
Shared code-index relocation
mrbgems/mruby-regexp/src/re_compile.c
op_holds_code_index() identifies offsets that store instruction indices. Instruction insertion relocates jump, split, and lookaround targets.
Atom-copy relocation and regression coverage
mrbgems/mruby-regexp/src/re_compile.c, mrbgems/mruby-regexp/test/regexp_syntax.rb
Atom copying relocates all internal code indices. Tests cover quantifier, repetition-copy, alternation, and non-relocated lookaround patterns.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 40fab

This localized change corrects lookaround behavior during regexp code relocation and adds targeted coverage; the reported checks pass, so no actionable merge-blocking risk remains.

Possibly related PRs

  • mruby/mruby#7064: Both changes modify instruction insertion and jump-target relocation in re_compile.c, but address different relocation constraints.

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: relocating lookaround offsets with the code they reference.
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