Skip to content

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.

REPRO-2026-00209 oj · Ruby Buffer Overflow Variant found Jul 2, 2026 CVE entry ↗ .txt
Severity
MEDIUM
CVSS
6.3
Confidence
HIGH
Reproduced in
18m 33s
Tool calls
184
Spend
$1.67
01 · Overview

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

02 · Severity & CVSS

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 threat level
6.3 / 10 CVSS base
Weakness CWE-121

Medium — meaningful risk under specific conditions. Schedule a fix in the normal cycle.

03 · Affected Versions

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
or 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
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-54502

Memory corruption — reproduced
  • reached the target end-to-end
  • crash observed
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

indent: 2147483647

Attack chain
  1. Oj.dump({a: 1}, indent: INT_MAX)
Runnable proof: reproduction_steps.sh
Captured evidence: fixed dumpfixed string writerfixed stream writerfixed default optionsfixed negative indentfixed bignum indent
Variants tested

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 449 events · 184 tool calls · 19 min
19 minDuration
184Tool calls
128Reasoning steps
449Events
2Dead-ends
Agent activity over 19 min
Support
27
Hypothesis
2
Repro
254
Judge
18
Variant
144
0:0018:33

Root Cause and Exploit Chain for CVE-2026-54502

Versions: Prior to 3.17.2 (vulnerable parent commit 4587e87; fix commit ec368db).

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

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

Variant Analysis & Alternative Triggers for CVE-2026-54502

Versions: versions (as tested):Fixed: ec368db (v3.17.2)

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:

  1. Alternate entry point: Oj::StringWriter.new(indent: 2147483647)

    • Vulnerable: SIGSEGV (reaches fill_indent via oj_str_writer_push_value / fill_indent).
    • Fixed: ArgumentError rejected by parse_options_cb.
    • Same root cause, but same option-parsing path; not a distinct bypass.
  2. Alternate entry point: Oj::StreamWriter.new(io, indent: 2147483647)

    • Vulnerable: SIGSEGV / abort (reaches fill_indent via oj_str_writer_push_value).
    • Fixed: ArgumentError rejected by parse_options_cb.
    • Same root cause, same option-parsing path; not a distinct bypass.
  3. Alternate option path: Oj.default_options = { indent: 2147483647 }; Oj.dump({a: 1})

    • Vulnerable: SIGSEGV (default options propagate to Oj.dump).
    • Fixed: ArgumentError rejected when setting default options.
    • Same root cause, same option-parsing path; not a distinct bypass.
  4. Bypass attempt: negative :indent (indent: -1)

    • Vulnerable: no crash (fill_indent checks 0 < out->indent before writing).
    • Fixed: no crash.
    • Not a bypass; the guard already prevents the overflow.
  5. Bypass attempt: Bignum :indent (indent: 10**40)

    • Vulnerable: RangeError from FIX2INT (range-checked conversion).
    • Fixed: RangeError.
    • Not a bypass; Ruby's conversion rejects the value before it reaches the validation.

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/oj C extension, specifically the fill_indent helper in ext/oj/dump.h and the option parser in ext/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)
  • Risk level: Medium. On the vulnerable version, the alternate entry points cause the same deterministic native crash as the original Oj.dump trigger. 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 ArgumentError rejection for all four.
  • Parity: none for the variant-as-bypass — no bypass was reproduced. The alternate triggers on the vulnerable version achieve full parity 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:

  1. Ensures/clones the Oj repository into three separate worktrees under bundle/artifacts/.
  2. Checks out and builds the vulnerable (4587e87), fixed (ec368db), and latest (b0677dc) commits.
  3. Runs six test cases on each commit:
    • Oj.dump({a: 1}, indent: 2147483647)
    • Oj::StringWriter.new(indent: 2147483647).push_array.push_value(1).pop_all
    • Oj::StreamWriter.new(StringIO.new, indent: 2147483647).push_array.push_value(1).pop_all
    • Oj.default_options = {indent: 2147483647}; Oj.dump({a: 1})
    • Oj.dump({a: 1}, indent: -1)
    • Oj.dump({a: 1}, indent: 10**40)
  4. Classifies each run as segfault, rejected, ok, or unknown.
  5. Prints a summary and exits 0 if a bypass is detected (segfault on fixed/latest), otherwise exits 1.
Expected evidence
  • bundle/logs/vuln_*: logs showing SIGSEGV / abort for the large-indent tests.
  • bundle/logs/fixed_* and bundle/logs/latest_*: logs showing ArgumentError: indent is limited to 16 characters. or RangeError for the large-indent tests.
  • bundle/logs/vuln_variant_reproduction_steps.log: complete driver output with the final BYPASS_FOUND: false 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)
  • Oj latest commit b0677dc (post-v3.17.2)
  • C extension built directly with extconf.rb + make in 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 :indent stack 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_indent itself as a fail-safe. Even if the option parser is ever bypassed in the future, fill_indent should refuse to write more than out->end - out->cur bytes.
  • Regression coverage: Extend the existing test suite to include Oj::StringWriter, Oj::StreamWriter, and Oj.default_options with indent: 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->indent guard; Bignum indent is rejected by Ruby's FIX2INT conversion. 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.

