Tech9 min read

Ruby 4.0.9, 3.4.12, and 3.3.13 Fix the Onigmo Bugs Behind Four GitLab CVEs

IkesanContents

TL;DR

What’s affected Ruby apps that build a regular expression from a string they received from outside (Regexp.new, Regexp.union, Regexp.compile, /#{input}/). A crafted pattern corrupts memory while Ruby compiles or matches it
What to do Upgrade to Ruby 4.0.9, 3.4.12, 3.3.13, or later. Until you can, stop compiling regexes from untrusted input
Not affected Apps that only use regexes their own developers wrote. Regexp.timeout and pattern length limits do not stop these bugs


On October 23, 2026, the Ruby core team released Ruby 4.0.9, 3.4.12, and 3.3.13 (Regexp compilation fixes related to GitLab’s CVE-2026-89078, CVE-2026-93577, CVE-2026-97229 and CVE-2026-110264).
The releases fix three bugs in Onigmo, Ruby’s regular expression engine (written in C), and every one of them was behind a remote code execution bug (an attacker running arbitrary programs on the server over the network) that GitLab published as its own CVE.

I covered the GitLab side in the October 21 post on CVE-2026-110264 (GitLab CVE-2026-110264 (CVSS 10.0) Lets Unauthenticated Attackers Run Code by Rendering an .org File).
At that point GitLab had only patched the Ruby bundled with Omnibus (its official package), and the fix in Ruby itself existed only on the development master branch.
With these releases, Ruby apps other than GitLab can get the fix by upgrading Ruby.

The four GitLab CVEs and the Ruby fixes

GitLab compiles regexes written by its users (patterns in CI/CD configuration, for example) with Regexp.new, which turns the pattern string into the engine’s internal instruction sequence used for matching.
GitLab found that a crafted pattern could corrupt memory during Ruby’s compilation step and that this could be turned into code execution on the GitLab server, and it published four CVEs between September and October.

GitLab CVEGitLab fixed inBug in RubyRuby fix4.03.43.3
CVE-2026-8907819.4.1When parsing a pattern fails after an alternation (|), the same memory is freed twice (double free)#133324.0.03.4.123.3.13
CVE-2026-93577, CVE-2026-9722919.4.1, 19.4.2Integer overflow when computing the size of the compiled program#190204.0.83.4.123.3.13
CVE-2026-11026419.5.1Internal IDs overflow in a pattern with many repeats#168014.0.93.4.123.3.13

The three columns on the right are the first release in each series that contains the fix.
Ruby 4.0.9, 3.4.12, and 3.3.13 contain all three fixes.

The double-free fix, #13332, was merged into master in May 2025 and shipped in 4.0.0 in December 2025.
It never reached the 3.4 and 3.3 series until 3.4.12 and 3.3.13.
GitLab’s 19.4.1 advisory (GitLab Critical Patch Release: 19.4.1, 19.3.3, 19.2.7) says CVE-2026-89078 is reached through a regex in CI/CD configuration, lets an authenticated user execute code, and scores CVSS 9.9 (out of 10).

#19020 fixes nested repeats such as (?:.{90000,}){90000}, where multiplying out the expanded size overflows a 32-bit integer.
Before the fix, Ruby tried to unroll a repeat that should have been over the limit, and Regexp.new stopped responding.
Concatenating a few hundred kilobytes of .{100000,} made the compiled program larger than INT_MAX (about 2.1 billion), and the jump offsets inside it wrapped around (a value past the limit turned negative or small).

#16801 is the fix for the repeat ID wraparound I covered in the previous post. It makes Ruby raise RegexpError at compile time.

The 3.3 series is in security maintenance, where only vulnerability fixes are released.
According to the announcement, these fixes went into 3.3.13 as an exception because GitLab assigned CVEs to the issues.
Ruby 3.2 and earlier are past end of life, and no fixed release exists for them (Ruby Maintenance Branches).

Which Ruby apps are affected

According to the announcement, these bugs are reached only when an application compiles a regex from a string an attacker controls. These are the forms it lists, with pattern or words coming from outside:

# 影響を受ける書き方(patternやwordsが外部から来る場合)
Regexp.new(pattern)
Regexp.compile(pattern)
Regexp.union(words)
/#{pattern}/

Apps that only use regexes written into their source code by their developers are not affected.
Code that passes search form input, API parameters, or user-saved settings straight into Regexp.new is the kind of place that is.

Regexp.timeout (the setting that aborts a match that takes too long) does not protect against these bugs.
The announcement also says limiting pattern length is not a reliable defense, because some of the bugs can be triggered by short patterns.
For anyone who cannot upgrade, the only advice it gives is to avoid compiling regexes from untrusted input.

No Ruby CVE IDs yet

The Ruby team has not requested CVE IDs on Ruby’s side for these fixes.
Ruby has generally considered compiling and using a regex built from untrusted input to be unsafe.
Even so, it treated an earlier double free in regex compilation (CVE-2022-28738) and an out-of-bounds memory read (reading memory outside the allocated region, CVE-2024-27282) as vulnerabilities after assessing them.

The current Regexp documentation, on the other hand, talks about untrusted patterns only as a denial-of-service (DoS) problem and just recommends setting a timeout.
It does not clearly say whether compiling an untrusted pattern is something Ruby supports.
The team plans to discuss how to document this, whether to treat this kind of issue as a Ruby vulnerability, and whether to request CVE IDs.
Until then, the announcement asks people to use GitLab’s CVE IDs when tracking these issues, for example in a vulnerability scanner.

Running the fix PRs’ test patterns on five Ruby versions

To check whether each fix is present, I compiled the patterns from the regression tests (tests that make sure a fixed bug does not come back) added in each PR with Regexp.new.
This Windows PC has no Ruby installed, so I built Ruby from the official source under WSL2 (Ubuntu 26.04, x86_64) on Windows 11.
As of the evening of October 23, 2026 (JST), the official Docker Hub images went up to 4.0.8, with no 4.0.9, 3.4.12, or 3.3.13 yet.

The five versions were 4.0.6 (the same version I tested with Homebrew on a Mac in the previous post), 4.0.8 (which has only #19020), 4.0.9, 3.4.11, and 3.4.12.
The SHA256 of each downloaded tar.xz matched the value in the release announcement.
The regex engine is part of Ruby’s core, so I tested with miniruby, the minimal build that skips the extension libraries.

curl -sLO https://cache.ruby-lang.org/pub/ruby/4.0/ruby-4.0.9.tar.xz
sha256sum ruby-4.0.9.tar.xz
tar xf ruby-4.0.9.tar.xz
cd ruby-4.0.9
./configure --disable-install-doc
make -j$(nproc) miniruby
./miniruby -v

I ran five patterns, each passed to Regexp.new in a separate process.
Some bugs might only show up during matching rather than compilation, so whenever compilation succeeded I also matched the regex against "xxxxxxxxxx".
Any pattern that took longer than 30 seconds was killed.

PatternSource PR
|{1000000}Test in #13332 (double free)
(?:.{90000,}){90000}Test in #19020 (nested repeat)
.{100000,} repeated 21,475 timesExample in the #19020 description (program exceeds INT_MAX)
(?:foobar){0,100} repeated 100,000 timesTest in #16801 (repeat ID)
(?:(?:foo)?|(?:bar)?)* repeated 100,000 timesTest in #16801 (null-check ID)

My first version used timeout’s default SIGTERM (a signal that asks the process to exit) for the 30-second cutoff, but the nested-repeat pattern on 4.0.6 kept running for more than 10 minutes.
My guess is that Ruby does not stop the moment it receives SIGTERM but handles the signal at a later checkpoint, so it kept going until compilation finished.
I switched to SIGKILL (a signal that kills the process on the spot) and reran all five versions.

Pattern3.4.113.4.124.0.64.0.84.0.9
Double free (#13332)Aborted with double free detectedRegexpErrorRegexpErrorRegexpErrorRegexpError
Nested repeat (#19020)Killed at 30 s (2.6 GB used)Compiles immediatelyKilled at 30 s (2.6 GB used)Compiles immediatelyCompiles immediately
Over INT_MAX (#19020)Compiles in about 5 s (2.1 GB used)RegexpErrorCompiles in about 6 s (2.1 GB used)RegexpErrorRegexpError
Repeat ID (#16801)Segmentation fault while matchingRegexpErrorSegmentation fault while matchingSegmentation fault while matchingRegexpError
Null-check ID (#16801)Compiles without errorRegexpErrorCompiles without errorCompiles without errorRegexpError

On 3.4.11, just passing the #13332 test pattern |{1000000} to Regexp.new was enough for glibc (the standard C library on Linux) to detect a double free and abort the process.
The 4.0 series has had this fix since 4.0.0, so 4.0.6, 4.0.8, and 4.0.9 all raise RegexpError with too big number for repeat range.

On 4.0.6 and 3.4.11, the #16801 repeat-ID pattern compiled, and matching it against a 10-character string ended in [BUG] Segmentation fault (the process touched memory it was not allowed to access and was killed).
On the Mac in the previous post I had only confirmed that the pattern compiles. Going on to the match step made the process crash.

4.0.8 is the routine bugfix release from October 22. It behaved like the fixed versions on the two #19020 patterns, but crashed on the #16801 pattern just like 4.0.6.
The #16801 fix landed the next day in 4.0.9.

On 4.0.9 and 3.4.12, every one of the five patterns either raised RegexpError right away or compiled without trouble.
After the fix, the nested-repeat pattern compiles immediately without an error, and matching returns nil (no match).
The segmentation faults and memory corruption happened with these patterns on this WSL2 x86_64 Linux setup, and I did not try to turn them into code execution.

Which version to upgrade to

ruby -v shows the version you have.

Current versionUpgrade to
4.0.0 to 4.0.84.0.9
3.4.0 to 3.4.113.4.12
3.3.0 to 3.3.123.3.13
3.2 or earlierNo fixed release. Move to a supported series

Ruby 3.3.13 also updates the default gem resolv (a library bundled with Ruby) to 0.3.2, which fixes CVE-2026-80212 and CVE-2026-80213 (Ruby 3.3.13 Released).

Omnibus GitLab bundles its own Ruby, but a GitLab installed from source uses a Ruby installed separately, for example through the OS.
In that case too, upgrading that Ruby to 4.0.9, 3.4.12, or 3.3.13 gives you all three Ruby-side fixes.