mruby-regexp: free the compile buffers before the message is built - #7229
mruby-regexp: free the compile buffers before the message is built#7229takumin wants to merge 1 commit into
Conversation
`compile_error()` builds the message string first and frees the four
compile buffers afterwards. The comment above the frees is written for
the longjmp out of `mrb_exc_raise`, but `mrb_format` longjmps too: a
failing allocation raises `mrb->nomem_err`, and on `MRB_GC_FIXED_ARENA`
builds an arena overflow leaves the same way. Neither reaches the frees,
and `re_compiler` is a stack local of `mrb_re_compile()`, so `code`,
`classes`, `named_captures` and `stripped` are lost with it.
The message quotes the pattern in full, so a long pattern makes that
allocation large. A short one fits in an embedded string and never asks
the allocator at all, which is why the window only opens on long
patterns.
Move the three allocating calls after the frees. `emsg` reads only
`c->orig` and `c->orig_end`, which `mrb_re_compile()` sets from the
caller's pattern before `pattern` is swapped for `c.stripped`, so the
message still quotes a buffer that outlives the compile.
Failing every allocation from the Nth on, with
`Regexp.new("[a] (?<n>b) " + "z"*2000 + "(", Regexp::EXTENDED)` under
ASan and LeakSanitizer, this pattern's compile asks 14 times. The two
allocations inside `compile_error()`, #12 and #13, each lost 10541
bytes and now lose none. The other rows are unchanged.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan includes up to 8 reviews per rolling hour; 5 remain after this review. 📝 WalkthroughWalkthroughThe regexp compiler now releases compiler-owned buffers before constructing formatted compilation errors. Error formatting still uses the original, unpreprocessed pattern and its explicit length. ChangesRegexp compilation error cleanup
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to The change frees temporary regexp compile buffers before building allocation-failure messages, preventing the documented leak without changing message or exception behavior; no actionable merge-blocking risk remains after normal checks and review. Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
Closing this in favour of a structural fix. The leak this patch closes is one exit of a wider defect: nothing the compiler allocates is reachable from a GC object while the compile runs, so every raising call inside The replacement removes the class rather than the instance. |
compile_error()builds the message string first and frees the four compile buffers afterwards:The comment names the longjmp out of
mrb_exc_raise, and misses thatmrb_formatleaves the same way. A failing allocation raisesmrb->nomem_err, which is itself anmrb_exc_raise, so the fourmrb_freecalls below are never reached.re_compileris a stack local ofmrb_re_compile()and there is nothing above it that owns those buffers, socode,classes,named_capturesandstrippedgo with it. On aMRB_GC_FIXED_ARENAbuild, whichbuild_config/ci/gcc-clang.rbandbuild_config/ci/msvc.rbboth use, an arena overflow ingc_arena_keep()opens the same exit.The message quotes the pattern in full, so the size of that allocation is the size of the pattern. A short pattern makes a message that fits in an embedded string and never asks the allocator at all; the window belongs to long patterns.
The fix
Move the three allocating calls,
mrb_format,mrb_exc_get_idandmrb_exc_new_str, below the frees.emsgreads onlyc->origandc->orig_end, whichmrb_re_compile()sets from the caller'spatternbefore it swapspatternforc.stripped, soc->orignever points into a buffer being freed here and the message still quotes the pattern as written.Measurement
A driver that redefines
mrb_basic_alloc_functo fail every allocation from the Nth on, counting only the allocations made inside aRegexp.newcall, runningunder ASan and LeakSanitizer. This pattern's compile asks 14 times. The
sizecolumn is the allocation that was refused; the twoleakedcolumns are what LeakSanitizer reports at exit.#1#2#3#4#5#6#7#8#9#10#11#12#13#14#12and#13are the two allocationsmrb_formatmakes, the buffer growth and then the 2049-byte message itself (unmatched '(': /plus the 2013-byte preprocessed pattern plus/). The 10541 bytes they lost are the compile buffers: 8192 ofcode, 320 of one class's ranges, 16 of the class table and 2013 ofstripped.#14allocates the exception object, after the frees in both orders, so it loses nothing either way.Rows
#4to#11are a failure part-way through the compile, and they are unchanged because they are a different defect:mrb_re_compile()has nothing that catches a longjmp out of the middle of a parse. That is not what this PR is about.The stack LeakSanitizer gives for the 320-byte object at
#13before the change:Nothing observable changes when the allocator answers, so there is no test to add: the message, its wording and the exception class are the same, and
rake -m teston the default configuration and onci/gcc-clang, whose six builds report no failure and no warning, says so.Environment
Versions
rake -m test, clang 22.1.8 for the sanitizer buildSanitizer build and driver
The measurement build is
full-corewithMRB_GC_FIXED_ARENA,enable_debugandenable_sanitizer "address":-MMD -c,-Iand-odropped:clang -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -Wzero-length-array -fsanitize=address -g3 -O0 -DMRB_GC_FIXED_ARENA -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 mrbgems/mruby-regexp/src/re_compile.cThe driver, linked against that
libmruby.awithclang -g -O0 -fsanitize=address. Its ownmrb_basic_alloc_funcreplaces the one insrc/allocf.c, and__count_onbounds the window so the counted allocations are the onesRegexp.newmakes rather than the parser's. It fails every allocation from the Nth on, not the Nth alone, becausemrb_realloc()runs the GC and asks once more before it gives up.Summary by CodeRabbit