Event 1/40
0:001:20
0:00
session startedaccounts/fireworks/models/kimi-k2p7-code · CVE-2026-54502 · REPRO-20
0:08
0:10
web search
0:16
0:16
0:18
0:19
0:25
0:26
web search
0:29
0:32
web search
0:35
0:36
web search
0:41
0:42
web search
0:53
0:54
extract_facts
no facts extracted
0:55
0:55
0:55
supportrepro
1:15
1:17
1:17
1:17
1:19
1:19
1:19
1:20

Artifacts and Evidence for CVE-2026-54502

Scripts, logs, diffs, and output captured during the reproduction.

bundle/ticket.md0.7 KB
bundle/ticket.json1.1 KB
bundle/repro/validation_verdict.json0.6 KB
bundle/repro/runtime_manifest.json0.4 KB
bundle/logs/reproduction_steps.log3.9 KB
bundle/logs/vulnerable.log1.1 KB
bundle/logs/vulnerable.result0.0 KB
bundle/logs/fixed.log0.2 KB
bundle/logs/fixed.result0.0 KB
bundle/logs/vuln_variant_reproduction_steps.log10.2 KB
bundle/logs/vuln_dump.log1.2 KB
bundle/logs/vuln_dump.result0.0 KB
bundle/logs/fixed_dump.log0.3 KB
bundle/logs/fixed_dump.result0.0 KB
bundle/logs/latest_dump.log0.3 KB
bundle/logs/latest_dump.result0.0 KB
bundle/logs/vuln_string_writer.log1.2 KB
bundle/logs/vuln_string_writer.result0.0 KB
bundle/logs/fixed_string_writer.log0.3 KB
bundle/logs/fixed_string_writer.result0.0 KB
bundle/logs/latest_string_writer.log0.3 KB
bundle/logs/latest_string_writer.result0.0 KB
bundle/logs/vuln_stream_writer.log1.2 KB
bundle/logs/vuln_stream_writer.result0.0 KB
bundle/logs/fixed_stream_writer.log0.4 KB
bundle/logs/fixed_stream_writer.result0.0 KB
bundle/logs/latest_stream_writer.log0.4 KB
bundle/logs/latest_stream_writer.result0.0 KB
bundle/logs/vuln_default_options.log1.2 KB
bundle/logs/vuln_default_options.result0.0 KB
bundle/logs/fixed_default_options.log0.3 KB
bundle/logs/fixed_default_options.result0.0 KB
bundle/logs/latest_default_options.log0.3 KB
bundle/logs/latest_default_options.result0.0 KB
bundle/logs/vuln_negative_indent.log0.0 KB
bundle/logs/vuln_negative_indent.result0.0 KB
bundle/logs/fixed_negative_indent.log0.0 KB
bundle/logs/fixed_negative_indent.result0.0 KB
bundle/logs/latest_negative_indent.log0.0 KB
bundle/logs/latest_negative_indent.result0.0 KB
bundle/logs/vuln_bignum_indent.log0.3 KB
bundle/logs/vuln_bignum_indent.result0.0 KB
bundle/logs/fixed_bignum_indent.log0.3 KB
bundle/logs/fixed_bignum_indent.result0.0 KB
bundle/logs/latest_bignum_indent.log0.3 KB
bundle/logs/latest_bignum_indent.result0.0 KB
bundle/vuln_variant/patch_analysis.md7.9 KB
bundle/vuln_variant/validation_verdict.json0.8 KB
bundle/vuln_variant/variant_manifest.json2.7 KB
bundle/vuln_variant/source_identity.json0.7 KB
bundle/vuln_variant/runtime_manifest.json1.2 KB
bundle/vuln_variant/root_cause_equivalence.json1.6 KB
bundle/repro/reproduction_steps.sh8.6 KB
bundle/repro/rca_report.md6.7 KB
bundle/vuln_variant/reproduction_steps.sh7.9 KB
bundle/vuln_variant/rca_report.md9.8 KB
08 · How to Fix

How to Fix CVE-2026-54502

Upgrade oj · Ruby to 3.17.3 or later.

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-54502 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-54502

How does the Oj :indent stack overflow crash occur?

A developer (or attacker who controls the :indent argument passed to Oj.dump) supplies :indent => 2147483647. fill_indent's unchecked memset call then writes 2 GB of space characters into the small stack-allocated output buffer, corrupting the stack and crashing the Ruby process with a SIGSEGV.

Which versions of Oj are affected by CVE-2026-54502, and where is it fixed?

Affected versions are < 3.17.2 (per the reporting user; the GitHub advisory lists affected < 3.17.2, patched 3.17.3). It is fixed in 3.17.3.

How severe is CVE-2026-54502?

Medium severity -- a deterministic native crash (denial of service) via stack corruption in any process that lets external input control the :indent option passed to Oj.dump.

How can I reproduce CVE-2026-54502?

Download the verified script from this page and run it in an isolated environment against Oj before the fix. It calls Oj.dump with :indent set to INT_MAX and shows the resulting stack-buffer overflow crashing the Ruby process with a SIGSEGV.
11 · References

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.