Skip to content

JSOM 3.1.2 - crash fix: deep indentation could kill the process

Latest

Choose a tag to compare

@HarryPehkonen HarryPehkonen released this 22 Sep 02:33
· 5 commits to main since this release

Crash fix: a deeply indented document could kill the process

to_json() / JsonFormatter::format() threw std::length_error when a document was nested
deeper than the indentation could fit on one line, and a process that let that exception
escape died. The cause is unsigned arithmetic, not formatting:

available_width = options_.max_line_width - line_prefix.length();   // wrapped to ~2^64

The indent prefix grows with depth, so once it exceeds max_line_width the subtraction
wrapped and the line buffer asked for a reserve of about 2^64 bytes.

Reachability. Pretty (indent 2, width 100) breaks at 51 levels of nesting; Debug
(indent 4, width 80) at 21. The parser accepts up to 256 levels, so a 320-byte document
is enough — that is the input the fuzzer produced. The line is present in every release
back to 2.0.0
, so if you format untrusted or generated JSON with the pretty presets, this
is worth taking.

Fixed by clamping: with no room left on the line, every element goes on its own line,
which is what "nothing else fits" means. The two branches that computed the width computed
the same value, so there is one now, and an audit of the formatter found no other unsigned
width - length subtraction that is not bounded by its own maximum.

Pinned by tests/test_formatter_options.cpp (60 levels, 12-element array at the bottom,
across Pretty/Config/Api/Debug: no throw, and the output still parses back to the same
document) and archived as fuzz/regressions/deep-nesting-exceeds-line-width.json, which the
fuzz stage now replays on every run.

How it was found

The nightly fuzz campaign crashed 48 seconds into its JSOM window (2026-09-20 21:00) and
wrote an artifact; the fuzz-report watchdog reported it the next morning. Both halves
worked. The campaign's corpus keeps the input, so the next run replays it against the fixed
build.

Also in this release

The fuzz stage now feeds fuzz/regressions/ to the fuzzer (as well as fuzz/seeds/), so
every archived finding is replayed by every gate instead of only being kept on file. 15 CI
stages green.