mruby-regexp: cap bt_match recursion depth

The backtracking engine recurses via C function calls at RE_SPLIT,
RE_SPLITNG, RE_SAVE, RE_LOOKAHEAD, RE_NEG_LOOKAHEAD, RE_LOOKBEHIND,
and RE_NEG_LOOKBEHIND.  Patterns like `(?=)+` make the engine
recurse without consuming input, exhausting the C stack and
triggering SIGSEGV long before MRB_REGEXP_STEP_LIMIT is reached
(each recursion charges only ~1 step, but each frame costs ~150
bytes of stack).

Reported by ClusterFuzz testcase
clusterfuzz-testcase-minimized-mruby_fuzzer-4653331195953152.

Add an integer recursion-depth counter passed alongside the step
counter, and abort the current branch with FALSE when it exceeds
MRB_REGEXP_RECURSION_LIMIT (default 1000, configurable like
STEP_LIMIT).  Legitimate patterns nest only a few levels;
pathological inputs bail without crashing the VM.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-05-19 10:26:35 +09:00
parent 3e676af568
commit 5bb4a15086
2 changed files with 17 additions and 9 deletions
@@ -97,6 +97,12 @@ typedef struct mrb_regexp_pattern {
#define MRB_REGEXP_STEP_LIMIT 1000000
#endif
/* Recursion-depth limit for bt_match: bounds C stack growth on
patterns like `(?=)+` that recurse without consuming input. */
#ifndef MRB_REGEXP_RECURSION_LIMIT
#define MRB_REGEXP_RECURSION_LIMIT 1000
#endif
/* Maximum captures */
#define RE_MAX_CAPTURES 32
+11 -9
View File
@@ -388,8 +388,10 @@ pike_vm(mrb_state *mrb, const mrb_regexp_pattern *pat,
*/
static mrb_bool
bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end,
const char *sp, uint32_t pc, int *captures, int ncap, int *steps)
const char *sp, uint32_t pc, int *captures, int ncap, int *steps,
int depth)
{
if (depth > MRB_REGEXP_RECURSION_LIMIT) return FALSE;
while (pc < pat->code_len) {
if (++(*steps) > MRB_REGEXP_STEP_LIMIT) return FALSE;
@@ -428,12 +430,12 @@ bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end,
break;
case RE_SPLIT:
if (bt_match(pat, str, str_end, sp, pc + 1, captures, ncap, steps)) return TRUE;
if (bt_match(pat, str, str_end, sp, pc + 1, captures, ncap, steps, depth + 1)) return TRUE;
pc = inst.offset;
break;
case RE_SPLITNG:
if (bt_match(pat, str, str_end, sp, inst.offset, captures, ncap, steps)) return TRUE;
if (bt_match(pat, str, str_end, sp, inst.offset, captures, ncap, steps, depth + 1)) return TRUE;
pc++;
break;
@@ -443,7 +445,7 @@ bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end,
if (slot < ncap) {
int old = captures[slot];
captures[slot] = (int)(sp - str);
if (bt_match(pat, str, str_end, sp, pc + 1, captures, ncap, steps)) return TRUE;
if (bt_match(pat, str, str_end, sp, pc + 1, captures, ncap, steps, depth + 1)) return TRUE;
captures[slot] = old;
}
return FALSE;
@@ -502,13 +504,13 @@ bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end,
break;
case RE_LOOKAHEAD:
if (!bt_match(pat, str, str_end, sp, pc + 1, captures, ncap, steps))
if (!bt_match(pat, str, str_end, sp, pc + 1, captures, ncap, steps, depth + 1))
return FALSE;
pc = inst.offset;
break;
case RE_NEG_LOOKAHEAD:
if (bt_match(pat, str, str_end, sp, pc + 1, captures, ncap, steps))
if (bt_match(pat, str, str_end, sp, pc + 1, captures, ncap, steps, depth + 1))
return FALSE;
pc = inst.offset;
break;
@@ -517,7 +519,7 @@ bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end,
{
int lb_len = inst.a;
if (sp - str < lb_len) return FALSE; /* not enough text before */
if (!bt_match(pat, str, str_end, sp - lb_len, pc + 1, captures, ncap, steps))
if (!bt_match(pat, str, str_end, sp - lb_len, pc + 1, captures, ncap, steps, depth + 1))
return FALSE;
pc = inst.offset;
}
@@ -527,7 +529,7 @@ bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end,
{
int lb_len = inst.a;
if (sp - str >= lb_len) {
if (bt_match(pat, str, str_end, sp - lb_len, pc + 1, captures, ncap, steps))
if (bt_match(pat, str, str_end, sp - lb_len, pc + 1, captures, ncap, steps, depth + 1))
return FALSE;
}
/* if not enough text before, negative lookbehind succeeds */
@@ -567,7 +569,7 @@ backtrack_exec(mrb_state *mrb, const mrb_regexp_pattern *pat,
memset(caps, -1, sizeof(int) * ncap);
int steps = 0;
if (bt_match(pat, str, str_end, sp, 0, caps, ncap, &steps)) {
if (bt_match(pat, str, str_end, sp, 0, caps, ncap, &steps, 0)) {
if (captures) {
int copy = ncap < captures_size ? ncap : captures_size;
memcpy(captures, caps, sizeof(int) * copy);