Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions configs/components/ruby-3.2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@

base = 'resources/patches/ruby_32'

# Backport the Ruby 3.3.13 Regexp compilation fixes for GitLab's
# CVE-2026-89078, CVE-2026-93577, CVE-2026-97229, and CVE-2026-110264.
# Keep this order: the size-limit error depends on the ID error codes,
# and the final two patches follow up earlier fixes.
pkg.apply_patch "#{base}/0001-regexp-avoid-alternation-double-free.patch"
pkg.apply_patch "#{base}/0002-regexp-reject-overflowed-repeat-ids.patch"
pkg.apply_patch "#{base}/0003-regexp-avoid-quantifier-size-overflow.patch"
pkg.apply_patch "#{base}/0004-regexp-limit-compiled-program.patch"
pkg.apply_patch "#{base}/0005-regexp-free-failed-alternation-branch.patch"
pkg.apply_patch "#{base}/0006-regexp-preserve-program-size-error.patch"

if platform.is_windows?
pkg.apply_patch "#{base}/windows_mingw32_mkmf.patch"
pkg.apply_patch "#{base}/ruby-faster-load_32.patch"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
From: Hiroya Fujinami <make.just.on@gmail.com>
Date: 2025-05-16T10:14:26+09:00
Subject: [PATCH 1/6] Prevent double free while parsing regexp alternations

Backport of Ruby 3.3 commit 63d0ba80a7d7fd348423fa450a3d766091d1f3a8
(ruby/ruby#13332, CVE-2026-89078).

---
diff --git a/regparse.c b/regparse.c
index 33df0e06c7..c2839899bb 100644
--- 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,14 +6552,17 @@ 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;
}

@@ -6567,8 +6570,11 @@ parse_subexp(Node** top, OnigToken* tok, int term,
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);
diff --git a/test/ruby/test_regexp.rb b/test/ruby/test_regexp.rb
index b385760b42..9c4ef98ffb 100644
--- 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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
From: Hiroya Fujinami <make.just.on@gmail.com>
Date: 2026-04-29T11:09:24+09:00
Subject: [PATCH 2/6] Reject overflowed repeat and null-check IDs in regexps

Backport of Ruby 3.3 commit 63eda2681e24cdf9fbe45fdd8a3d8866f4f59878
(ruby/ruby#16801, CVE-2026-110264). Place tests at the end of the
Ruby 3.2 test_regexp.rb suite.

---
diff --git a/include/ruby/onigmo.h b/include/ruby/onigmo.h
index 8d7c601703..ebeca9eb5c 100644
--- 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_INVALID_CODE_POINT_VALUE -400
#define ONIGERR_INVALID_WIDE_CHAR_VALUE -400
#define ONIGERR_TOO_BIG_WIDE_CHAR_VALUE -401
diff --git a/regcomp.c b/regcomp.c
index be85d85f93..231ccae8ce 100644
--- a/regcomp.c
+++ b/regcomp.c
@@ -395,6 +395,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);
@@ -704,6 +705,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;
diff --git a/regerror.c b/regerror.c
index b18fc2e88b..545d533824 100644
--- 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_MANY_CAPTURE_GROUPS:
p = "too many capture groups are specified"; break;
case ONIGERR_INVALID_CODE_POINT_VALUE:
diff --git a/test/ruby/test_regexp.rb b/test/ruby/test_regexp.rb
index 9c4ef98ffb..ebce6d90b8 100644
--- 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_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
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
From: Hiroshi SHIBATA <hsbt@ruby-lang.org>
Date: 2026-09-25T11:44:12+09:00
Subject: [PATCH 3/6] Avoid overflow in the quantifier expansion size check

Backport of Ruby 3.3 commit fa98ef030a54595d4387998b2fb48a412fc5c63c
(ruby/ruby#19020, CVE-2026-93577 and CVE-2026-97229).
Place the regression test at the end of the Ruby 3.2 regexp suite.

---
diff --git a/regcomp.c b/regcomp.c
index 231ccae8ce..bc1c1d0c88 100644
--- a/regcomp.c
+++ b/regcomp.c
@@ -742,6 +742,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
@@ -1006,7 +1010,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;
}
@@ -1032,8 +1036,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);
}
@@ -1083,7 +1087,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
@@ -1153,8 +1157,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);
diff --git a/test/ruby/test_regexp.rb b/test/ruby/test_regexp.rb
index ebce6d90b8..ea75b65804 100644
--- a/test/ruby/test_regexp.rb
+++ b/test/ruby/test_regexp.rb
@@ -1856,4 +1856,16 @@ def test_too_many_null_check
assert_raise(RegexpError) { Regexp.new(source) }
assert_raise(SyntaxError) { eval("/#{source}/") }
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
65 changes: 65 additions & 0 deletions resources/patches/ruby_32/0004-regexp-limit-compiled-program.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
From: Hiroshi SHIBATA <hsbt@ruby-lang.org>
Date: 2026-09-25T12:10:25+09:00
Subject: [PATCH 4/6] Reject compiled regexp programs too large for offsets

