Skip to content

Oj::Doc malformed no-root inputs can create invalid Doc state and crash later Doc APIs #1016

@damseleng

Description

@damseleng

Summary

I found a reproducible native crash in Oj::Doc handling of malformed/no-root inputs.

Oj::Doc.open() and Oj::Doc.open_file() can accept inputs that do not produce a valid root leaf, but still return a Doc object. Later calls to public Doc APIs such as Doc#local_key or Doc#each_leaf can then dereference invalid internal state and crash the Ruby process.

This reproduces in a normal, non-sanitized Ruby build. I am not claiming code execution.

Affected version tested

I confirmed the issue on the latest development/release commit I tested:

oj v3.17.3
commit: bbde91a679728f94c4492ebc3683f4fa3309049f

I also reproduced the main Doc#local_key crash with the installed gem version:

gem install oj -v 3.17.3

Reproducer

Minimal example:

require "oj"

doc = Oj::Doc.open("")
doc.local_key

This crashes the Ruby process with a native segmentation fault.

The same class of crash is also reachable through file input:

require "oj"

File.write("empty.json", "")
doc = Oj::Doc.open_file("empty.json")
doc.local_key

Inputs that trigger the invalid Doc state

I tested Oj::Doc.open and Oj::Doc.open_file across malformed/no-root inputs. Crashes were observed for inputs such as:

empty input
space
tab
newline
#
/* comment */
/* comment
// comment\n
// comment

Crashing APIs

The crash was observed through:

Doc#local_key
Doc#each_leaf

In my brute-force check across open / open_file, 21 inputs, and 14 Doc APIs, I observed 36 normal-build native crashes.

Observed behavior

The process aborts with a Ruby native crash, for example:

[BUG] Segmentation fault

The relevant frame in my logs points to the Oj::Doc implementation, including:

fast.c
doc_local_key

The apparent root cause is that a Doc object can be returned with no valid root/where path state, and later API calls do not reject that invalid state before dereferencing it.

Expected behavior

Malformed or no-root inputs should not produce a Doc object that can later crash the process.

Possible safe behaviors would be:

  • reject the input in Oj::Doc.open() / Oj::Doc.open_file()
  • return a Doc object that consistently represents an empty/invalid document and raises Ruby exceptions for invalid operations
  • add guards in Doc#local_key, Doc#each_leaf, and similar APIs before dereferencing root/where path state

Additional sanitizer-only observations

The reproduction package also includes two secondary observations:

  1. Oj.saj_parse with comment-only inputs can produce an ASan heap-buffer-overread, but I did not observe a normal-build crash.
  2. Oj.load with a custom IO object returning "" on the second read can trigger stack smashing detected in a normal build and an ASan negative-size-param report. This may depend on IO contract assumptions, so I am treating it separately from the main Oj::Doc issue.

The main report is the normal-build Oj::Doc crash from malformed/no-root string and file inputs.

Reproduction package

I attached a minimal reproduction package without convenience binaries:

oj-maintainer-repro_deep_public_minimal.zip
SHA-256: 37225257CB7D02A94C91C90594F90F732D2ACAFC61A4EBDC63F037AF5474406A

The package contains:

README
PoCs
normal-build crash logs
ASan logs
open/open_file API matrix results
gem install reproduction logs
reproduction scripts
SHA256SUMS.txt

I can provide the full package with convenience binaries if useful.

oj-maintainer-repro_deep_public_minimal.zip

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions