From 5bb4a15086adc0424bf9f2bce957a18cc2fa3d48 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Tue, 19 May 2026 10:26:35 +0900 Subject: [PATCH] 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 --- mrbgems/mruby-regexp/include/re_internal.h | 6 ++++++ mrbgems/mruby-regexp/src/re_exec.c | 20 +++++++++++--------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/mrbgems/mruby-regexp/include/re_internal.h b/mrbgems/mruby-regexp/include/re_internal.h index 07e022677..a03752b67 100644 --- a/mrbgems/mruby-regexp/include/re_internal.h +++ b/mrbgems/mruby-regexp/include/re_internal.h @@ -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 diff --git a/mrbgems/mruby-regexp/src/re_exec.c b/mrbgems/mruby-regexp/src/re_exec.c index 948f6d080..889c60c38 100644 --- a/mrbgems/mruby-regexp/src/re_exec.c +++ b/mrbgems/mruby-regexp/src/re_exec.c @@ -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);