Skip to content

CVE-2026-54500: Verified Repro With Script Download

CVE-2026-54500: Oj Ruby gem uninitialized stack memory leak via long JSON keys

CVE-2026-54500 is verified against the affected target. This medium reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00208.

REPRO-2026-00208 Variant found Jul 2, 2026 CVE entry ↗ .txt
Severity
MEDIUM
CVSS
5.3
Confidence
HIGH
Reproduced in
20m 41s
Tool calls
153
Spend
$2.43
01 · Overview

What Is CVE-2026-54500?

CVE-2026-54500 is a medium-severity information disclosure vulnerability (CWE-125 out-of-bounds read, CWE-908 uninitialized resource) in the Oj Ruby JSON gem. Parsing a JSON object with a key 254 bytes or longer in :object mode leaks uninitialized process stack memory. Pruva reproduced it (reproduction REPRO-2026-00208).

02 · Severity & CVSS

CVE-2026-54500 Severity & CVSS Score

CVE-2026-54500 is rated medium severity, with a CVSS base score of 5.3 out of 10.

MEDIUM threat level
5.3 / 10 CVSS base
Weakness CWE-125 (Out-of-bounds Read), CWE-908 (Uninitialized Resource) — Out-of-bounds Read

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

How to Reproduce CVE-2026-54500

$ pruva-verify REPRO-2026-00208
or curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00208/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-54500

Information disclosure — reproduced
  • reached the target end-to-end
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

JSON object key >= 254 bytes (300 'A' chars) supplied to Oj.load in :object mode

Attack chain
  1. Oj.load(json, mode: :object)
  2. object.c:oj_set_obj_ivar
  3. intern.c:oj_attr_intern
  4. cache.c:cache_intern (bypasses cache, len>=35)
  5. intern.c:form_attr long-key path: rb_intern3(uninitialized buf, len+1) reads stack memory
Variants tested

No bypass or distinct alternate trigger found. The fix (intern.c buf->b, commit bbde91a, v3.17.3) fully closes the only reachable copy of the form_attr uninitialized-stack-memory-read sink (Oj.load :object mode -> object.c -> oj_attr_intern -> intern.c form_attr). The duplicate copy in usual.c was already fixed earlie…

How the agent worked 366 events · 153 tool calls · 21 min
21 minDuration
153Tool calls
99Reasoning steps
366Events
Agent activity over 21 min
Support
17
Repro
144
Judge
28
Variant
173
0:0020:41

Root Cause and Exploit Chain for CVE-2026-54500

Versions: Oj 0.0.1 – 3.17.2 (fixed in 3.17.3)

Oj (Optimized JSON), a Ruby gem with a C extension, contains an uninitialized stack memory read in ext/oj/intern.c's form_attr() function. When Oj.load parses a JSON object in :object mode whose key is 254 bytes or longer, the long-key code path allocates a heap buffer b, correctly fills it with the attribute name, then frees it — but passes the uninitialized 256-byte stack buffer buf (not b) to rb_intern3(). Ruby therefore interns len + 1 bytes of uninitialized stack memory (and, for keys ≥ 256 bytes, reads out of bounds past buf). The leaked bytes surface to the caller via the produced Symbol or via the EncodingError message raised when the stack garbage is not valid UTF-8, disclosing process stack contents. The fix is a single-character change: rb_intern3(buf, ...)rb_intern3(b, ...).

  • Package/component: ohler55/oj — C extension, ext/oj/intern.c, form_attr()
  • Affected versions: Oj 0.0.1 – 3.17.2 (fixed in 3.17.3)
  • Risk level: Medium
  • Consequences: Information disclosure of process stack memory. An attacker who controls the JSON input (a key ≥ 254 bytes) can cause Oj.load to read and surface uninitialized stack bytes. The leak is observable through the EncodingError exception message (which embeds the invalid bytes) or through the produced Symbol object. The exact bytes and message length vary between process invocations, confirming the source is uninitialized (non-deterministic) memory.

Impact Parity

  • Disclosed/claimed maximum impact: Uninitialized stack memory read / out-of-bounds read, leaking process stack contents via Symbol or EncodingError message.
  • Reproduced impact from this run: Uninitialized stack memory read confirmed. Every vulnerable run raised an EncodingError whose message contained 1262–1423 bytes of non-input (leaked stack) data, with message lengths varying across runs (1276–1432 bytes). The fixed version produced the correct, deterministic attribute name with zero leaked bytes.
  • Parity: full — the disclosed information-disclosure symptom (uninitialized stack memory surfacing via the EncodingError message, with per-run variation) was reproduced exactly, and the negative control on the fixed commit confirmed the fix.
  • Not demonstrated: No code execution was claimed or demonstrated; this is an information-disclosure / memory-read bug, not a code-execution vulnerability.

Root Cause

In ext/oj/intern.c, form_attr(const char *str, size_t len) converts a JSON object key into a Ruby attribute ID (interned symbol). It declares a 256-byte stack buffer buf (uninitialized) and branches on key length:

static VALUE form_attr(const char *str, size_t len) {
    char buf[256];                              // UNINITIALIZED

    if (sizeof(buf) - 2 <= len) {               // long-key path: len >= 254
        char *b = OJ_R_ALLOC_N(char, len + 2);  // heap buffer
        ID    id;
        // ... b is filled correctly with '@' + key + '\0' ...
        id = rb_intern3(buf, len + 1, oj_utf8_encoding);  // BUG: reads `buf`, not `b`
        OJ_R_FREE(b);
        return id;
    }
    // short-key path: buf IS properly filled before use (correct)
    ...
    return (VALUE)rb_intern3(buf, len + 1, oj_utf8_encoding);
}

In the long-key path, b is the correctly-populated heap buffer, but rb_intern3 is called with buf — the uninitialized stack buffer. rb_intern3 reads len + 1 bytes from buf. When len >= 256, this also reads out of bounds past the 256-byte buf. The bytes are interned as a symbol; if they are not valid UTF-8, Ruby raises an EncodingError whose message includes the offending bytes, leaking them to the caller.

This is a duplicate of an earlier fix in ext/oj/usual.c that was missed in intern.c.

Call path: Oj.load(json, mode: :object)object.c:oj_set_obj_ivar()intern.c:oj_attr_intern()cache.c:cache_intern()intern.c:form_attr(). Since CACHE_MAX_KEY is 35, keys ≥ 35 bytes bypass the cache and call form_attr directly every time, so the uninitialized read occurs on every invocation with a long key.

Fix commit: bbde91a679728f94c4492ebc3683f4fa3309049f ("Fix intern.c and fast.c (#1015)") — changes rb_intern3(buf, len + 1, oj_utf8_encoding) to rb_intern3(b, len + 1, oj_utf8_encoding) in the long-key path of form_attr().

Reproduction Steps

  1. Reference: bundle/repro/reproduction_steps.sh (self-contained, idempotent).
  2. What the script does:
    • Installs Ruby + build tools, clones (or reuses) ohler55/oj.
    • Checks out the vulnerable commit 495cc38 (v3.17.2, parent of the fix), builds the C extension via ruby extconf.rb && make.
    • Runs Oj.load('{"^o":"Oj::Bag","AAA...300...AAA":1}', mode: :object) in 6 separate Ruby processes. The ^o:Oj::Bag marker creates a non-Hash object so that oj_set_obj_ivaroj_attr_internform_attr is invoked.
    • Checks out the fixed commit bbde91a, rebuilds, and runs the same probe 6 times as a negative control.
    • Compares results, writes runtime_manifest.json, and exits 0 if confirmed.
  3. Expected evidence:
    • Vulnerable: all runs raise EncodingError; message lengths vary per run (1276–1432 bytes), with 1262–1423 non-A (leaked stack) bytes.
    • Fixed: all runs return an Oj::Bag with a single 301-byte instance variable @AAA... (0x40 + 300×0x41), deterministic across all runs.

Evidence

  • Log: bundle/logs/reproduction_steps.log — full build + probe transcript.
  • Vulnerable outcomes: bundle/logs/vuln_outcomes.txt
  • Fixed outcomes: bundle/logs/fixed_outcomes.txt
  • Message-length variation: bundle/logs/vuln_msg_lengths.txt
  • Probe script: bundle/repro/probe.rb
  • Runtime manifest: bundle/repro/runtime_manifest.json
Key excerpts (from the second verification run)

Vulnerable (commit 495cc38, v3.17.2) — all 6 runs leak:

[vuln run 1] encoding_error   MSG_LEN=1348  NON_A_BYTES=1339
[vuln run 2] encoding_error   MSG_LEN=1349  NON_A_BYTES=1341
[vuln run 3] encoding_error   MSG_LEN=1350  NON_A_BYTES=1343
[vuln run 4] encoding_error   MSG_LEN=1276  NON_A_BYTES=1262
[vuln run 5] encoding_error   MSG_LEN=1432  NON_A_BYTES=1423
[vuln run 6] encoding_error   MSG_LEN=1368  NON_A_BYTES=1343

The EncodingError message begins invalid symbol in encoding UTF-8 :" followed by Ruby \xNN escapes of the leaked stack bytes (e.g. \xB8\xFF, \xD8\xFF, \xC0\xFF) — these are pointers/binary data, not the 0x41 (A) input bytes. The message length varies across runs (1348–1432), which is impossible for deterministic, initialized data and confirms the source is uninitialized stack memory.

Fixed (commit bbde91a) — all 6 runs clean:

[fixed run 1] parsed  IVAR_LEN=301  CORRECT_ATTR=true  FIRST_BYTES=40414141...
[fixed run 2] parsed  IVAR_LEN=301  CORRECT_ATTR=true  FIRST_BYTES=40414141...
... (identical for all 6 runs)

FIRST_BYTES = 40 (@) + 41 (A) repeated — the correct, deterministic attribute name. No EncodingError, no leaked bytes.

Environment
  • Ruby 3.3.8 (x86_64-linux-gnu), GCC 15.2.0, Ubuntu.
  • Oj built from source at vulnerable commit 495cc38 and fixed commit bbde91a.

Recommendations / Next Steps

  • Upgrade to Oj 3.17.3+ which contains the one-character fix.
  • Audit ext/oj/usual.c and any other copies of the form_attr pattern for the same buf/b confusion (this was already a duplicate of a usual.c fix).
  • Add a regression test that parses a JSON object with a ≥ 254-byte key in :object mode and asserts the resulting attribute name matches the input.
  • Consider compiling with -ftrivial-auto-var-init=pattern to make uninitialized reads more visible in CI, and enabling MSan/ASan in the test suite.

Additional Notes

  • Idempotency: The script was run twice consecutively; both runs exited 0 with CONFIRMED=true. The script cleans all build artifacts between vulnerable/fixed builds (git clean -fdx ext/oj lib/oj) and uses a manual extconf.rb + make flow (avoiding rake compile, which loads bundler and can interfere with the git checkout state).
  • Key-length boundary: The bug triggers at len >= 254 (sizeof(buf) - 2 = 254). At len >= 256 the read also goes out of bounds past the 256-byte buf. The reproduction uses a 300-byte key to exercise both the uninitialized read and the OOB read.
  • Cache bypass: Because CACHE_MAX_KEY = 35, the 300-byte key bypasses the attribute cache entirely, so form_attr is called fresh on every invocation — maximizing the observable per-run variation.

Variant Analysis & Alternative Triggers for CVE-2026-54500

Versions: versions (as tested): Oj 3.17.2 (495cc38) — vulnerable via :object

No bypass or materially distinct alternate trigger was found. CVE-2026-54500 is an uninitialized stack-memory read in ext/oj/intern.c's form_attr() long-key path (rb_intern3(buf, ...) should be rb_intern3(b, ...)). The fix (commit bbde91a, v3.17.3) is a single-character change that closes the only code path that reaches this sink: Oj.load(json, mode: :object)object.c:oj_set_obj_ivar()intern.c:oj_attr_intern()cache.c:cache_intern()intern.c:form_attr(). An exhaustive empirical sweep of every Oj parse mode (:object, :compat, :rails, :strict, :null, :wab, :custom) plus the newer Oj::Parser API (:usual with object creation, :usual Hash, symbol-cached) — each tested with a 300-byte key on both the vulnerable commit (495cc38, v3.17.2) and the fixed commit (bbde91a, v3.17.3) — confirms that only :object mode leaks, and only on the vulnerable version. The duplicate copy of the same pattern in usual.c was already fixed earlier (ec368db, #1014, an ancestor of v3.17.2), so :compat/:rails and the newer parser were never vulnerable to this specific bug on v3.17.2. The bbde91a commit also bundles an unrelated fast.c depth-overflow fix (doc_each_child) which is a separate bug, not a variant of CVE-2026-54500.

