Skip to content

Repository files navigation

json-fuzz

A reusable, structure-aware JSON fuzzer for Go JSON parsers. It provides a grammar-based JSON generator, a suite of JSON-aware mutation operators, adversarial key-path generation, a seed corpus covering every known parser bug class, and correctness gates (assertions) that any parser project can wire into its fuzz harness in ~10 lines.

Extracted from the fuzz infrastructure that found real bugs in buger/jsonparser.

Why structure-aware fuzzing?

Blind libFuzzer mutation reaches the dangerous structural boundaries of a JSON parser (truncation right after a :, truncation inside a \uXXXX escape, an unmatched { with no closer) only after exponentially many trials. json-fuzz replaces blind mutation with a grammar-based generator that always emits valid JSON, then applies JSON-aware mutation operators that break that JSON at precisely the byte positions where the parser's state machine has to make a transition.

Bugs found in jsonparser

This fuzzer found the following real bugs in buger/jsonparser:

Bug class Details
Empty-key panics (8 sites) Passing "" as a key path component caused nil-pointer dereferences in searchKeys, EachKey, createInsertComponent, calcAllocateSpace, and the parser.go:981 site.
Lone-surrogate synthesis The escape decoder accepted lone surrogates (\uD800 alone) and synthesized invalid code points instead of rejecting them.
Delete trailing-comma malformation Delete on certain object keys left a trailing comma ({"a":1,}), producing invalid JSON.
Cross-type Set corruption Set with an array-index path ([N]) on an object root appended ,value without a key, producing invalid JSON like {"co":1,42}.
Scalar-array data loss Set at an index beyond array length silently dropped data in certain configurations.
ArrayEach non-array-root spurious callback ArrayEach on a non-array root fired the callback with stale offsets.

Quick start

Implement ParserTarget (and the optional extension interfaces your parser supports), then wire the fuzzer into a _test.go file:

package myparser_test

import (
    "testing"

    "github.com/probelabs/json-fuzz"
)

// myTargetAdapter adapts your parser to the jsonfuzz.ParserTarget interface.
type myTargetAdapter struct{}

func (a *myTargetAdapter) Get(data []byte, path ...string) ([]byte, jsonfuzz.DataType, int, error) {
    val, dt, off, err := myparser.Get(data, path...)
    return val, jsonfuzz.DataType(dt), off, err
}
// ... implement NumberParser / StringParser / etc. as your parser supports.

func FuzzMyParser(f *testing.F) {
    jsonfuzz.AddCorpus(f) // seeds all dangerous inputs
    f.Fuzz(func(t *testing.T, data []byte, path []byte) {
        jsonfuzz.RunStructureAware(t, data, path, &myTargetAdapter{})
    })
}

Run it:

go test -fuzz=FuzzMyParser -fuzztime=60s

Manual gate wiring

For fine-grained control, use the Gates helper directly:

func FuzzMyParser(f *testing.F) {
    jsonfuzz.AddCorpus(f)
    f.Fuzz(func(t *testing.T, data []byte, path []byte) {
        gates := jsonfuzz.NewGates(t, data, path)
        gates.NoPanic(func() { myparser.Get(data, jsonfuzz.SplitPath(path)...) })
        if result, err := myparser.Set(data, val, jsonfuzz.SplitPath(path)...); gates.NoError(err) {
            gates.OutputValidJSON(result)
            gates.RoundTrip(result, path, val, func(d []byte, p ...string) ([]byte, error) {
                v, _, _, e := myparser.Get(d, p...)
                return v, e
            })
        }
    })
}

API reference

Generator

Grammar-based JSON generation. Always emits valid JSON (RFC 8259); the mutation operators then break it at known-dangerous boundaries.

Function Description
GenJSON(r *rand.Rand) []byte Generate a valid JSON document (random depth).
GenJSONAtDepth(r *rand.Rand, depth int) []byte Generate at explicit depth.
GenValue(r, depth, &buf) Append one weighted-random JSON value.
GenObject(r, depth, &buf) Emit an object with 0–5 adversarial keys.
GenArray(r, depth, &buf) Emit an array with 0–5 elements.
GenKey(r, &buf) Emit an object key (16 cases: empty, unicode, surrogate, invalid-UTF-8, BOM, …).
GenString(r, &buf) Emit a string value (22 cases: escapes, multibyte UTF-8, surrogates, control chars, …).
GenNumber(r, &buf) Emit a number (20 cases: int64 boundaries, exponents, negative zero, Infinity text, …).
GenDeepNesting(depth, closed) []byte Emit deeply-nested [[[...]]] (CVE-2020-29652 class).
MaxGenDepth Maximum generation depth (5).

Mutations

JSON-aware mutation operators. Each returns a NEW slice (never mutates in place). ApplyMutation picks one at random; each operator is also exported for direct use.

Operator Target boundary
ApplyMutation(r, data) []byte Pick one operator at random.
TruncateAtValueBoundary Cut right after : (OSS-Fuzz sentinel_value_boundary class).
TruncateMidKey Cut inside a string key.
TruncateMidStructure Cut after unclosed { or [.
TruncateMidEscape Cut inside \uXXXX.
TruncateMidElement Cut inside an array element.
DuplicateKey Inject duplicate object key.
ByteflipAtStructuralByte Flip byte at {,},[,],:,,,".
InjectInvalidUTF8 Insert overlong/truncated/never-valid UTF-8 in a string.
InjectLoneSurrogate Corrupt \uXXXX into a lone surrogate.
InjectUnbalancedOpen Insert unmatched { or [.
InjectMismatchedCloser Swap } for ] or vice versa.

Path utilities

Function Description
SplitPath(path []byte) []string Split fuzz path bytes on / (empty segments → "" components).
GenKeyPath(r) []string Generate an adversarial multi-component path.
GenPathComponent(r) string Generate one adversarial component (empty key, [99], unicode, …).
InjectPathHazard(r, path) []string Inject empty-key or OOB-index hazard.
PathShapeMatchesRoot(data, path) bool Check root container-type compatibility.
PathShapeMatchesData(data, path, resolve) bool Deep per-level container-type check.

Gates

Correctness assertions. Construct via NewGates, then call methods as you exercise your parser.

Method Gate Bug class caught
NewGates(t, data, path) *Gates Construct for one fuzz iteration.
NoPanic(fn) 1. NO-PANIC Empty-key panics, truncation panics, sentinel-deref.
OutputValidJSON(data) 2. OUTPUT-VALIDITY Silent data corruption after Set/Delete.
RoundTrip(doc, path, expected, get) 3. ROUND-TRIP Set/Get asymmetry, index-beyond-length data loss.
NumericOracle(token, got, err) 4. NUMERIC-ORACLE (int) ParseInt overflow / leading-zero divergence.
NumericOracleFloat(token, got, err) 4b. NUMERIC-ORACLE (float) ParseFloat precision / overflow divergence.
OffsetBounds(offset, dataLen) 5. OFFSET-BOUNDS OOB index at call site.
Determinism(fn) 6. DETERMINISM Map-iteration / stale-state bugs.
SliceAliasing(slice, input) 7. ALIASING Synthesized/stale slice corruption.
ParseStringDiff(raw, got, err) 8. PARSESTRING Unescape bugs on \uXXXX, surrogates, control chars.
ParseBooleanDiff(token, got, err) 9. PARSEBOOLEAN Boolean coercion divergence.
NoError(err) Convenience: gate post-conditions on op success.

Corpus

Function Description
AddCorpus(f) Register ALL seeds (data + path) with a *testing.F.
AddCorpusDataOnly(f) Register data-only seeds (for single-arg fuzz functions).
StructureAwareSeeds() []SeedPair Raw structure-aware seed pairs.
PathMutationSeeds() []SeedPair Raw path-mutation seed pairs.
EncodingJSONSeeds() [][]byte Raw encoding/json data-only seeds.

Target interfaces

Interface Methods Gates unlocked
ParserTarget (required) Get NoPanic, OffsetBounds, Determinism
NumberParser ParseInt, ParseFloat NumericOracle, NumericOracleFloat
StringParser ParseString ParseStringDiff
BooleanParser ParseBoolean ParseBooleanDiff
IteratingParser ArrayEach, ObjectEach NoPanic on iteration + NumericOracle on elements
AliasingParser AliasesInput() bool SliceAliasing (opt-in; byte-oriented parsers only)
MutatingTarget Set, Delete OutputValidJSON, RoundTrip
Orchestrator Description
RunStructureAware(t, data, path, target) Full structure-aware fuzz body (generator + mutations + all gates).
RunSequence(t, data, path, target) Multi-operation sequence fuzzer (S1–S5: Set→Get, Delete→Get, Set→Set, Delete→Delete, Set→Delete→Set). Requires MutatingTarget.

The encodingjson sub-package

github.com/probelabs/json-fuzz/encodingjson is a self-contained structure- aware fuzzer that probes Go's standard-library encoding/json for internal consistency. It does not require a ParserTarget — call Fuzz directly:

package stdlib_test

import (
    "testing"
    "github.com/probelabs/json-fuzz/encodingjson"
)

func FuzzEncodingJSON(f *testing.F) {
    encodingjson.Fuzz(f)
}

Gates exercised (each maps to a known stdlib bug class):

Gate Assertion
A. NO-PANIC json.Unmarshal must never panic (recover-wrapped).
B. VALID CONSISTENCY json.Valid and json.Unmarshal must agree.
C. ROUND-TRIP Unmarshal → Marshal → Unmarshal must DeepEqual (both interface{} and json.Number variants).
D. DEEP-NESTING [[[...]]] must error cleanly, never stack-overflow (CVE-2020-29652 class).
E. STREAMING Decoder.Decode and Unmarshal must agree on valid JSON.
F. TRANSFORMS Compact/Indent/HTMLEscape must never panic.
H. TRANSFORM DoS Compact/Indent/HTMLEscape on deep nesting must be budget-bounded.
I. MARSHAL DETERMINISM Marshal on the same value is byte-identical.

How to run

# Run the self-tests (no -fuzz needed):
go test ./... -count=1 -race

# Fuzz your parser:
go test -fuzz=FuzzMyParser -fuzztime=60s

# Fuzz encoding/json:
go test -fuzz=FuzzEncodingJSON -fuzztime=60s

License

MIT, same as the Go ecosystem.

About

Structure-aware JSON fuzzer: grammar-based generator, JSON-aware mutations, and correctness gates for differential testing of JSON parsers (Go).

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages