# REPRO-2026-00209: Oj Ruby gem stack buffer overflow via large :indent value ## Summary Status: published Severity: medium Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00209 CVE: CVE-2026-54502 ## Package Name: oj Ecosystem: Ruby Affected: < 3.17.2 (per user); GitHub advisory lists affected < 3.17.2, patched 3.17.3 Fixed: 3.17.3 ## Root Cause # RCA Report: CVE-2026-54502 (Oj stack buffer overflow via large :indent) ## Summary The Oj Ruby gem (ohler55/oj) is vulnerable to a stack-based buffer overflow in versions prior to 3.17.2. When `Oj.dump` is called with a large `:indent` option (e.g., `INT_MAX`), the native `fill_indent` helper in `ext/oj/dump.h` multiplies the indentation count by `out->indent` and calls `memset(out->cur, ' ', cnt)` without validating that the destination buffer can hold the requested bytes. The stack-allocated output buffer is only a few kilobytes, so a 2 GB `memset` corrupts the stack and crashes the Ruby interpreter with a SIGSEGV. Commit `ec368db` ("Fix stack limits (#1014)", released as 3.17.2) mitigates the issue by rejecting `:indent` values greater than 16 at the option-parsing layer. ## Impact - **Package/component affected:** `ohler55/oj` (Optimized JSON gem for Ruby), specifically the C extension `ext/oj/dump.c` and the inline `fill_indent` helper in `ext/oj/dump.h`. - **Affected versions:** Prior to 3.17.2 (vulnerable parent commit `4587e87`; fix commit `ec368db`). - **Risk level and consequences:** Medium severity. A developer-controlled `:indent` value of `2147483647` causes a deterministic native crash (SIGSEGV) due to stack corruption. In processes that expose JSON serialization to untrusted input, this could be used for denial of service or, with further research, potentially memory corruption exploitation. ## Impact Parity - **Disclosed/claimed maximum impact:** memory corruption (stack buffer overflow) / crash. - **Reproduced impact from this run:** Native SIGSEGV crash in `Oj.dump` on the vulnerable version; the same call is cleanly rejected with an `ArgumentError` on the fixed version. - **Parity:** `full` — the reproduced crash directly matches the claimed memory-corruption impact. - **Not demonstrated:** Full arbitrary code execution was not attempted; only the crash/memory-corruption symptom was proven. ## Root Cause `ext/oj/dump.h` defines an inline function: ```c inline static void fill_indent(Out out, int cnt) { if (0 < out->indent) { cnt *= out->indent; *out->cur++ = '\n'; memset(out->cur, ' ', cnt); out->cur += cnt; } } ``` `out->indent` is populated from the Ruby `:indent` option in `ext/oj/oj.c` (`parse_options_cb`). In the vulnerable code there is no upper bound on the value, so passing `indent: 2147483647` makes `cnt` equal to `INT_MAX` and `memset` attempts to write ~2 GB of spaces into the stack-allocated output buffer, causing a stack overflow and SIGSEGV. Fix commit `ec368db` ("Fix stack limits (#1014)") introduces `MAX_INDENT 16` and raises `rb_raise(rb_eArgError, "indent is limited to %d characters.", MAX_INDENT)` when the provided indent exceeds that limit. This validation is performed before the value reaches `fill_indent`, preventing the overflow. - **Fix commit:** `ec368dbe936ef0104b782e4b0f67b17d6c7276f7` - **Vulnerable commit:** `4587e87e23adc9a4163834dc8c9ba9d7206c6501` (parent of fix, matches v3.17.1) ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh`. 2. The script reads `bundle/project_cache_context.json` and clones the Oj repository from the project cache into `bundle/artifacts/oj-vuln` and `bundle/artifacts/oj-fixed`. 3. It checks out the vulnerable commit (`4587e87`) in one copy, builds the C extension, and runs: ```ruby Oj.dump({a: 1}, indent: 2147483647) ``` This produces a SIGSEGV (exit code 139) and the Ruby interpreter prints a segmentation-fault backtrace. 4. It checks out the fixed commit (`ec368db`) in the second copy, builds the C extension, and runs the same Ruby call. The fixed version raises an `ArgumentError`: ``` indent is limited to 16 characters. ``` 5. The script compares the two outcomes and writes `bundle/repro/runtime_manifest.json` and `bundle/repro/validation_verdict.json`. ### Expected evidence of reproduction - `bundle/logs/vulnerable.log`: contains `[BUG] Segmentation fault at ...` and the Ruby/C backtrace. - `bundle/logs/fixed.log`: contains `ArgumentError: indent is limited to 16 characters.` - `bundle/logs/reproduction_steps.log`: contains the full build/test output and the final `CONFIRMED` line. ## Evidence ### Environment - Ruby 3.3.8 (x86_64-linux-gnu) - Oj vulnerable commit `4587e87` (VERSION 3.17.1) - Oj fixed commit `ec368db` (VERSION 3.17.2) - C extension built directly with `extconf.rb` + `make` in each checkout ### Key excerpts **Vulnerable run (`bundle/logs/vulnerable.log`):** ``` -e:1: [BUG] Segmentation fault at 0x00007ffc5049e000 ruby 3.3.8 (2025-04-09 revision b200bad6cd) [x86_64-linux-gnu] -- Control frame information ----------------------------------------------- c:0003 p:---- s:0012 e:000011 CFUNC :dump ... -- Machine register context ------------------------------------------------ ... RDX: 0x000000007fffffff ... ``` The `RDX` register holds `0x7fffffff` (`INT_MAX`), matching the requested indent size. **Fixed run (`bundle/logs/fixed.log`):** ``` -e:1:in `dump': indent is limited to 16 characters. (ArgumentError) require 'oj'; puts Oj::VERSION; Oj.dump({a: 1}, indent: 2147483647); puts 'no crash' ^^^^^^^^^^^^^^^^^^^^^^^^^^ from -e:1:in `
' 3.17.2 ``` **Driver log (`bundle/logs/reproduction_steps.log`):** ``` VULN_RESULT=0 FIXED_RESULT=1 CONFIRMED: vulnerable version crashes with SIGSEGV, fixed version does not. ``` ## Recommendations / Next Steps - **Suggested fix:** Apply the upstream patch from `ec368db` and enforce a maximum `:indent` value (currently 16) at the option-parsing layer, before any native buffer operation. Any location that accepts user-provided indentation settings should validate the value. - **Upgrade guidance:** Upgrade to Oj 3.17.2 or later. The vulnerable behavior is fixed by the upstream validation. - **Testing recommendations:** Add regression tests that call `Oj.dump` with `indent: 2147483647` and expect an `ArgumentError`. Also test with a variety of nested objects/arrays and negative/edge-case indent values to ensure no other path reaches `fill_indent` with an unbounded size. ## Additional Notes - **Idempotency:** The script was executed twice successfully from a clean state and from a state where the artifact clones already existed. Both runs produced the same SIGSEGV on the vulnerable build and `ArgumentError` on the fixed build, then exited with code 0 and wrote the required runtime manifest and verdict. - **Edge cases / limitations:** The reproduction uses the exact Ruby API call named in the ticket (`Oj.dump(..., indent: INT_MAX)`). The crash is a native SIGSEGV, not a sanitizer report; no ASAN/UBSAN build was used, so the primary oracle is the process exit status and the Ruby interpreter's segmentation-fault backtrace. ## Reproduction Details Reproduced: 2026-07-02T19:47:40.952Z Duration: 1113 seconds Tool calls: 184 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00209 pruva-verify CVE-2026-54502 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00209&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00209/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-54502 - Source: https://github.com/advisories/GHSA-3v45-f3vh-wg7m ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 8792 bytes) - bundle/repro/rca_report.md (analysis, 6821 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 8128 bytes) - bundle/vuln_variant/rca_report.md (analysis, 10008 bytes) - bundle/ticket.md (ticket, 745 bytes) - bundle/ticket.json (other, 1174 bytes) - bundle/repro/validation_verdict.json (other, 639 bytes) - bundle/repro/runtime_manifest.json (other, 431 bytes) - bundle/logs/reproduction_steps.log (log, 3996 bytes) - bundle/logs/vulnerable.log (log, 1175 bytes) - bundle/logs/vulnerable.result (other, 2 bytes) - bundle/logs/fixed.log (log, 251 bytes) - bundle/logs/fixed.result (other, 2 bytes) - bundle/logs/vuln_variant_reproduction_steps.log (log, 10427 bytes) - bundle/logs/vuln_dump.log (log, 1193 bytes) - bundle/logs/vuln_dump.result (other, 9 bytes) - bundle/logs/fixed_dump.log (log, 271 bytes) - bundle/logs/fixed_dump.result (other, 9 bytes) - bundle/logs/latest_dump.log (log, 272 bytes) - bundle/logs/latest_dump.result (other, 9 bytes) - bundle/logs/vuln_string_writer.log (log, 1212 bytes) - bundle/logs/vuln_string_writer.result (other, 9 bytes) - bundle/logs/fixed_string_writer.log (log, 339 bytes) - bundle/logs/fixed_string_writer.result (other, 9 bytes) - bundle/logs/latest_string_writer.log (log, 340 bytes) - bundle/logs/latest_string_writer.result (other, 9 bytes) - bundle/logs/vuln_stream_writer.log (log, 1244 bytes) - bundle/logs/vuln_stream_writer.result (other, 9 bytes) - bundle/logs/fixed_stream_writer.log (log, 425 bytes) - bundle/logs/fixed_stream_writer.result (other, 9 bytes) - bundle/logs/latest_stream_writer.log (log, 426 bytes) - bundle/logs/latest_stream_writer.result (other, 9 bytes) - bundle/logs/vuln_default_options.log (log, 1204 bytes) - bundle/logs/vuln_default_options.result (other, 9 bytes) - bundle/logs/fixed_default_options.log (log, 324 bytes) - bundle/logs/fixed_default_options.result (other, 9 bytes) - bundle/logs/latest_default_options.log (log, 325 bytes) - bundle/logs/latest_default_options.result (other, 9 bytes) - bundle/logs/vuln_negative_indent.log (log, 46 bytes) - bundle/logs/vuln_negative_indent.result (other, 3 bytes) - bundle/logs/fixed_negative_indent.log (log, 47 bytes) - bundle/logs/fixed_negative_indent.result (other, 3 bytes) - bundle/logs/latest_negative_indent.log (log, 48 bytes) - bundle/logs/latest_negative_indent.result (other, 3 bytes) - bundle/logs/vuln_bignum_indent.log (log, 272 bytes) - bundle/logs/vuln_bignum_indent.result (other, 9 bytes) - bundle/logs/fixed_bignum_indent.log (log, 273 bytes) - bundle/logs/fixed_bignum_indent.result (other, 9 bytes) - bundle/logs/latest_bignum_indent.log (log, 274 bytes) - bundle/logs/latest_bignum_indent.result (other, 9 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 8040 bytes) - bundle/vuln_variant/validation_verdict.json (other, 774 bytes) - bundle/vuln_variant/variant_manifest.json (other, 2797 bytes) - bundle/vuln_variant/source_identity.json (other, 703 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 1217 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 1599 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00209 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00209/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00209 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev