VYPR
High severity8.7GHSA Advisory· Published Jun 19, 2026· Updated Jun 19, 2026

Oj: Use-After-Free in Oj::Parser array_class/hash_class GC Marking

CVE-2026-54901

Description

Summary

Oj::Parser in usual mode does not mark array_class and hash_class references during garbage collection. If GC runs after the class is assigned but before a parse, the class object is reclaimed, leaving the parser holding a dangling VALUE. The subsequent parse call dereferences the freed object, producing a segfault.

Version

  • Software: oj gem
  • Affected: all versions with ext/oj/usual.c / ext/oj/parser.c
  • Latest tested: 3.17.1 (confirmed present)

Details

The parser_mark function in ext/oj/parser.c is registered as the GC mark callback for the parser's TypedData. If array_class (stored as d->array_class in the Usual struct) is not passed to rb_gc_mark, the GC does not know it is referenced and may collect it.

When close_array_class (usual.c:405) later calls rb_funcallv on the collected class VALUE, it accesses freed memory, crashing at RIP: 0x7f... / 0x0000000000000000.

Crash output: `` array_class finalized about to parse [BUG] Segmentation fault at 0x0000000000000000 close_array_class+0x194 /ext/oj/usual.c:405 parse+0x17b3 /ext/oj/parser.c:715 parser_parse+0x10b /ext/oj/parser.c:1408 RIP: 0x7fd1b46d68b7 RBP: 0x0000000000000000 ``

Reproduce

require 'oj'
p = Oj::Parser.new(:usual,
  array_class: (ac = Class.new { def <<(_x); end }))
ObjectSpace.define_finalizer(ac, proc { warn 'array_class finalized' })
ac = nil
GC.start(full_mark: true, immediate_sweep: true)  # collect the class
p.parse('[1]')  # segfault

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Affected products

1

Patches

Vulnerability mechanics

Root cause

"Missing `rb_gc_mark` calls on `array_class` and `hash_class` in the parser's GC mark callback, allowing the garbage collector to reclaim objects still referenced by the parser."

Attack vector

An attacker who can control the `array_class` or `hash_class` option passed to `Oj::Parser.new` can trigger a use-after-free. The attacker creates a custom class, assigns it as `array_class`, drops all Ruby references to it, and forces a full GC run before calling `parse`. Because the parser's mark function does not protect the class reference [ref_id=1][ref_id=2], the GC reclaims the class object, and the subsequent `parse` call dereferences the dangling VALUE, producing a segfault. No network access or authentication is required; the attacker only needs the ability to instantiate an `Oj::Parser` with a custom class and trigger GC.

Affected code

The bug resides in `ext/oj/parser.c` (the `parser_mark` function) and `ext/oj/usual.c` (the `close_array_class` function at line 405). The `parser_mark` callback fails to call `rb_gc_mark` on `d->array_class` (and `d->hash_class`), so the Ruby GC does not know those VALUEs are still referenced. When `close_array_class` later invokes `rb_funcallv` on the collected class, it dereferences freed memory, causing a segfault.

What the fix does

The advisory states that the `parser_mark` function in `ext/oj/parser.c` must call `rb_gc_mark` on `d->array_class` and `d->hash_class` so the GC knows those VALUEs are still live [ref_id=1][ref_id=2]. Without that call, the GC treats the class objects as unreachable and frees them. The fix adds the missing `rb_gc_mark` invocations, ensuring the custom class objects are kept alive as long as the parser holds a reference to them. No patch diff is included in the bundle, but the remediation is clearly described.

Preconditions

  • inputThe attacker must be able to instantiate an Oj::Parser with a custom array_class or hash_class option.
  • configThe attacker must trigger a full GC (e.g., GC.start) after dropping all Ruby references to the custom class and before calling parse.

Reproduction

```ruby require 'oj' p = Oj::Parser.new(:usual, array_class: (ac = Class.new { def <<(_x); end })) ObjectSpace.define_finalizer(ac, proc { warn 'array_class finalized' }) ac = nil GC.start(full_mark: true, immediate_sweep: true) # collect the class p.parse('[1]') # segfault ```

Generated on Jun 19, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.