CVE
CVE-2026-110264, CVE-2026-97229, CVE-2026-93577, CVE-2026-89078
CWE
CWE-415, CWE-190, CWE-787
Affected Surface
- CRuby (`ruby`) `4.0.x < 4.0.9` (note: `4.0.8` lacks the fix for `CVE-2026-110264`), `3.4.x < 3.4.12`, `3.3.x < 3.3.13`, and end-of-life `<= 3.2.x` (no upstream patch)
- GitLab Community Edition (CE) and Enterprise Edition (EE) `7.0` through `18.11.12`, `19.0.0` through `19.0.9`, `19.1.0` through `19.1.8`, `19.2.0` through `19.2.8`, `19.3.0` through `19.3.5`, `19.4.0` through `19.4.2`, and `19.5.0` (fixed in `18.11.13`, `19.0.10`, `19.1.9`, `19.2.9`, `19.3.6`, `19.4.3`, and `19.5.1`)
- Ruby applications and background workers that compile user-supplied regular expressions via `Regexp.new`, `Regexp.compile`, `Regexp.union`, or string interpolation (`/#{input}/`)
TL;DR
On 23 October 2026, the Ruby core team released Ruby 4.0.9, 3.4.12, and 3.3.13 and published an advisory explaining that three memory-corruption bugs in CRuby’s built-in Onigmo regular-expression compiler (regparse.c and regcomp.c) were the root cause of four critical remote code execution CVEs that GitLab patched between 23 September and 21 October 2026:
CVE-2026-89078(CVSS 9.9 Critical): A double-free inparse_subexp()when parsing fails inside the second or later branch of an alternation (a|...), fixed by Ruby PR#13332.CVE-2026-93577(CVSS 9.9 Critical) andCVE-2026-97229(CVSS 10.0 Critical): Signed 32-bit integer overflows in repeat unrolling (tlen * qn->lower) and compiled bytecode offset calculation (RelAddrType), fixed by Ruby PR#19020and follow-up commit366f5fdc00(which prevents subexpression calls from overwriting theOP_ENDsize error).CVE-2026-110264(CVSS 10.0 Critical): Signed 16-bit/32-bitMemNumTypecounter wraparound onreg->num_repeatandreg->num_null_checkwhen compiling patterns with many range repeats or nullable quantifiers, fixed by Ruby PR#16801. On 21 October, GitLab backported this fix across seven release branches after confirming that the bug affected every self-managed GitLab version since7.0(2014).
Because Ruby has not requested separate Ruby-specific CVE IDs yet, vulnerability scanners that match CPEs against gitlab:gitlab will not flag vulnerable Ruby runtimes or Docker base images unless they also track CVE-2026-89078, CVE-2026-93577, CVE-2026-97229, and CVE-2026-110264 against CRuby. Any Ruby service that passes user-controlled strings to Regexp.new, Regexp.compile, Regexp.union, or /#{input}/ should upgrade to Ruby 4.0.9, 3.4.12, or 3.3.13 immediately.
Affected packages and projects
| Ecosystem | Package / product | CVE | Severity | Affected versions | Fixed versions | Underlying Ruby / Onigmo change |
|---|---|---|---|---|---|---|
| Ruby | ruby (CRuby Onigmo) | CVE-2026-89078 | CVSS 9.9 Critical | 3.3.x < 3.3.13, 3.4.x < 3.4.12, <= 3.2.x (EOL) | 4.0.0, 3.4.12, 3.3.13 | PR #13332 (35000ac2ed / 8382f95cfd) + leak fix d64496b422: double free on alternation parse failure in regparse.c |
| Ruby | ruby (CRuby Onigmo) | CVE-2026-93577, CVE-2026-97229 | CVSS 9.9 / 10.0 Critical | 4.0.x < 4.0.8 (4.0.8 incomplete with subexp calls), 3.4.x < 3.4.12, 3.3.x < 3.3.13, <= 3.2.x (EOL) | 4.0.9, 3.4.12, 3.3.13 | PR #19020 (cf35145fc4 / 2d1859ffa6) + 366f5fdc00: tlen * qn->lower overflow and MAX_COMPILED_PROGRAM_SIZE cap in regcomp.c |
| Ruby | ruby (CRuby Onigmo) | CVE-2026-110264 | CVSS 10.0 Critical | 4.0.x < 4.0.9, 3.4.x < 3.4.12, 3.3.x < 3.3.13, <= 3.2.x (EOL) | 4.0.9, 3.4.12, 3.3.13 | PR #16801 (493d26dcde): MemNumType overflow on reg->num_repeat and reg->num_null_check in regcomp.c |
| GitLab | GitLab CE / EE | CVE-2026-110264 | CVSS 10.0 Critical | 7.0 .. 18.11.12, 19.0.0 .. 19.0.9, 19.1.0 .. 19.1.8, 19.2.0 .. 19.2.8, 19.3.0 .. 19.3.5, 19.4.0 .. 19.4.2, 19.5.0 | 18.11.13, 19.0.10, 19.1.9, 19.2.9, 19.3.6, 19.4.3, 19.5.1 | Patches bundled Ruby inside Omnibus GitLab and CNG container images |
| GitLab | GitLab CE / EE | CVE-2026-97229, CVE-2026-93577, CVE-2026-89078 | CVSS 10.0 / 9.9 Critical | 19.2.x < 19.2.8, 19.3.x < 19.3.5, 19.4.x < 19.4.2 | 19.2.8, 19.3.5, 19.4.2 (superseded by 21 Oct releases above) | Earlier GitLab patches for PR #13332 and #19020 |
Technical analysis
All three bugs occur during pattern compilation inside onig_parse_make_tree() and onig_compile(), before the regular expression ever matches against a target string. That is why Regexp.timeout offers no protection: Regexp.timeout only bounds backtracking during matching after Regexp.new has already finished compiling the bytecode buffer.
1. CVE-2026-89078: double-free in parse_subexp() when an alternation branch fails to parse (PR #13332)
In regparse.c, parse_subexp(Node** top, ...) parses a parenthesized group or top-level pattern. When the parser encounters an alternation token (| / TK_ALT), it wraps the first branch node in an alternation list node (*top = onig_node_new_alt(node, NULL)) and loops over the remaining branches.
Before commit 35000ac2ed (backported to 3.4 in 8382f95cfd alongside the companion leak fix d64496b422), parse_subexp() wrote the alternation list directly into the caller’s *top pointer and then, if a later branch failed in fetch_token() or parse_branch() (for example on an oversized quantifier like /|{1000000}/), called onig_node_free(node) without clearing *top:
--- a/regparse.c
+++ b/regparse.c
@@ -6678,19 +6678,21 @@ parse_subexp(Node** top, OnigToken* tok, int term,
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) {
- onig_node_free(node);
+ onig_node_free(topnode);
return r;
}
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;
}
In the unpatched code, the first node was already owned by *top. When fetch_token() failed on the second branch, onig_node_free(node) freed that first branch while *top still pointed to the alternation wrapper referencing it. When the outer caller cleaned up *top on error, it freed the same Node a second time, giving an attacker a deterministic double-free from a 12-byte pattern.
2. CVE-2026-93577 and CVE-2026-97229: 32-bit integer overflows in repeat unrolling and bytecode emission (PR #19020 and 366f5fdc00)
Reported through GitLab’s bug bounty program by Johan Carlsson (joaxcar), the second bug class sits in regcomp.c. Onigmo decides whether to expand a bounded quantifier inline or emit a loop instruction by comparing the sub-tree byte length tlen multiplied by qn->lower against EXPAND_STRING_MAX_LENGTH.
With a nested bounded repeat such as (?:.{90000,}){90000}, the 32-bit signed product tlen * qn->lower overflows and wraps negative, passing the threshold check. Even without nesting, concatenating several .{100000,} repeats grows the compiled bytecode buffer reg->used past INT_MAX. Because Onigmo stores jump offsets in a signed 32-bit RelAddrType and accumulates lengths in int, a compiled program larger than INT_MAX wraps both the length pass and the emitted jump offsets.
Commit cf35145fc4 (PR #19020, backported in 2d1859ffa6) replaces the multiplication with division and enforces a hard MAX_COMPILED_PROGRAM_SIZE (INT_MAX_LIMIT / 4) inside add_opcode(), which every emitted instruction passes through:
--- a/regint.h
+++ b/regint.h
@@ -378,6 +378,11 @@ typedef unsigned int BitStatusType;
+/* 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))
--- a/regcomp.c
+++ b/regcomp.c
@@ -223,6 +223,10 @@ 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;
}
During backport testing on 23 October, the Ruby team also caught a subtle error-clobbering bug in onig_compile() (commit 366f5fdc00): if compile_tree() stopped right at the boundary and the final add_opcode(reg, OP_END) returned ONIGERR_TOO_BIG_COMPILED_PROGRAM, a pattern containing a subexpression call (\g<name>) immediately overwrote r with r = unset_addr_list_fix(&uslist, reg) (0), allowing the oversized bytecode buffer to finish compiling without raising RegexpError:
--- a/regcomp.c
+++ b/regcomp.c
@@ -5978,6 +5978,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);
3. CVE-2026-110264: MemNumType ID wraparound on range repeats and null checks (PR #16801)
The third bug, which backs GitLab’s CVSS 10.0 CVE-2026-110264 advisory across all GitLab releases back to 7.0, sits in compile_tree_empty_check() and compile_range_repeat_node() in regcomp.c.
Each range repeat (OP_REPEAT) and nullable-loop check (OP_NULL_CHECK_START) is assigned a sequential index from reg->num_repeat or reg->num_null_check, serialized into the bytecode stream as a signed MemNumType (short / int). Before commit 493d26dcde (PR #16801), neither counter was checked for wraparound:
--- a/regcomp.c
+++ b/regcomp.c
@@ -394,6 +394,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);
@@ -703,6 +704,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;
When a pattern repeated enough (?:foobar){0,100} or (?:(?:foo)?|(?:bar)?)* groups to wrap (MemNumType)reg->num_repeat or (MemNumType)reg->num_null_check into negative values, the VM indexed out of bounds of the repeat and null-check state arrays during execution. Note that Ruby 4.0.8 included PR #19020 but did not include PR #16801, which is why Ruby 4.0 users must upgrade to 4.0.9.
Detection and remediation
1. Check Ruby and GitLab versions across hosts and container images
Verify your installed Ruby runtime and search your codebase for dynamic regular-expression construction from request parameters, CI/CD YAML rules, or user filters:
# Check the running Ruby version (must be >= 4.0.9, >= 3.4.12, or >= 3.3.13)
ruby -v
# On self-managed GitLab Omnibus hosts, check the installed GitLab and embedded Ruby versions
cat /opt/gitlab/version-manifest.txt 2>/dev/null | grep -E '^(gitlab-(ce|ee)|ruby)\s' || true
# Search Ruby codebases for untrusted input passed to Regexp constructors or interpolated into // literals
rg -n 'Regexp\.(new|compile|union)\s*\(|/#\{[^}]+\}/' --glob '*.rb' . 2>/dev/null
# Search Dockerfiles, .ruby-version, and Gemfile manifests for unpatched Ruby 3.3, 3.4, 4.0, or EOL <= 3.2
rg -n 'FROM\s+(\S+/)?ruby:|ruby\s+["'\''][0-9]+\.[0-9]+\.[0-9]+' \
Dockerfile* .ruby-version .tool-versions Gemfile* 2>/dev/null
For self-managed GitLab administrators, GitLab also published two Sigma-format detection rules in its open-source TLDR repository (Onigmo native worker crash and Org mode render error burst) to spot worker crashes and parser anomalies in GitLab application logs.
2. Upgrade Ruby runtimes and self-managed GitLab instances
- Upgrade Ruby to
4.0.9,3.4.12, or3.3.13:- If you run Ruby
4.0.8, upgrade again to4.0.9so PR#16801(CVE-2026-110264) and commit366f5fdc00are present. - If you are on Ruby
3.2or older (end-of-life since 1 April 2026), migrate to3.4.12or4.0.9; upstream Ruby3.2will not receive backports.
- If you run Ruby
- Upgrade self-managed GitLab to the 21 October patch releases:
- Upgrade to
19.5.1,19.4.3,19.3.6,19.2.9,19.1.9,19.0.10, or18.11.13. Even instances that patched on 12 October forCVE-2026-97229(19.4.2,19.3.5,19.2.8) remain vulnerable toCVE-2026-110264until upgraded to the 21 October release.
- Upgrade to
- Avoid compiling untrusted regular expressions: Do not rely on
Regexp.timeoutor string length caps to makeRegexp.new(user_input)safe. Where user-supplied matching is required, use literal substring helpers (String#include?,String#start_with?,Regexp.escape(input)), or evaluate untrusted patterns in a memory-safe engine such as RE2 (re2gem) rather than Onigmo.
Timeline
- 2025-05-16: Ruby merges PR
#13332(35000ac2ed) onmasterto fix a double-free on oversized repetition quantifiers inside alternations. - 2026-04-29: Ruby merges PR
#16801(493d26dcde) onmasterto reject overflowed repeat and null-check IDs. - 2026-09-23: GitLab discloses
CVE-2026-89078andCVE-2026-93577(both CVSS 9.9, reported byjoaxcar) and patches its bundled Ruby in GitLab19.4.1,19.3.3, and19.2.7. - 2026-09-25: Ruby merges PR
#19020(cf35145fc4) capping compiled Onigmo program size atINT_MAX_LIMIT / 4. - 2026-10-12: GitLab discloses unauthenticated
CVE-2026-97229(CVSS 10.0) in19.4.2,19.3.5, and19.2.8. - 2026-10-21: GitLab discloses unauthenticated
CVE-2026-110264(CVSS 10.0), affecting all GitLab versions since7.0, and ships backports across seven release branches (19.5.1down to18.11.13). - 2026-10-23 (03:11 UTC): Ruby releases
4.0.9,3.4.12, and3.3.13with all three Onigmo fixes (plus follow-up commit366f5fdc00) and publishes its coordinated advisory mapping the GitLab CVEs to CRuby commits.
From research to remediation
Check whether this pattern exists in your codebase
Turn this research into a remediation workflow. Use AI-native static analysis to find similar application vulnerabilities and generate review-ready fixes.
References
- Ruby advisory: Regexp compilation fixes related to GitLab's CVEs
- oss-security: Ruby Regexp compilation fixes related to GitLab's CVEs
- ruby/ruby PR #13332 (commit 35000ac2ed): Prevent double free for too big repetition quantifiers
- ruby/ruby PR #19020 (commit cf35145fc4) and follow-up commit 366f5fdc00: Reject compiled programs too big for their offset type
- ruby/ruby PR #16801 (commit 493d26dcde): Reject overflowed repeat/null-check IDs in huge regexps
- GitLab 19.5.1, 19.4.3, and 19.3.6 patch release advisory (CVE-2026-110264)
- The Hacker News: Ruby fixes three regex engine bugs responsible for four critical GitLab RCE flaws
- The Hacker News: Critical GitLab flaw (CVSS 10.0) could allow unauthenticated RCE across 12 years of releases