Backport of Ruby 3.3 commit b03e500193371ae87dc4536d855008b42a9cd1a7
(ruby/ruby#19020, CVE-2026-93577 and CVE-2026-97229).
Apply after 0002, which introduces the preceding error codes.

---
diff --git a/include/ruby/onigmo.h b/include/ruby/onigmo.h
index ebeca9eb5c..5adcf71b78 100644
--- a/include/ruby/onigmo.h
+++ b/include/ruby/onigmo.h
@@ -688,6 +688,7 @@ ONIG_EXTERN const OnigSyntaxType* OnigDefaultSyntax;
#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
#define ONIGERR_TOO_BIG_WIDE_CHAR_VALUE -401
diff --git a/regcomp.c b/regcomp.c
index bc1c1d0c88..9453101418 100644
--- 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;
}
diff --git a/regerror.c b/regerror.c
index 545d533824..cee59ff56b 100644
--- a/regerror.c
+++ b/regerror.c
@@ -169,6 +169,8 @@ onig_error_code_to_format(OnigPosition code)
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:
p = "too many capture groups are specified"; break;
case ONIGERR_INVALID_CODE_POINT_VALUE:
diff --git a/regint.h b/regint.h
index cee766ad6e..0655b1dc12 100644
--- 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) \
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
From: Sethu <s.c.arunachalam@lifelenz.com>
Date: 2026-09-25T14:40:12+09:30
Subject: [PATCH 5/6] Free unlinked branch on regexp alternation error

Backport of Ruby 3.3 commit 745e06b1b0e6ec2af5ecb4bc0ee0de5e8d641207
(Ruby bug #22384). Follow-up to the alternation fix in 0001;
free the incomplete branch as well as its parent on failure.

---
diff --git a/regparse.c b/regparse.c
index c2839899bb..59287880d2 100644
--- a/regparse.c
+++ b/regparse.c
@@ -6562,6 +6562,8 @@ parse_subexp(Node** top, OnigToken* tok, int term,
}
r = parse_branch(&node, tok, term, src, end, env);
if (r < 0) {
+ /* the failed branch is not linked into topnode yet: free both */
+ onig_node_free(node);
onig_node_free(topnode);
return r;
}
diff --git a/test/ruby/test_regexp.rb b/test/ruby/test_regexp.rb
index ea75b65804..8f8473fe03 100644
--- a/test/ruby/test_regexp.rb
+++ b/test/ruby/test_regexp.rb
@@ -972,6 +972,23 @@ def test_regsub_K
assert_equal('foobazquux/foobazquux', result, bug8856)
end

+ def test_alternation_compile_error_no_memory_leak
+ # A compile error in the 2nd+ branch of a top-level alternation leaked the
+ # partially parsed branch (parse_subexp freed the alternation list but not
+ # the node parse_branch had built so far). Regexp#to_s reaches this path
+ # for every /(?:a|b)c(d)/-shaped regexp.
+ assert_no_memory_leak([], "#{<<~"begin;"}", "#{<<~"end;"}", "[Bug #22384]", rss: true)
+ code = proc do
+ Regexp.new("a|b(c") rescue nil
+ /(?:a|b)c(d)/.to_s
+ end
+
+ 1_000.times(&code)
+ begin;
+ 300_000.times(&code)
+ end;
+ end
+
def test_ignorecase
v = assert_deprecated_warning(/variable \$= is no longer effective/) { $= }
assert_equal(false, v)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
From: Hiroshi SHIBATA <hsbt@ruby-lang.org>
Date: 2026-10-23T10:22:03+09:00
Subject: [PATCH 6/6] Preserve compiled regexp size errors with subexp calls

Backport of Ruby 3.3 commit f6496bca68ca445bcc1ced04818bfa4220b2bc7f.
Follow-up to the size limit from 0004; do not overwrite a failed
OP_END result while fixing subexpression call addresses.

---
diff --git a/regcomp.c b/regcomp.c
index 9453101418..e36b9b7d3e 100644
--- a/regcomp.c
+++ b/regcomp.c
@@ -5886,6 +5886,7 @@ onig_compile(regex_t* reg, const UChar* pattern, const UChar* pattern_end,
r = compile_tree(root, reg);
if (r == 0) {
r = add_opcode(reg, OP_END);
+ if (r != 0) goto err_unset;
#ifdef USE_SUBEXP_CALL
if (scan_env.num_call > 0) {
r = unset_addr_list_fix(&uslist, reg);