Vulnerability research

GitLab's 10.0 flaw starts in the regex compiler

CVE-2026-97229 affects three GitLab CE/EE maintenance branches and can, under certain conditions, enable unauthenticated server code execution through CI/CD regex compilation, rated 10.0 by GitLab. Install the complete distribution with runtime fixes at 19.2.8, 19.3.5 or 19.4.2, since updating Runner alone leaves the server component unchanged.

Warm-paper drawing of a short paper strip entering a compiler mechanism and expanding into a long instruction roll that outgrows its length gauge.
In this article

When reviewing a pipeline configuration, people usually start with the commands it will run. A regular expression looks like a selection rule, deciding whether a branch, file or variable matches. Before matching can begin, however, the expression has to become a program. Compilation itself calculates lengths, emits instructions and writes memory.

On October 12, GitLab issued an emergency security advisory. CVE-2026-97229 concerns its CI/CD regular-expression compiler. The vendor says a crafted expression can, under certain conditions, allow an unauthenticated attacker to execute code on the GitLab server. GitLab assigned a CVSS 3.1 score of 10.0, with PR:N and UI:N indicating no prior privileges or user interaction. The complete remote triggering request has not been disclosed.

Self-managed administrators need to update the GitLab distribution and the runtime it ships. Updating Runner alone cannot replace the regex compiler on the server. The current official targets are 19.2.8, 19.3.5 and 19.4.2. Pay particular attention to 19.3. Some reports still name 19.3.4, while the current advisory uses 19.3.5 and explains that the earlier build affected merge request pages with the Ona integration enabled. GitLab.com is patched, and Dedicated customers do not need to perform this upgrade themselves.

1 A short rule can produce a long program

A regex compiler has to turn repetition into executable instructions. For a sufficiently small repeat, copying instructions can avoid loop-control overhead. A large repeat is usually better kept as a loop. The compiler therefore estimates the expanded length before choosing a representation.

The Ruby program-size repair backported in this GitLab release comes from upstream PR #19020 and shows how that choice can fail. The complete GitLab request entry point remains undisclosed. The following code locations refer to upstream Ruby before and after the repair. In lines 983–1047 of upstream's pre-fix regcomp.c, compile_length_quantifier_node() calculates the length required by a repetition node. tlen is the compiled length of its inner expression, and qn->lower is its minimum repeat count. Certain branches compare their product with QUANTIFIER_EXPAND_LIMIT_SIZE, whose value is 50.

// Before the repair: one condition deciding whether to expand a repeat
tlen * qn->lower <= QUANTIFIER_EXPAND_LIMIT_SIZE

Two individually valid numbers can have a product outside the range of int. Nested repetition enlarges the inner expression, and the outer repeat multiplies its length again. The comparison intended to prevent enormous expansion now depends on an overflowed result.

An eight-bit model makes the mistake easier to see. A length of 20 repeated 13 times has a true product of 260, clearly above 50. Keeping only its low eight bits gives 4, making the comparison accept a result of 4 against the limit of 50. The repaired form evaluates 20 <= 50 / 13. Integer division gives 3, correctly rejecting expansion. This is a safe truncation model. Signed overflow in actual C is undefined behavior, so its particular result depends on the build.

The cost reaches beyond estimation. In lines 1084–1105 of the same older revision, compile_quantifier_node() makes the corresponding decision and calls compile_tree_n_times() after entering the branch. It actually emits the repeated instructions. A mistaken choice turns a compact loop into extensive program copying.

The upstream Ruby patch explanation describes a second growth path. Concatenating large repeats can take the final program beyond INT_MAX, causing relative jump offsets emitted into it to overflow. Source length, compiled-program length and the distance an offset type can represent are three different quantities. Bounding the first does not establish that the other two remain valid.

2 Repair the arithmetic, then bound the emitted program

The first change preserves the decision to expand small repeats and changes how the comparison is made. Lines 747–751 of the fixed source introduce IS_EXPAND_LIMIT_OK. For a positive repeat count, it compares the inner length with the limit divided by that count. It avoids calculating the potentially overflowing product. The macro short-circuits when n <= 0, without performing division.

#define IS_EXPAND_LIMIT_OK(tlen, n) \
  ((n) <= 0 || (tlen) <= QUANTIFIER_EXPAND_LIMIT_SIZE / (n))

For a nonnegative length and a positive count, both forms express the same size relationship. The division form can evaluate it without constructing a large product first. The macro is used in the corresponding length-estimation and instruction-generation branches. Changing only one side would let the two stages make different expansion decisions.

The second change bounds the total program. add_opcode() at lines 223–232 checks reg->used before writing an opcode and returns ONIGERR_TOO_BIG_COMPILED_PROGRAM when the limit is exceeded. It observes the program actually emitted, avoiding reliance on an estimated length that can itself overflow.

if (reg->used > MAX_COMPILED_PROGRAM_SIZE)
    return ONIGERR_TOO_BIG_COMPILED_PROGRAM;
BBUF_ADD1(reg, opcode);

