{"repro_id":"REPRO-2026-00209","version":8,"title":"Oj Ruby gem stack buffer overflow via large :indent value","repro_type":"security","status":"published","severity":"medium","description":"Oj (Optimized JSON) is a JSON parser and Object marshaller packaged as a Ruby gem. In versions prior to 3.17.2, Oj.dump is vulnerable to a stack-based buffer overflow when a large :indent value is provided by the developer. fill_indent in dump.h calls memset(indent_str, ' ', (size_t)opts->indent) without validating the size. When opts->indent is set to INT_MAX (2,147,483,647), the (size_t) cast preserves the large value and memset writes 2 GB into the stack-allocated out buffer (4,184 bytes), corrupting the stack and crashing the process. Fixed in version 3.17.2.","root_cause":"# RCA Report: CVE-2026-54502 (Oj stack buffer overflow via large :indent)\n\n## Summary\n\nThe 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.\n\n## Impact\n\n- **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`.\n- **Affected versions:** Prior to 3.17.2 (vulnerable parent commit `4587e87`; fix commit `ec368db`).\n- **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.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** memory corruption (stack buffer overflow) / crash.\n- **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.\n- **Parity:** `full` — the reproduced crash directly matches the claimed memory-corruption impact.\n- **Not demonstrated:** Full arbitrary code execution was not attempted; only the crash/memory-corruption symptom was proven.\n\n## Root Cause\n\n`ext/oj/dump.h` defines an inline function:\n\n```c\ninline static void fill_indent(Out out, int cnt) {\n    if (0 < out->indent) {\n        cnt *= out->indent;\n        *out->cur++ = '\\n';\n        memset(out->cur, ' ', cnt);\n        out->cur += cnt;\n    }\n}\n```\n\n`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.\n\nFix 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.\n\n- **Fix commit:** `ec368dbe936ef0104b782e4b0f67b17d6c7276f7`\n- **Vulnerable commit:** `4587e87e23adc9a4163834dc8c9ba9d7206c6501` (parent of fix, matches v3.17.1)\n\n## Reproduction Steps\n\n1. Run `bundle/repro/reproduction_steps.sh`.\n2. 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`.\n3. It checks out the vulnerable commit (`4587e87`) in one copy, builds the C extension, and runs:\n   ```ruby\n   Oj.dump({a: 1}, indent: 2147483647)\n   ```\n   This produces a SIGSEGV (exit code 139) and the Ruby interpreter prints a segmentation-fault backtrace.\n4. 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`:\n   ```\n   indent is limited to 16 characters.\n   ```\n5. The script compares the two outcomes and writes `bundle/repro/runtime_manifest.json` and `bundle/repro/validation_verdict.json`.\n\n### Expected evidence of reproduction\n\n- `bundle/logs/vulnerable.log`: contains `[BUG] Segmentation fault at ...` and the Ruby/C backtrace.\n- `bundle/logs/fixed.log`: contains `ArgumentError: indent is limited to 16 characters.`\n- `bundle/logs/reproduction_steps.log`: contains the full build/test output and the final `CONFIRMED` line.\n\n## Evidence\n\n### Environment\n\n- Ruby 3.3.8 (x86_64-linux-gnu)\n- Oj vulnerable commit `4587e87` (VERSION 3.17.1)\n- Oj fixed commit `ec368db` (VERSION 3.17.2)\n- C extension built directly with `extconf.rb` + `make` in each checkout\n\n### Key excerpts\n\n**Vulnerable run (`bundle/logs/vulnerable.log`):**\n\n```\n-e:1: [BUG] Segmentation fault at 0x00007ffc5049e000\nruby 3.3.8 (2025-04-09 revision b200bad6cd) [x86_64-linux-gnu]\n\n-- Control frame information -----------------------------------------------\nc:0003 p:---- s:0012 e:000011 CFUNC  :dump\n...\n-- Machine register context ------------------------------------------------\n ...\n RDX: 0x000000007fffffff\n ...\n```\n\nThe `RDX` register holds `0x7fffffff` (`INT_MAX`), matching the requested indent size.\n\n**Fixed run (`bundle/logs/fixed.log`):**\n\n```\n-e:1:in `dump': indent is limited to 16 characters. (ArgumentError)\n\nrequire 'oj'; puts Oj::VERSION; Oj.dump({a: 1}, indent: 2147483647); puts 'no crash'\n                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^\n\tfrom -e:1:in `<main>'\n3.17.2\n```\n\n**Driver log (`bundle/logs/reproduction_steps.log`):**\n\n```\nVULN_RESULT=0\nFIXED_RESULT=1\nCONFIRMED: vulnerable version crashes with SIGSEGV, fixed version does not.\n```\n\n## Recommendations / Next Steps\n\n- **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.\n- **Upgrade guidance:** Upgrade to Oj 3.17.2 or later. The vulnerable behavior is fixed by the upstream validation.\n- **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.\n\n## Additional Notes\n\n- **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.\n- **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.\n","cve_id":"CVE-2026-54502","source_url":"https://github.com/advisories/GHSA-3v45-f3vh-wg7m","package":{"name":"oj","ecosystem":"Ruby","affected_versions":"< 3.17.2 (per user); GitHub advisory lists affected < 3.17.2, patched 3.17.3","fixed_version":"3.17.3"},"reproduced_at":"2026-07-02T19:47:40.952817+00:00","duration_secs":1113.0,"tool_calls":184,"handoffs":2,"total_cost_usd":1.6746091000000003,"agent_costs":{"hypothesis_generator":0.0101864,"judge":0.14476699999999998,"repro":0.8796011900000003,"support":0.04876101,"vuln_variant":0.5912934999999999},"cost_breakdown":{"hypothesis_generator":{"accounts/fireworks/models/kimi-k2p7-code":0.0101864},"judge":{"gpt-5.5":0.14476699999999998},"repro":{"accounts/fireworks/models/kimi-k2p7-code":0.8796011900000003},"support":{"accounts/fireworks/models/kimi-k2p7-code":0.04876101},"vuln_variant":{"accounts/fireworks/models/kimi-k2p7-code":0.5912934999999999}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-02T19:47:41.942874+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":8792,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":6821,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":8128,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":10008,"category":"analysis"},{"path":"bundle/ticket.md","filename":"ticket.md","size":745,"category":"ticket"},{"path":"bundle/ticket.json","filename":"ticket.json","size":1174,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":639,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":431,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":3996,"category":"log"},{"path":"bundle/logs/vulnerable.log","filename":"vulnerable.log","size":1175,"category":"log"},{"path":"bundle/logs/vulnerable.result","filename":"vulnerable.result","size":2,"category":"other"},{"path":"bundle/logs/fixed.log","filename":"fixed.log","size":251,"category":"log"},{"path":"bundle/logs/fixed.result","filename":"fixed.result","size":2,"category":"other"},{"path":"bundle/logs/vuln_variant_reproduction_steps.log","filename":"vuln_variant_reproduction_steps.log","size":10427,"category":"log"},{"path":"bundle/logs/vuln_dump.log","filename":"vuln_dump.log","size":1193,"category":"log"},{"path":"bundle/logs/vuln_dump.result","filename":"vuln_dump.result","size":9,"category":"other"},{"path":"bundle/logs/fixed_dump.log","filename":"fixed_dump.log","size":271,"category":"log"},{"path":"bundle/logs/fixed_dump.result","filename":"fixed_dump.result","size":9,"category":"other"},{"path":"bundle/logs/latest_dump.log","filename":"latest_dump.log","size":272,"category":"log"},{"path":"bundle/logs/latest_dump.result","filename":"latest_dump.result","size":9,"category":"other"},{"path":"bundle/logs/vuln_string_writer.log","filename":"vuln_string_writer.log","size":1212,"category":"log"},{"path":"bundle/logs/vuln_string_writer.result","filename":"vuln_string_writer.result","size":9,"category":"other"},{"path":"bundle/logs/fixed_string_writer.log","filename":"fixed_string_writer.log","size":339,"category":"log"},{"path":"bundle/logs/fixed_string_writer.result","filename":"fixed_string_writer.result","size":9,"category":"other"},{"path":"bundle/logs/latest_string_writer.log","filename":"latest_string_writer.log","size":340,"category":"log"},{"path":"bundle/logs/latest_string_writer.result","filename":"latest_string_writer.result","size":9,"category":"other"},{"path":"bundle/logs/vuln_stream_writer.log","filename":"vuln_stream_writer.log","size":1244,"category":"log"},{"path":"bundle/logs/vuln_stream_writer.result","filename":"vuln_stream_writer.result","size":9,"category":"other"},{"path":"bundle/logs/fixed_stream_writer.log","filename":"fixed_stream_writer.log","size":425,"category":"log"},{"path":"bundle/logs/fixed_stream_writer.result","filename":"fixed_stream_writer.result","size":9,"category":"other"},{"path":"bundle/logs/latest_stream_writer.log","filename":"latest_stream_writer.log","size":426,"category":"log"},{"path":"bundle/logs/latest_stream_writer.result","filename":"latest_stream_writer.result","size":9,"category":"other"},{"path":"bundle/logs/vuln_default_options.log","filename":"vuln_default_options.log","size":1204,"category":"log"},{"path":"bundle/logs/vuln_default_options.result","filename":"vuln_default_options.result","size":9,"category":"other"},{"path":"bundle/logs/fixed_default_options.log","filename":"fixed_default_options.log","size":324,"category":"log"},{"path":"bundle/logs/fixed_default_options.result","filename":"fixed_default_options.result","size":9,"category":"other"},{"path":"bundle/logs/latest_default_options.log","filename":"latest_default_options.log","size":325,"category":"log"},{"path":"bundle/logs/latest_default_options.result","filename":"latest_default_options.result","size":9,"category":"other"},{"path":"bundle/logs/vuln_negative_indent.log","filename":"vuln_negative_indent.log","size":46,"category":"log"},{"path":"bundle/logs/vuln_negative_indent.result","filename":"vuln_negative_indent.result","size":3,"category":"other"},{"path":"bundle/logs/fixed_negative_indent.log","filename":"fixed_negative_indent.log","size":47,"category":"log"},{"path":"bundle/logs/fixed_negative_indent.result","filename":"fixed_negative_indent.result","size":3,"category":"other"},{"path":"bundle/logs/latest_negative_indent.log","filename":"latest_negative_indent.log","size":48,"category":"log"},{"path":"bundle/logs/latest_negative_indent.result","filename":"latest_negative_indent.result","size":3,"category":"other"},{"path":"bundle/logs/vuln_bignum_indent.log","filename":"vuln_bignum_indent.log","size":272,"category":"log"},{"path":"bundle/logs/vuln_bignum_indent.result","filename":"vuln_bignum_indent.result","size":9,"category":"other"},{"path":"bundle/logs/fixed_bignum_indent.log","filename":"fixed_bignum_indent.log","size":273,"category":"log"},{"path":"bundle/logs/fixed_bignum_indent.result","filename":"fixed_bignum_indent.result","size":9,"category":"other"},{"path":"bundle/logs/latest_bignum_indent.log","filename":"latest_bignum_indent.log","size":274,"category":"log"},{"path":"bundle/logs/latest_bignum_indent.result","filename":"latest_bignum_indent.result","size":9,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":8040,"category":"documentation"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":774,"category":"other"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":2797,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":703,"category":"other"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":1217,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1599,"category":"other"}]}