
Behind the GitLab RCE: A depthfirst Journey into the Ruby Ecosystem | depthfirst
depthfirst's Open Defense Initiative scanned about 40 Ruby gems with native C extensions and validated 105 vulnerabilities across 34 projects, which together have over 8.6 billion RubyGems downloads. The post walks through four cases: a Nokogiri 32/64-bit index truncation (CVE-2026-57235), an Oj garbage-collection use-after-free (CVE-2026-54902), a concurrent-ruby read/write lock bit overflow (CVE-2026-54905) and an sqlite3-ruby block lifetime bug (CVE-2026-54619). It also states that this work led to the earlier GitLab RCE chain through Oj.
What it is
depthfirst's Open Defense Initiative ran its agentic scanner over about 40 Ruby gems with native C extensions. It validated 105 vulnerabilities across 34 projects, which together have more than 8.6 billion RubyGems downloads. As of publication, maintainers had acknowledged 62 of the 105, and remediation of the rest was still in progress. For each validated finding the platform produced a triggering input, a reproduction script and evidence such as AddressSanitizer output. The page details four cases. It also says validated gem findings were the basis for the earlier GitLab RCE, which chained two memory-safety bugs through the Oj JSON parser.
How it works
Nokogiri, CVE-2026-57235. Nokogiri::XML::NodeSet#[] receives a 64-bit long index but bounds-checks abs((int)offset), a 32-bit truncation. On 64-bit platforms, -4,294,967,297 truncates to -1 and passes the check. The code then adds the node count to the original value, so the index stays deeply negative and reads far before nodeTab (out-of-bounds read). The fix compares the original value: offset < -c_self->nodeNr.
Oj, CVE-2026-54902. In Oj::Parser SAJ mode, the key of a nested object is held as a Ruby VALUE in C parser state that the garbage collector cannot see. A GC run while the object is still open can free a long key. The parser then reuses the stale reference, causing a use-after-free. The fix keeps keys of open objects and arrays on a dedicated stack that the GC marks, and pops each key when its object closes.
concurrent-ruby, CVE-2026-54905. Concurrent::ReentrantReadWriteLock stores the read-hold count (low 15 bits) and the WRITE_LOCK_HELD flag (bit 15) in one integer. The 32,768th reentrant read acquisition carries into bit 15. try_write_lock then returns true without setting the global RUNNING_WRITER bit, so other readers keep access and mutual exclusion fails. This is pure Ruby, not C. The page gives this reproducer:
lock = Concurrent::ReentrantReadWriteLock.new
32_768.times { lock.acquire_read_lock }
lock.try_write_lock # => trueThe fix separates the read count from the write flag.
sqlite3-ruby, CVE-2026-54619. SQLite identifies functions by name plus argument count. sqlite3-ruby kept one Ruby block per name. Registering f with two arguments after f with one replaced the first block's reference, so the GC could free it while SQLite still held a pointer. Running select f(1) could then call freed memory. The fix retains every registered block.
Preconditions / Affected
- Nokogiri: introduced in 1.3.0 (2009), fixed in 1.19.4 on June 18, 2026. The exploitable truncation is described for 64-bit platforms.
- Oj: the bug is in
Oj::ParserSAJ mode. - concurrent-ruby: reachable only by code that takes 32,768 reentrant read locks on one lock in one thread.
- sqlite3-ruby: requires registering same-named functions with different arities.
What to do
Check lockfiles for Nokogiri below 1.19.4 and upgrade. For Oj, concurrent-ruby and sqlite3-ruby, the page gives no fixed version numbers, so look up the CVE ids in each project's advisories. Audit uses of SAJ-mode Oj::Parser and arity-overloaded define_function.
Caveats
- The page gives no CVSS scores or exploitability ratings for the four CVEs.
- The Nokogiri bug is described as an out-of-bounds read, and the Oj and sqlite3-ruby bugs as use-after-free with a possible crash. Only the GitLab chain is stated to reach RCE, and that chain is in a separate post.
- The page is also a pitch for depthfirst's Supply Chain module, and it does not list the other 100 or so findings.
Organisations named here
- depthfirst
- Open Defense Initiative
CVEs named here
- CVE-2026-57235
- CVE-2026-54902
- CVE-2026-54905
- CVE-2026-54619
Technologies named here
- AddressSanitizer
- RubyGems
- GitLab
- Ruby
- SQLite
- C
- concurrent-ruby: Concurrent::ReentrantReadWriteLock read count overflows into the write-lock flag, breaking mutual exclusion (CVE-2026-54905)
- depthfirst platform
- depthfirst Supply Chain module
- Nokogiri
- Nokogiri 1.3.0 (introduced 2009) up to before 1.19.4 (fixed June 18, 2026): out-of-bounds read in Nokogiri::XML::NodeSet#[] (CVE-2026-57235)
- Oj: use-after-free in Oj::Parser SAJ mode (CVE-2026-54902); also used in GitLab, where it was chained into RCE
- Open Defense Initiative
- Ruby gems with native C extensions: 105 validated vulnerabilities across 34 projects, over 8.6 billion downloads on RubyGems
- sqlite3-ruby: use-after-free from retaining one Ruby block per function name (CVE-2026-54619)
Techniques named here
- Use-after-free
- Integer Overflow
- Bit overflow of packed state (read count into write-lock flag)
- Dependency tracing from application into native gems
- Integer truncation (64-bit long to 32-bit int) bounds check bypass
- Native callback lifetime mismatch
- Use-after-free via garbage collector unaware C reference
Also named here
- Oj
- concurrent-ruby
- sqlite3-ruby
Related in AppSec
Flowise MCP Security Bypass Enables RCE
Inside an Oracle Database SQL Injection Attack | Huntress
OASIS