MAX_COMPILED_PROGRAM_SIZE is defined as INT_MAX_LIMIT / 4. The patch comment explains that program offsets use RelAddrType, while lengths accumulate in int. Leaving three quarters of the range available provides room for later sums involving offsets and lengths. This is a numerical bound for the implementation, not an appropriate memory budget for every server.

The upstream regression test also exercises nested repetition in a separate process with a compilation timeout and checks matching against short text. The local validation above only evaluates integers. It does not execute enormous regexes or a complete GitLab exploit.

Two independent repairs: safely compare by division before expanding a repeat, then check the actual emitted size and return a compilation error if it exceeds the limit.
Figure 1. tlen is the compiled length of the inner expression and n is its repeat count. The expansion threshold is 50, while the output bound is separately defined as INT_MAX_LIMIT / 4. Open full-size image

3 A Ruby version number cannot identify its backports

GitLab's release source connects the upstream changes to its packages. CNG is GitLab's cloud-native image build project. Its merge request !3128 backports the program-size repair to Ruby 3.3 and 3.4 on the 19.4 branch. The regexp-program-size.patch in the released v19.4.2-ee build contains both changes described above.

Lines 65–73 of gitlab-ruby/Dockerfile at that same commit download the Ruby source, call apply_ruby_patches.sh, then compile it. The file defaults to Ruby 3.3.12. Lines 17–40 of the script first check that mandatory patches exist, then apply patches in the matching Ruby major-minor and exact-version directories. The upstream version displayed by ruby -v alone cannot tell an administrator whether GitLab's backport is present.

The Linux package has a corresponding step. In the released 19.4.2+ee.0 source, lines 136–150 of config/software/ruby.rb add regexp-program-size for Ruby 3.3 and 3.4, then select the patch file for the actual Ruby branch. Verify the complete GitLab package or image version and its build provenance. Replacing a Ruby executable cannot stand in for the product update.

This release also lists a similarly named repair, the repeat-range double-free patch in CNG !3131. It comes from a different upstream Ruby change and addresses a node being freed twice on a parse-error path by changing temporary-pointer ownership. Program-size overflow concerns expansion decisions and jump offsets. Double-free concerns object lifetime during error handling. The distribution repairs both, but their causes remain distinct.

GitLab CE 19.4.1's ordinary CI expression wrapper calls UntrustedRegexp::RubySyntax. After syntax conversion, UntrustedRegexp constructs a RE2 regex. The public report has not identified which entry point reaches the Ruby compiler. GitLab's detailed vulnerability reports remain under its policy of disclosure 90 days after the security release.

4 The current 19.3 target is 19.3.5

GitLab's CNA record limits the affected range to the following three maintenance branches. The advisory is dated October 12, while the CNA record was published at 01:05 UTC on October 13. The installation targets below reflect the current official advisory checked on October 13.

Existing branchAffected rangeCurrent patch target
19.219.2.0–19.2.719.2.8
19.319.3.0–19.3.419.3.5
19.419.4.0–19.4.119.4.2

Version 19.3.4 affects merge request pages when the Ona integration is enabled. Version 19.3.5 fixes that regression and is the current target named by the advisory and CNA. Instances that already installed 19.3.4 should continue to this replacement build.

Allow time for migrations. The official upgrade notes say these patches include database migrations. A single-node instance needs downtime for migrations to finish, while multi-node installations can follow the documented zero-downtime procedure. Version 19.3.5 also includes post-deploy migrations. A patch-level release does not imply an upgrade without downtime or migrations.

If an immediate update is impossible, temporarily restrict access to the affected GitLab instance to trusted administrative and business sources. This containment can interrupt external collaboration, webhooks and jobs that need to reach the instance. Public materials do not supply a WAF rule covering the complete trigger chain or name disabling a single CI feature as a universal repair. Keep failed-upgrade nodes isolated. Rollback requires a patched package compatible with the backup, and any database rollback should follow the official backup-restoration procedure.

5 Verify that the new build is serving traffic

After upgrading, confirm that every web and background node handling work runs the target build. Check old containers, release directories and processes that have not restarted. Inspect database and post-deploy migration status, then create a normal pipeline and exercise rule evaluation and job execution. A 19.3 deployment using Ona should also open a merge request page to verify the replacement build's relevant business fix. Working rules and legitimate jobs provide the basis for removing access restrictions introduced during repair.

Investigating past exploitation is a separate job. A GitLab server can hold source code, repository access and CI/CD credentials. Confirmed server compromise therefore calls for an investigation of what that server could actually reach, extending beyond unexpected jobs on a Runner. Preserve proxy, application and host logs, check unexpected processes and unauthorized project or configuration changes, then rotate credentials and restore trusted builds based on the server's access and the investigation's findings.

A regex compiler emits matching instructions for its engine to interpret. Invalid compiled lengths and jump offsets affect the process handling that input. The public repairs above check sizes when choosing expansion and emitting the program. GitLab's advisory identifies server code execution as the consequence of CVE-2026-97229. Administrators therefore need to update the complete GitLab build actually running, including the compiler fixes shipped with it.