https://bugs.gentoo.org/984771 https://www.ruby-lang.org/en/news/2026/10/23/regexp-fixes-for-gitlab-cves/ https://github.com/ruby/ruby/pull/13332 https://github.com/ruby/ruby/pull/19020 https://github.com/ruby/ruby/pull/16801 From efe9ef18db005d102143098c7400242332c54dbc Mon Sep 17 00:00:00 2001 From: Hiroya Fujinami Date: Fri, 16 May 2025 10:14:26 +0900 Subject: [PATCH 1/4] Prevent double free for too big repetition quantifiers (#13332) Prevent double free for too big repetition quantifiers The previous implementation calls `free(node)` twice (on parsing and compiling a regexp) when it has an error, so it leads to a double-free issue. This commit enforces `free(node)` once by introducing a temporal pointer to hold parsing nodes. Cherry-picked to 3.2 with 8cd295ecf09e9419de0e3f3d4eecabfb430f9df1 included --- a/regparse.c +++ b/regparse.c @@ -6536,7 +6536,7 @@ parse_subexp(Node** top, OnigToken* tok, int term, UChar** src, UChar* end, ScanEnv* env) { int r; - Node *node, **headp; + Node *node, *topnode, **headp; *top = NULL; env->parse_depth++; @@ -6552,23 +6552,29 @@ parse_subexp(Node** top, OnigToken* tok, int term, *top = node; } else if (r == TK_ALT) { - *top = onig_node_new_alt(node, NULL); - headp = &(NCDR(*top)); + topnode = onig_node_new_alt(node, NULL); + headp = &(NCDR(topnode)); while (r == TK_ALT) { r = fetch_token(tok, src, end, env); - if (r < 0) return r; + if (r < 0) { + onig_node_free(topnode); + return r; + } r = parse_branch(&node, tok, term, src, end, env); if (r < 0) { - onig_node_free(node); + onig_node_free(topnode); return r; } *headp = onig_node_new_alt(node, NULL); - headp = &(NCDR(*headp)); + headp = &(NCDR(*headp)); } - if (tok->type != (enum TokenSyms )term) + if (tok->type != (enum TokenSyms )term) { + onig_node_free(topnode); goto err; + } + *top = topnode; } else { onig_node_free(node); --- a/test/ruby/test_regexp.rb +++ b/test/ruby/test_regexp.rb @@ -1643,6 +1643,12 @@ def test_invalid_group end; end + def test_too_big_number_for_repeat_range + assert_raise_with_message(SyntaxError, /too big number for repeat range/) do + eval(%[/|{1000000}/]) + end + end + # This assertion is for porting x2() tests in testpy.py of Onigmo. def assert_match_at(re, str, positions, msg = nil) re = Regexp.new(re) unless re.is_a?(Regexp) -- 2.56.0 From 3ca1d660a031ae86f758c9b7606639dfd70a630d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 25 Sep 2026 11:44:12 +0900 Subject: [PATCH 2/4] Avoid overflow in the quantifier expansion size check Nested bounded repeats let the int product tlen * qn->lower overflow, so the check that keeps the compiler from unrolling a large repeat could pass for an enormous one, hanging the compile or emitting wrapped jump offsets. Compare against the limit by division instead. Co-Authored-By: Claude Opus 5.5 --- a/regcomp.c +++ b/regcomp.c @@ -740,6 +740,10 @@ is_anychar_star_quantifier(QtfrNode* qn) } #define QUANTIFIER_EXPAND_LIMIT_SIZE 50 +/* (tlen * n <= QUANTIFIER_EXPAND_LIMIT_SIZE) without overflowing int: tlen + and n are each bounded by ONIG_MAX_REPEAT_NUM, but their product is not. */ +#define IS_EXPAND_LIMIT_OK(tlen, n) \ + ((n) <= 0 || (tlen) <= QUANTIFIER_EXPAND_LIMIT_SIZE / (n)) #define CKN_ON (ckn > 0) #ifdef USE_COMBINATION_EXPLOSION_CHECK @@ -1004,7 +1008,7 @@ compile_length_quantifier_node(QtfrNode* qn, regex_t* reg) mod_tlen = tlen; if (infinite && - (qn->lower <= 1 || tlen * qn->lower <= QUANTIFIER_EXPAND_LIMIT_SIZE)) { + (qn->lower <= 1 || IS_EXPAND_LIMIT_OK(tlen, qn->lower))) { if (qn->lower == 1 && tlen > QUANTIFIER_EXPAND_LIMIT_SIZE) { len = SIZE_OP_JUMP; } @@ -1030,8 +1034,8 @@ compile_length_quantifier_node(QtfrNode* qn, regex_t* reg) len = SIZE_OP_JUMP + tlen; } else if (!infinite && qn->greedy && - (qn->upper == 1 || (tlen + SIZE_OP_PUSH) * qn->upper - <= QUANTIFIER_EXPAND_LIMIT_SIZE)) { + (qn->upper == 1 || + IS_EXPAND_LIMIT_OK(tlen + SIZE_OP_PUSH, qn->upper))) { len = tlen * qn->lower; len += (SIZE_OP_PUSH + tlen) * (qn->upper - qn->lower); } @@ -1081,7 +1085,7 @@ compile_quantifier_node(QtfrNode* qn, regex_t* reg) mod_tlen = tlen; if (infinite && - (qn->lower <= 1 || tlen * qn->lower <= QUANTIFIER_EXPAND_LIMIT_SIZE)) { + (qn->lower <= 1 || IS_EXPAND_LIMIT_OK(tlen, qn->lower))) { if (qn->lower == 1 && tlen > QUANTIFIER_EXPAND_LIMIT_SIZE) { if (qn->greedy) { #ifdef USE_OP_PUSH_OR_JUMP_EXACT @@ -1151,8 +1155,8 @@ compile_quantifier_node(QtfrNode* qn, regex_t* reg) r = compile_tree(qn->target, reg); } else if (!infinite && qn->greedy && - (qn->upper == 1 || (tlen + SIZE_OP_PUSH) * qn->upper - <= QUANTIFIER_EXPAND_LIMIT_SIZE)) { + (qn->upper == 1 || + IS_EXPAND_LIMIT_OK(tlen + SIZE_OP_PUSH, qn->upper))) { int n = qn->upper - qn->lower; r = compile_tree_n_times(qn->target, qn->lower, reg); --- a/test/ruby/test_regexp.rb +++ b/test/ruby/test_regexp.rb @@ -1844,4 +1844,16 @@ def test_linear_time_p assert_raise(TypeError) {Regexp.linear_time?(nil)} assert_raise(TypeError) {Regexp.linear_time?(Regexp.allocate)} end + + def test_nested_repeat_expansion_overflow + assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}", timeout: 30) + begin; + # A nested repeat whose expanded size overflows int must not be + # unrolled: the compiler used to hang or emit wrapped jump offsets. + ["(?:.{90000,}){90000}", "(?:.{46341,}){46341}"].each do |src| + re = Regexp.new(src) + assert_nil(re.match("x" * 10), src) + end + end; + end end -- 2.56.0 From a3724c56e436a730b95f930acba46c010230dc92 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 25 Sep 2026 12:10:25 +0900 Subject: [PATCH 3/4] Reject a compiled program too big for its offset type Concatenated large repeats reach a program past INT_MAX from a few hundred kilobytes of source, and the offsets emitted into it wrap. Cap the size while the program is emitted, which does not depend on the length pass that overflows along with it. Co-Authored-By: Claude Opus 5 --- a/include/ruby/onigmo.h +++ b/include/ruby/onigmo.h @@ -686,6 +686,7 @@ ONIG_EXTERN const OnigSyntaxType* OnigDefaultSyntax; #define ONIGERR_NEVER_ENDING_RECURSION -221 #define ONIGERR_GROUP_NUMBER_OVER_FOR_CAPTURE_HISTORY -222 #define ONIGERR_INVALID_CHAR_PROPERTY_NAME -223 +#define ONIGERR_TOO_BIG_COMPILED_PROGRAM -226 #define ONIGERR_INVALID_CODE_POINT_VALUE -400 #define ONIGERR_INVALID_WIDE_CHAR_VALUE -400 #define ONIGERR_TOO_BIG_WIDE_CHAR_VALUE -401 --- a/regcomp.c +++ b/regcomp.c @@ -224,6 +224,10 @@ unset_addr_list_add(UnsetAddrList* uslist, int offset, struct _Node* node) static int add_opcode(regex_t* reg, int opcode) { + /* Every instruction passes here, so this is where a program that would + outgrow its offset type is stopped, while it is still being emitted. */ + if (reg->used > MAX_COMPILED_PROGRAM_SIZE) + return ONIGERR_TOO_BIG_COMPILED_PROGRAM; BBUF_ADD1(reg, opcode); return 0; } --- a/regerror.c +++ b/regerror.c @@ -165,6 +165,8 @@ onig_error_code_to_format(OnigPosition code) p = "group number is too big for capture history"; break; case ONIGERR_INVALID_CHAR_PROPERTY_NAME: p = "invalid character property name {%n}"; break; + case ONIGERR_TOO_BIG_COMPILED_PROGRAM: + p = "too big compiled program"; break; case ONIGERR_TOO_MANY_CAPTURE_GROUPS: p = "too many capture groups are specified"; break; case ONIGERR_INVALID_CODE_POINT_VALUE: --- a/regint.h +++ b/regint.h @@ -396,6 +396,11 @@ typedef unsigned int BitStatusType; #define INT_MAX_LIMIT ((1UL << (SIZEOF_INT * 8 - 1)) - 1) #define LONG_MAX_LIMIT ((1UL << (SIZEOF_LONG * 8 - 1)) - 1) +/* Program offsets are RelAddrType, and the compile_length_*() functions sum + into int, so a larger program would wrap both. The quarter leaves room + for the sums the offset arithmetic makes out of those lengths. */ +#define MAX_COMPILED_PROGRAM_SIZE ((unsigned int )(INT_MAX_LIMIT / 4)) + #define DIGITVAL(code) ((code) - '0') #define ODIGITVAL(code) DIGITVAL(code) #define XDIGITVAL(enc,code) \ -- 2.56.0 From 98096298311569a4b58e48d97081456296ae1d32 Mon Sep 17 00:00:00 2001 From: Hiroya Fujinami Date: Wed, 29 Apr 2026 11:09:24 +0900 Subject: [PATCH 4/4] Reject overflowed repeat/null-check IDs in huge regexps (#16801) Add explicit errors for excessive range-repeat and null-check IDs; map new errors in regerror; add regression tests for oversized generated patterns. --- a/include/ruby/onigmo.h +++ b/include/ruby/onigmo.h @@ -686,6 +686,8 @@ ONIG_EXTERN const OnigSyntaxType* OnigDefaultSyntax; #define ONIGERR_NEVER_ENDING_RECURSION -221 #define ONIGERR_GROUP_NUMBER_OVER_FOR_CAPTURE_HISTORY -222 #define ONIGERR_INVALID_CHAR_PROPERTY_NAME -223 +#define ONIGERR_TOO_MANY_RANGE_REPEAT -224 +#define ONIGERR_TOO_MANY_NULL_CHECK -225 #define ONIGERR_TOO_BIG_COMPILED_PROGRAM -226 #define ONIGERR_INVALID_CODE_POINT_VALUE -400 #define ONIGERR_INVALID_WIDE_CHAR_VALUE -400 --- a/regcomp.c +++ b/regcomp.c @@ -399,6 +399,7 @@ compile_tree_empty_check(Node* node, regex_t* reg, int empty_info) r = add_mem_num(reg, reg->num_null_check); /* NULL CHECK ID */ if (r) return r; reg->num_null_check++; + if ((MemNumType)reg->num_null_check <= 0) return ONIGERR_TOO_MANY_NULL_CHECK; } r = compile_tree(node, reg); @@ -708,6 +709,7 @@ compile_range_repeat_node(QtfrNode* qn, int target_len, int empty_info, if (r) return r; r = add_mem_num(reg, num_repeat); /* OP_REPEAT ID */ reg->num_repeat++; + if ((MemNumType)reg->num_repeat <= 0) return ONIGERR_TOO_MANY_RANGE_REPEAT; if (r) return r; r = add_rel_addr(reg, target_len + SIZE_OP_REPEAT_INC); if (r) return r; --- a/regerror.c +++ b/regerror.c @@ -165,6 +165,10 @@ onig_error_code_to_format(OnigPosition code) p = "group number is too big for capture history"; break; case ONIGERR_INVALID_CHAR_PROPERTY_NAME: p = "invalid character property name {%n}"; break; + case ONIGERR_TOO_MANY_RANGE_REPEAT: + p = "too many range repeat"; break; + case ONIGERR_TOO_MANY_NULL_CHECK: + p = "too many null check"; break; case ONIGERR_TOO_BIG_COMPILED_PROGRAM: p = "too big compiled program"; break; case ONIGERR_TOO_MANY_CAPTURE_GROUPS: --- a/test/ruby/test_regexp.rb +++ b/test/ruby/test_regexp.rb @@ -1856,4 +1856,16 @@ def test_nested_repeat_expansion_overflow end end; end + + def test_too_many_range_repeat + source = '(?:foobar){0,100}' * 100000 + assert_raise(RegexpError) { Regexp.new(source) } + assert_raise(SyntaxError) { eval("/#{source}/") } + end + + def test_too_many_null_check + source = '(?:(?:foo)?|(?:bar)?)*' * 100000 + assert_raise(RegexpError) { Regexp.new(source) } + assert_raise(SyntaxError) { eval("/#{source}/") } + end end -- 2.56.0