1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
|
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 <make.just.on@gmail.com>
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 <hsbt@ruby-lang.org>
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 <noreply@anthropic.com>
--- 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 <hsbt@ruby-lang.org>
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 <noreply@anthropic.com>
--- 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 <make.just.on@gmail.com>
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
|