CVE-2026-54502: Verified Repro With Script Download
CVE-2026-54502: Oj Ruby gem stack buffer overflow via large :indent value
CVE-2026-54502 is verified against oj · Ruby. Affected versions: < 3.17.2 (per user); GitHub advisory lists affected < 3.17.2, patched 3.17.3. Fixed in 3.17.3. Vulnerability class: Buffer Overflow. This medium reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00209.
What Is CVE-2026-54502?
CVE-2026-54502 is a medium-severity stack-based buffer overflow in the Oj Ruby gem's Oj.dump, triggered when a caller passes an oversized :indent option. Pruva reproduced it (reproduction REPRO-2026-00209).
CVE-2026-54502 Severity & CVSS Score
CVE-2026-54502 is rated medium severity, with a CVSS base score of 6.3 out of 10.
Medium — meaningful risk under specific conditions. Schedule a fix in the normal cycle.
Affected oj Versions
oj · Ruby versions < 3.17.2 (per user); GitHub advisory lists affected < 3.17.2, patched 3.17.3 are affected.
How to Reproduce CVE-2026-54502
pruva-verify REPRO-2026-00209 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00209/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-54502
- reached the target end-to-end
- crash observed
- high confidence
- the upstream fix blocks the same trigger
indent: 2147483647
- Oj.dump({a: 1}, indent: INT_MAX)
reproduction_steps.sh No bypass found for the Oj :indent stack overflow. Alternate entry points (Oj::StringWriter, Oj::StreamWriter, Oj.default_options) trigger the same sink on the vulnerable version but are all covered by the upstream MAX_INDENT validation in parse_options_cb.
How the agent worked
Root Cause and Exploit Chain for CVE-2026-54502
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.
- Package/component affected:
ohler55/oj(Optimized JSON gem for Ruby), specifically the C extensionext/oj/dump.cand the inlinefill_indenthelper inext/oj/dump.h. - Affected versions: Prior to 3.17.2 (vulnerable parent commit
4587e87; fix commitec368db). - Risk level and consequences: Medium severity. A developer-controlled
:indentvalue of2147483647causes 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.dumpon the vulnerable version; the same call is cleanly rejected with anArgumentErroron 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:
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
- Run
bundle/repro/reproduction_steps.sh. - The script reads
bundle/project_cache_context.jsonand clones the Oj repository from the project cache intobundle/artifacts/oj-vulnandbundle/artifacts/oj-fixed. - It checks out the vulnerable commit (
4587e87) in one copy, builds the C extension, and runs:
This produces a SIGSEGV (exit code 139) and the Ruby interpreter prints a segmentation-fault backtrace.Oj.dump({a: 1}, indent: 2147483647) - 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 anArgumentError:indent is limited to 16 characters. - The script compares the two outcomes and writes
bundle/repro/runtime_manifest.jsonandbundle/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: containsArgumentError: indent is limited to 16 characters.bundle/logs/reproduction_steps.log: contains the full build/test output and the finalCONFIRMEDline.
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+makein 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 `<main>'
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
ec368dband enforce a maximum:indentvalue (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.dumpwithindent: 2147483647and expect anArgumentError. Also test with a variety of nested objects/arrays and negative/edge-case indent values to ensure no other path reachesfill_indentwith 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
ArgumentErroron 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.
Variant Analysis & Alternative Triggers for CVE-2026-54502
This variant analysis tested whether the :indent stack-buffer-overflow bug in the Oj Ruby gem could be triggered through a different entry point or bypassed on the patched version. The original vulnerability (CVE-2026-54502) is in ext/oj/dump.h::fill_indent, which writes indent * depth spaces into a stack-allocated output buffer when the caller supplies a large :indent value. The upstream fix (ec368db, v3.17.2) rejects any numeric :indent greater than 16 in the shared option parser (ext/oj/oj.c::parse_options_cb).
We tested four entry points on the vulnerable, fixed, and latest commits: Oj.dump, Oj::StringWriter, Oj::StreamWriter, and Oj.default_options. All four crash on the vulnerable version and are cleanly rejected on the fixed/latest versions. No bypass was found. The alternate entry points are not distinct security variants because they share the same option-parsing path and the same trust boundary as the original trigger.
Fix Coverage / Assumptions
The upstream fix relies on a single invariant: every dump option, including :indent, is parsed by oj_parse_options() and its parse_options_cb callback before it reaches fill_indent. The fix adds:
#define MAX_INDENT 16
if (MAX_INDENT < FIX2INT(v)) {
rb_raise(rb_eArgError, "indent is limited to %d characters.", MAX_INDENT);
}
This explicitly covers all dump APIs because they all route through oj_parse_options():
Oj.dump/Oj.to_json(ext/oj/oj.c)Oj::StringWriter.new(options)(ext/oj/string_writer.c:280)Oj::StreamWriter.new(io, options)(ext/oj/stream_writer.c:113)Oj.default_options = { ... }(ext/oj/oj.c:609)- All
oj_dump_*mode-specific paths (Object, Strict, Compat, Rails, Custom, WAB)
The fix does not rely on callers being well-behaved; it validates at the library boundary. It does not cover direct memory corruption or C-level exploitation, which are outside the library's threat model.
Variant / Alternate Trigger
We evaluated the following candidate variants/bypasses:
Alternate entry point:
Oj::StringWriter.new(indent: 2147483647)- Vulnerable: SIGSEGV (reaches
fill_indentviaoj_str_writer_push_value/fill_indent). - Fixed:
ArgumentErrorrejected byparse_options_cb. - Same root cause, but same option-parsing path; not a distinct bypass.
- Vulnerable: SIGSEGV (reaches
Alternate entry point:
Oj::StreamWriter.new(io, indent: 2147483647)- Vulnerable: SIGSEGV / abort (reaches
fill_indentviaoj_str_writer_push_value). - Fixed:
ArgumentErrorrejected byparse_options_cb. - Same root cause, same option-parsing path; not a distinct bypass.
- Vulnerable: SIGSEGV / abort (reaches
Alternate option path:
Oj.default_options = { indent: 2147483647 }; Oj.dump({a: 1})- Vulnerable: SIGSEGV (default options propagate to
Oj.dump). - Fixed:
ArgumentErrorrejected when setting default options. - Same root cause, same option-parsing path; not a distinct bypass.
- Vulnerable: SIGSEGV (default options propagate to
Bypass attempt: negative
:indent(indent: -1)- Vulnerable: no crash (
fill_indentchecks0 < out->indentbefore writing). - Fixed: no crash.
- Not a bypass; the guard already prevents the overflow.
- Vulnerable: no crash (
Bypass attempt: Bignum
:indent(indent: 10**40)- Vulnerable:
RangeErrorfromFIX2INT(range-checked conversion). - Fixed:
RangeError. - Not a bypass; Ruby's conversion rejects the value before it reaches the validation.
- Vulnerable:
No bypass or materially distinct variant was found. The fixed and latest versions reject the same inputs that crash the vulnerable version.
- Package/component affected:
ohler55/ojC extension, specifically thefill_indenthelper inext/oj/dump.hand the option parser inext/oj/oj.c. - Affected versions (as tested):
- Vulnerable:
4587e87(v3.17.1) - Fixed:
ec368db(v3.17.2) - Latest:
b0677dc(post-v3.17.2, clang-format only)
- Vulnerable:
- Risk level: Medium. On the vulnerable version, the alternate entry points cause the same deterministic native crash as the original
Oj.dumptrigger. On fixed/latest versions, the crash is prevented.
Impact Parity
- Disclosed/claimed maximum impact for the parent: Memory corruption / stack buffer overflow / crash.
- Reproduced impact from this variant run:
- Vulnerable: SIGSEGV on all four dump entry points (
Oj.dump,Oj::StringWriter,Oj::StreamWriter,Oj.default_options+Oj.dump). - Fixed/Latest: Clean
ArgumentErrorrejection for all four.
- Vulnerable: SIGSEGV on all four dump entry points (
- Parity:
nonefor the variant-as-bypass — no bypass was reproduced. The alternate triggers on the vulnerable version achievefullparity with the original crash impact. - Not demonstrated: Arbitrary code execution was not attempted; only the crash was reproduced.
Root Cause
The same root cause applies to all tested entry points: fill_indent in ext/oj/dump.h performs an unbounded memset whose size is the product of out->indent and the current depth. In the vulnerable version, out->indent can be set to INT_MAX through the public :indent option. The fixed version enforces MAX_INDENT at the single option-parsing choke point, so out->indent can never exceed 16 when it reaches fill_indent.
Because every tested entry point uses the same option parser, the fix closes the sink for all of them. There is no alternate code path that populates out->indent without going through parse_options_cb.
- Fix commit:
ec368dbe936ef0104b782e4b0f67b17d6c7276f7 - Vulnerable commit:
4587e87e23adc9a4163834dc8c9ba9d7206c6501 - Latest tested commit:
b0677dccb6d3e3dc260d19e1f1c2c3913f378afc
Reproduction Steps
Run the variant reproduction script:
bash bundle/vuln_variant/reproduction_steps.sh
The script:
- Ensures/clones the Oj repository into three separate worktrees under
bundle/artifacts/. - Checks out and builds the vulnerable (
4587e87), fixed (ec368db), and latest (b0677dc) commits. - Runs six test cases on each commit:
Oj.dump({a: 1}, indent: 2147483647)Oj::StringWriter.new(indent: 2147483647).push_array.push_value(1).pop_allOj::StreamWriter.new(StringIO.new, indent: 2147483647).push_array.push_value(1).pop_allOj.default_options = {indent: 2147483647}; Oj.dump({a: 1})Oj.dump({a: 1}, indent: -1)Oj.dump({a: 1}, indent: 10**40)
- Classifies each run as
segfault,rejected,ok, orunknown. - Prints a summary and exits
0if a bypass is detected (segfault on fixed/latest), otherwise exits1.
Expected evidence
bundle/logs/vuln_*: logs showing SIGSEGV / abort for the large-indent tests.bundle/logs/fixed_*andbundle/logs/latest_*: logs showingArgumentError: indent is limited to 16 characters.orRangeErrorfor the large-indent tests.bundle/logs/vuln_variant_reproduction_steps.log: complete driver output with the finalBYPASS_FOUND: falseline.
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) - Oj latest commit
b0677dc(post-v3.17.2) - C extension built directly with
extconf.rb+makein each checkout
Key excerpts from bundle/logs/vuln_variant_reproduction_steps.log
vuln_dump outcome: segfault
fixed_dump outcome: rejected
latest_dump outcome: rejected
vuln_string_writer outcome: segfault
fixed_string_writer outcome: rejected
latest_string_writer outcome: rejected
vuln_stream_writer outcome: segfault
fixed_stream_writer outcome: rejected
latest_stream_writer outcome: rejected
vuln_default_options outcome: segfault
fixed_default_options outcome: rejected
latest_default_options outcome: rejected
vuln_negative_indent outcome: ok
fixed_negative_indent outcome: ok
latest_negative_indent outcome: ok
vuln_bignum_indent outcome: rejected
fixed_bignum_indent outcome: rejected
latest_bignum_indent outcome: rejected
BYPASS_FOUND: false
Sample log: bundle/logs/vuln_string_writer.log
--- vuln_string_writer ---
-e:1:in `push_array': [BUG] Segmentation fault
...
Sample log: bundle/logs/fixed_string_writer.log
--- fixed_string_writer ---
-e:1:in `new': indent is limited to 16 characters. (ArgumentError)
...
Recommendations / Next Steps
- No bypass was found. The upstream fix is sufficient for the
:indentstack overflow. The recommended action is to apply the upstream patch (ec368db) and upgrade to Oj 3.17.2 or later. - Additional hardening: Add a defensive bounds check inside
fill_indentitself as a fail-safe. Even if the option parser is ever bypassed in the future,fill_indentshould refuse to write more thanout->end - out->curbytes. - Regression coverage: Extend the existing test suite to include
Oj::StringWriter,Oj::StreamWriter, andOj.default_optionswithindent: 2147483647, ensuring the validation remains effective for all entry points. - No code change required for this variant: Because the fix already covers all tested entry points, no additional patch is needed to address the variant search.
Additional Notes
- Idempotency: The script was run twice successfully from the same state. Both runs produced identical classifications: all vulnerable large-indent tests crashed, and all fixed/latest large-indent tests were rejected.
- No trust-boundary change: The alternate entry points (
StringWriter,StreamWriter,default_options) are all library APIs controlled by the same caller. They do not cross a different trust boundary, so they are not independent security variants even though they reach the same sink. - Negative/Bignum results: These attempts were deliberately included to test the robustness of the validation. Negative indent is harmless because of the existing
0 < out->indentguard; Bignum indent is rejected by Ruby'sFIX2INTconversion. Neither constitutes a bypass.
CVE-2026-54502 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-54502
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-54502
Upgrade oj · Ruby to 3.17.3 or later.
FAQ: CVE-2026-54502
How does the Oj :indent stack overflow crash occur?
Which versions of Oj are affected by CVE-2026-54502, and where is it fixed?
How severe is CVE-2026-54502?
How can I reproduce CVE-2026-54502?
References for CVE-2026-54502
Authoritative sources for CVE-2026-54502 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.