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.
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.
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. |
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=60sFor 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
})
}
})
}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). |
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. |
| 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. |
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. |
| 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. |
| 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. |
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. |
# 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=60sMIT, same as the Go ecosystem.