Fix Coverage / Assumptions

  • Invariant the fix relies on: The only way an attacker-controlled JSON key of length ≥ 254 reaches the uninitialized-stack-buffer read is via intern.c:form_attr() 's long-key branch. The fix changes that one rb_intern3(buf, len + 1, ...) to rb_intern3(b, len + 1, ...) so the correctly-populated heap buffer b is interned instead of the uninitialized stack buffer buf.
  • Code path explicitly covered: Oj.load(..., mode: :object)object.c oj_set_obj_ivaroj_attr_intern (intern.c:145) → cache_intern (cache.c:324, threshold CACHE_MAX_KEY = 35) → intern.c:form_attr long-key path (len ≥ 254).
  • What the fix does NOT cover (and why that is OK here):
    • usual.c has its own static form_attr() with the identical long-key pattern. It is not touched by bbde91a, but it was already fixed in ec368db (#1014 "Fix stack limits / Fix extreme key length bug"), which is an ancestor of v3.17.2. Verified: git show 495cc38:ext/oj/usual.c already contains rb_intern3(b, ...).
    • :compat/:rails modes dispatch to oj_compat_parse (compat.c), which uses oj_calc_hash_key for Hash keys and json_create for object creation — it never calls form_attr or oj_attr_intern.
    • :strict/:null modes (strict.c) intern keys with rb_intern3(parent->key, parent->klen, ...) directly from the parsed key buffer (no stack buffer).
    • :wab (wab.c) uses oj_sym_internform_sym (builds a Ruby String first; no stack buffer).
    • :custom (custom.c) uses oj_calc_hash_key for Hash keys.
    • The newer Oj::Parser API accumulates keys in a dynamic p->key buffer, copies them into a Key struct (kp->buf for short, heap kp->key for long — both properly filled by push_key), and for object creation reaches usual.c's form_attr via get_attr_idcache_intern(d->attr_cache) (already fixed). Its :object mode is unimplemented (// TBD placeholder, parser.c:1263).

Variant / Alternate Trigger

No bypass or alternate trigger was confirmed. The following candidate entry points were tested empirically (4 processes each, on both vulnerable and fixed versions):

Mode / Entry point Reaches intern.c form_attr? Reaches usual.c form_attr? Vuln leak? Fixed leak?
Oj.load :object (^o:Oj::Bag) Yes (only path) No Yes (encoding_error, per-run variation) No (correct)
Oj.load :compat (json_class / ^o) No (uses oj_calc_hash_key) No (compat.c) No No
Oj.load :compat (plain Hash) No No No No
Oj.load :rails No (dispatches to oj_compat_parse) No No No
Oj.load :strict No (rb_intern3 from parsed key) No No No
Oj.load :null No No No No
Oj.load :wab No (oj_sym_intern/form_sym) No No No
Oj.load :custom No (oj_calc_hash_key) No No No
Oj::Parser.new(:usual) + create_id (object) No Yes (already fixed pre-v3.17.2) No No
Oj::Parser.new(:usual) (Hash) No No No No
Oj::Parser.new(:usual) + create_id + cache_keys No Yes (already fixed) No No
Oj::Parser.new(:object) N/A — unimplemented (// TBD) N/A N/A N/A

The "alternate entry point that reaches a different unfixed copy of the same sink" candidate (usual.c form_attr) was ruled out because that copy was already fixed before the vulnerable version was tagged. There is no third copy of the form_attr long-key pattern in the codebase (grep for rb_intern3(b, / rb_intern3(buf, / char buf[256]

  • OJ_R_ALLOC_N(char, len confirms only intern.c and usual.c).
  • Package/component: ohler55/oj — C extension, ext/oj/intern.c, form_attr()
  • Affected versions (as tested): Oj 3.17.2 (495cc38) — vulnerable via :object mode only; Oj 3.17.3 (bbde91a) — fixed.
  • Risk level: Medium (information disclosure of process stack memory).
  • Consequences: An attacker controlling JSON input with a key ≥ 254 bytes can cause Oj.load(..., mode: :object) to intern len+1 bytes of uninitialized stack memory (and, for keys ≥ 256, read out of bounds past the 256-byte buf). The leaked bytes surface via the produced Symbol/instance-variable name or via the EncodingError message when the garbage is not valid UTF-8.

Impact Parity

  • Disclosed/claimed maximum impact: Uninitialized stack memory read / OOB read, leaking process stack contents via Symbol or EncodingError message.
  • Reproduced impact from this variant run: The original :object-mode leak was re-confirmed on the vulnerable version (EncodingError, MSG_LEN varying 1271–1432 across runs — proving non-deterministic uninitialized memory, 1250–1426 non-A leaked bytes). No other mode reproduced any leak on either version.
  • Parity: none for the variant search — no additional impact path was found beyond the already-known :object-mode path, which is fully closed by the fix.
  • Not demonstrated: No code execution (information-disclosure bug only).

Root Cause

The root cause is a buf/b variable confusion in intern.c:form_attr(). When a key is ≥ 254 bytes (sizeof(buf) - 2 <= len), the function allocates a heap buffer b, correctly fills it with '@' + key + '\0', then erroneously passes the uninitialized 256-byte stack buffer buf to rb_intern3() instead of b. This is a duplicate of a bug that was previously fixed in usual.c (commit ec368db, #1014) but missed in intern.c. The fix commit bbde91a (#1015, "Fix intern.c and fast.c") corrects intern.c (bufb) and, separately, adds a MAX_STACK depth check to fast.c:doc_each_child() (an unrelated depth-overflow bug, not a variant of this CVE).

Because oj_attr_intern (the sole caller chain to intern.c:form_attr) is invoked only from object.c:oj_set_obj_ivar, and every other mode uses different key-interning mechanisms that do not employ the stack-buffer-then-heap-fallback pattern, the fix completely eliminates the reachable vulnerable sink.

Fix commit: bbde91a679728f94c4492ebc3683f4fa3309049f Earlier duplicate fix (usual.c): ec368dbe936ef0104b782e4b0f67b17d6c7276f7

Reproduction Steps

  1. Reference: bundle/vuln_variant/reproduction_steps.sh (self-contained, idempotent).
  2. What the script does:
    • Resolves the durable project cache and reuses the ohler55/oj clone.
    • Builds the vulnerable commit 495cc38 (v3.17.2) via ruby extconf.rb && make.
    • Runs probe_variant.rb for 11 entry points (all Oj.load modes + newer Oj::Parser API variants), 4 separate processes each, with a 300-byte key.
    • Builds the fixed commit bbde91a (v3.17.3) and repeats the same 44 probe runs.
    • Emits a variant/bypass matrix and a verdict, writes runtime_manifest.json, and restores the repo to the fixed commit.
  3. Expected evidence:
    • Vulnerable: only object mode produces OUTCOME=encoding_error with per-run MSG_LEN variation (1271–1432); all other modes OUTCOME=correct.
    • Fixed: all 11 modes OUTCOME=correct (ivar_len=301 / key_len=300, deterministic 0x40+0x41… / 0x41… bytes); zero encoding_error, zero leak.

Evidence

  • Log: bundle/logs/vuln_variant_repro.log — full build + sweep transcript.
  • Vulnerable outcomes: bundle/logs/vuln_variant_outcomes.txt
  • Fixed outcomes: bundle/logs/fixed_variant_outcomes.txt
  • Probe script: bundle/vuln_variant/probe_variant.rb
  • Runtime manifest: bundle/vuln_variant/runtime_manifest.json
  • Fixed/vulnerable version identity: bundle/logs/vuln_variant/fixed_version.txt
Key excerpts (second verification run)

Vulnerable object mode — leaks, per-run variation (uninitialized memory):

[vuln object run 1] encoding_error MSG_LEN=1271 NON_A=1250
[vuln object run 2] encoding_error MSG_LEN=1343 NON_A=1333
[vuln object run 3] encoding_error MSG_LEN=1272 NON_A=1259
[vuln object run 4] encoding_error MSG_LEN=1351 NON_A=1317
[vuln compat_obj run 1] correct hash_key KEY_LEN=300 FIRST=41414141…

Fixed — all modes clean:

[fixed object run 1] correct ivar IVAR_LEN=301 FIRST=40414141…
[fixed object run 2] correct ivar IVAR_LEN=301 FIRST=40414141…
… (identical for all 11 modes, all 4 runs)
Environment
  • Ruby 3.3.8 (x86_64-linux-gnu), GCC 15.2.0, Ubuntu.
  • Oj built from source at vulnerable 495cc38 and fixed bbde91a.

Recommendations / Next Steps

  • No additional fix is required for CVE-2026-54500; the one-character bufb change in intern.c fully closes the only reachable sink. The Coding stage should ensure this change is present and consider the items below as defense-in-depth.
  • Consolidate the duplicate form_attr: intern.c and usual.c each carry their own copy of form_attr. Unifying them into a single shared function would prevent future copy-paste divergence (this exact class of bug already occurred twice).
  • Add a regression test parsing a JSON object with a ≥ 254-byte key in :object mode asserting the resulting attribute name equals the input (deterministic).
  • Harden the usual.c copy parity: note that intern.c's form_attr handles a ~-prefix case that usual.c's does not — a behavioral divergence worth aligning.
  • Consider -ftrivial-auto-var-init=pattern and MSan/ASan in CI to make uninitialized reads deterministic and caught automatically.
  • The Oj::Parser.new(:object) placeholder (// TBD) should be implemented or removed to avoid confusion; currently it silently produces a non-functional parser.

Additional Notes

  • Idempotency: The script was run twice consecutively; both runs completed fully (exit 1 = no bypass found) without crashing, and restored the repo to bbde91a.
  • Threat model: SECURITY.md is a generic template (supported versions + reporting process) with no explicit exclusions for memory-safety classes; the bug is within scope as an information-disclosure vulnerability reachable from attacker-controlled JSON input.
  • Scope discipline: The fast.c doc_each_child depth-overflow fix bundled in the same commit (bbde91a) is a separate bug (different root cause, different sink, different impact class) and was deliberately not claimed as a variant of CVE-2026-54500, per the rule that separate bugs are not bypasses of an unrelated fix.
  • Bounded search justification: Fewer than 3 "real" bypass candidates exist because static analysis (mode dispatch in oj.c:1255-1261, rb_intern3/form_attr call-site enumeration, usual.c fix ancestry) proves there is exactly one reachable copy of the vulnerable sink (intern.c form_attr) and one already-fixed copy (usual.c form_attr). All remaining modes use key-interning paths without the stack-buffer-then-heap-fallback pattern. The 11-mode empirical sweep confirms this exhaustively.

CVE-2026-54500 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:000:58
0:00
session startedaccounts/fireworks/routers/glm-5p2-fast · CVE-2026-54500 · REPRO-20
0:03
0:04
web search
0:07
web search
0:08
0:10
0:11
0:13
0:14
0:26
0:26
extract_facts
no facts extracted
0:27
0:27
0:27
supportrepro
0:29
0:29
0:30
0:30
0:32
0:32
0:37
0:37
$ls -la /data/pruva/project-cache/dc167dac-a6d2-43f6-837d-84c9d571596f/ 2>/dev/null && echo "---REPO---" && ls -la /data/pruva/project-cache/dc167dac-a6d2-43f6-837d-84c9d571596f/repo/ 2>/dev/null && echo "---MIRRORS---" && ls -la /data/pruva/project-cache/dc167dac-a6d2-43f6-837d-84c9d571596f/repo-mirrors/ 2>/dev/null
total 0
drwxr-xr-x 1 vscode vscode   24 Jul  2 17:17 .
drwxr-xr-x 1 vscode vscode 1390 Jul  2 17:17 ..
drwxr-xr-x 1 vscode vscode    0 Jul  2 17:17 repo-mirrors
---REPO---
0:37
$which ruby; ruby --version 2>/dev/null; which gem; gem --version 2>/dev/null; which bundle 2>/dev/null; which gcc; gcc --version 2>/dev/null | head -1
/usr/bin/gcc
gcc (Ubuntu 15.2.0-16ubuntu1) 15.2.0
0:42
0:46
web search
0:52
$sudo apt-get update -qq 2>&1 | tail -3; echo "===INSTALL RUBY==="; sudo apt-get install -y ruby ruby-dev build-essential 2>&1 | tail -15
6.9s
===INSTALL RUBY===
Setting up ruby-webrick (1.9.2-1) ...
Setting up libjs-jquery (3.7.1+dfsg+~3.5.33-1build1) ...
Setting up ruby-did-you-mean (2.0.0-1) ...
Setting up ruby-xmlrpc (0.3.3-2build1) ...
Setting up ruby3.3 (3.3.8-2ubuntu3) ...
Setting up libruby:amd64 (1:3.3build1) ...
Setting up ruby (1:3.3build1) ...
0:58

Artifacts and Evidence for CVE-2026-54500

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

bundle/ticket.md1.0 KB
bundle/ticket.json1.4 KB
bundle/repro/probe.rb1.4 KB
bundle/repro/runtime_manifest.json0.7 KB
bundle/repro/validation_verdict.json0.9 KB
bundle/logs/reproduction_steps.log17.5 KB
bundle/logs/vuln_outcomes.txt1.0 KB
bundle/logs/vuln_msg_lengths.txt0.0 KB
bundle/logs/vuln_run1.bin1.3 KB
bundle/logs/vuln_run2.bin1.3 KB
bundle/logs/vuln_run3.bin1.3 KB
bundle/logs/vuln_run4.bin1.2 KB
bundle/logs/vuln_run5.bin1.4 KB
bundle/logs/vuln_run6.bin1.3 KB
bundle/logs/vuln_leak_count0.0 KB
bundle/logs/vuln_err_count0.0 KB
bundle/logs/vuln_correct_count0.0 KB
bundle/logs/fixed_outcomes.txt0.8 KB
bundle/logs/fixed_msg_lengths.txt0.0 KB
bundle/logs/fixed_run1.bin0.3 KB
bundle/logs/fixed_run2.bin0.3 KB
bundle/logs/fixed_run3.bin0.3 KB
bundle/logs/fixed_run4.bin0.3 KB
bundle/logs/fixed_run5.bin0.3 KB
bundle/logs/fixed_run6.bin0.3 KB
bundle/logs/fixed_leak_count0.0 KB
bundle/logs/fixed_err_count0.0 KB
bundle/logs/fixed_correct_count0.0 KB
bundle/logs/vuln_variant_repro.log13.6 KB
bundle/logs/vuln_variant_outcomes.txt9.7 KB
bundle/logs/fixed_variant_outcomes.txt9.2 KB
bundle/logs/vuln_variant/fixed_version.txt0.1 KB
bundle/vuln_variant/probe_variant.rb5.1 KB
bundle/vuln_variant/runtime_manifest.json0.8 KB
bundle/vuln_variant/patch_analysis.md6.3 KB
bundle/vuln_variant/variant_manifest.json4.0 KB
bundle/vuln_variant/validation_verdict.json3.7 KB
bundle/vuln_variant/source_identity.json1.5 KB
bundle/vuln_variant/root_cause_equivalence.json2.4 KB
bundle/repro/reproduction_steps.sh13.3 KB
bundle/repro/rca_report.md8.6 KB
bundle/vuln_variant/reproduction_steps.sh10.9 KB
bundle/vuln_variant/rca_report.md12.2 KB
08 · How to Fix

How to Fix CVE-2026-54500

Coming soon

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

10 · FAQ

FAQ: CVE-2026-54500

How does the Oj stack-leak issue surface?

The leaked stack bytes reach the caller either as the produced Ruby Symbol object or via the EncodingError message raised when the garbage bytes are not valid UTF-8, disclosing process stack contents. The exact bytes vary between process invocations, confirming the source is uninitialized memory.

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

Oj versions 0.0.1 through 3.17.2 are affected. It is fixed in 3.17.3, via a single-character change: rb_intern3(buf, ...) is corrected to rb_intern3(b, ...).

How severe is CVE-2026-54500?

Medium severity — an information disclosure of process stack memory, observable through the EncodingError message or the resulting Symbol, rather than code execution.

How can I reproduce CVE-2026-54500?

Download the verified script from this page and run it in an isolated environment against Oj <= 3.17.2. Call Oj.load(json, mode: :object) with a JSON object whose key is 254 bytes or longer and observe leaked uninitialized stack bytes in the resulting Symbol or EncodingError message; confirm 3.17.3 no longer leaks.
11 · References

References for CVE-2026-54500

Authoritative sources for CVE-2026-54500 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.