A newly disclosed remote code execution chain affects GitLab CE and EE, built entirely from two memory corruption bugs in Oj, a widely used native C-based JSON parser for Ruby.
Documented by Depthfirst, the flaws lived undetected in Oj for nearly five years before researchers connected them to GitLab’s notebook diff renderer.
The chain starts far from any obvious attack surface: GitLab’s Jupyter Notebook diff feature. When a user commits an .ipynb file, GitLab’s in-tree ipynbdiff gem parses the notebook JSON using Oj::Parser.usual.parse to render a human-readable diff.
That call reaches Oj’s native C extension inside a Puma worker process, running under the git account. An authenticated project member with push access could exploit this without any special privileges, admin access, or victim interaction.

GitLab RCE Chain Exploited
Write primitive (out-of-bounds write): Oj’s nesting-depth tracker used a fixed 1,024-byte stack array with no bounds check. Deeply nested arrays caused a forward sweep of 0x01 bytes past the array’s end, eventually corrupting a buffer pointer (buf.head).
Combined with a crafted 4,001-digit number and a 446-element array, Yuhang turned this into an allocator-assisted “heap grooming” trick, tricking jemalloc into placing attacker-controlled data over Oj’s internal callback pointer (p->start).
Read primitive (heap-pointer disclosure): A separate bug in Oj’s key-length handling let an oversized JSON object key value get truncated from a 64-bit size to a 16-bit field. This caused Oj to leak 29 raw bytes of internal Key structure, including a live heap pointer back through GitLab’s rendered diff output.
The leaked pointer gave researchers a heap “landmark.” Since GitLab’s Puma workers run in preloaded cluster mode, forked workers share identical library base addresses.
By comparing the leaked address against a measured displacement range, Yuhang brute-forced the libc/libruby base within minutes using a callback-controlled probe technique.
With ASLR defeated and callback-pointer control established, two lexically ordered malicious notebook files delivered in a single diff request armed the corrupted pointer in one file and triggered it via the next parse call in the same request, hijacking control flow into system() and executing arbitrary commands as the git user.
Affected Versions
| Component | Affected | Fixed |
|---|---|---|
| GitLab CE/EE | 15.2.0–18.10.7 | 18.10.8 |
| GitLab CE/EE | 18.11.0–18.11.4 | 18.11.5 |
| GitLab CE/EE | 19.0.0–19.0.1 | 19.0.2 |
| Oj gem | 3.13.0–3.17.1 | 3.17.3 |
The same Oj audit produced nine CVEs spanning buffer overflows, use-after-free bugs, and integer overflows across the parser, loader, and dumper APIs, underscoring how native C extensions inside “memory-safe” Ruby gems remain a persistent, underexamined attack surface.
Cut SOC investigation blind spots and contain threats earlier to reduce response costs and business disruption with ANY.RUN.