A standalone, hardened, structure-aware GraphQL fuzzer for Rust GraphQL parsers. It provides grammar-based generators, GraphQL-aware mutation operators, a curated seed corpus, and correctness gates that any parser project can wire into its fuzz harness.
This is the GraphQL equivalent of
json-fuzz — a reusable, importable
fuzzer. It replaces blind byte-mutation with a grammar-based generator that
always emits valid (or near-valid) GraphQL, then applies GraphQL-aware
mutation operators that break the document at precisely the byte positions
where the parser's state machine transitions.
Extracted from the proptest fuzz infrastructure that probes
hive/router's graphql-tools
parser, query-planner, and validator — then hardened with full edge-case
coverage.
Blind libFuzzer mutation reaches the dangerous structural boundaries of a
GraphQL parser (truncation right after {, truncation inside (...),
unmatched braces, invalid UTF-8 inside a string literal) only after
exponentially many trials. graphql-fuzz always emits structurally-valid
GraphQL from the grammar, then applies mutations that break it at the exact
positions where the parser's state machine must make a transition — maximizing
coverage per fuzz iteration.
| Class | Examples |
|---|---|
| UTF-8 / surrogate handling | Lone surrogates (\uD800), overlong encodings (0xC0 0xAF), truncated sequences, never-valid bytes (0xFF), BOM. |
| Deep nesting DoS | 100+ level selection sets — stack-overflow surface. |
| Fragment cycles | A spreads B, B spreads A — infinite-loop surface. |
| Typeless inline fragments | ... { field } (no on T) — known to panic some normalizers. |
| Numeric boundaries | i64 min/max, 100+ digit integers, float overflow (1e999), negative zero. |
| String escapes | All 8 escapes, \uXXXX, block strings ("""..."""), raw control chars. |
| Structural truncation | Cut inside { }, inside ( ), inside @directive( ), inside default values. |
| Name validation | Empty, digit-start, unicode, keywords-as-names, very long (1 KB+). |
Add to Cargo.toml:
[dev-dependencies]
graphql_fuzz = "0.1"
rand = "0.8"Wire into a fuzz loop:
use graphql_fuzz as gfuzz;
use rand::SeedableRng;
use rand::rngs::StdRng;
fn fuzz_one(seed: u64) {
let mut rng = StdRng::seed_from_u64(seed);
// 1. Generate a structurally-valid operation.
let op = gfuzz::gen_operation(&mut rng, 5);
// 2. Optionally mutate it (truncate, inject invalid UTF-8, …).
let mutated = gfuzz::apply_mutation(&mut rng, op.as_bytes());
// 3. Feed it to your parser under the no-panic gate.
let input = String::from_utf8_lossy(&mutated);
gfuzz::gates::no_panic("parse_operation", || {
let _ = my_parser::parse_operation(&input); // your parser here
}).unwrap();
}use proptest::prelude::*;
use rand::SeedableRng;
fn operation_strategy() -> impl Strategy<Value = String> {
any::<u64>().prop_map(|seed| {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
graphql_fuzz::gen_operation(&mut rng, 5)
})
}
proptest! {
#[test]
fn fuzz_parse_no_panic(input in operation_strategy()) {
graphql_fuzz::gates::no_panic("parse", || {
let _ = my_parser::parse(&input);
}).unwrap();
}
}See tests/integration.rs for a complete working example (including a
reference "parser" that exercises every gate).
For fine-grained control, use the gate functions directly:
use graphql_fuzz::gates;
// Gate 1: NO-PANIC — catch_unwind; any panic is a finding.
gates::no_panic("parse", || my_parser::parse(&input)).unwrap();
// Gate 2: OUTPUT-VALID — validation returns Ok or typed Err, never panics.
gates::output_valid("validate", || my_validator::validate(&input)).unwrap();
// Gate 3: ROUND-TRIP — parse → print → parse produces equivalent AST.
gates::round_trip("rt", &input, &my_parser::parse, &my_parser::print).unwrap();
// Gate 4: DETERMINISM — two parses produce identical results.
gates::determinism("det", &input, &my_parser::parse).unwrap();
// Gate 5: DEEP-NESTING-SAFE — deeply nested input errors cleanly.
gates::deep_nesting_safe("deep", &input, &my_parser::parse).unwrap();Grammar-based GraphQL generation. The generators take &mut impl Rng and
return String.
| Function | Description |
|---|---|
gen_operation(r, max_depth) |
Recursive operation generator (query/mutation/subscription/shorthand, selection sets, fields, aliases, arguments, inline fragments, fragment spreads, variables, directives). |
gen_schema(r, max_types) |
SDL schema generator (object/input/interface/union/enum/scalar/directive, with query/mutation/subscription roots). |
gen_sdl_ish(r) |
SDL-ish strings (potentially malformed — for schema-parser error-path testing). |
gen_variable_value(r) |
GraphQL value literal (int/float/string/boolean/null/enum/list/object). |
gen_value(r, depth) |
GraphQL value at explicit recursion depth. |
gen_name(r) |
GraphQL name with adversarial variants (empty, digit-start, unicode, keywords-as-names, 1 KB+). |
gen_valid_name(r) |
Always-valid GraphQL name (/[_A-Za-z][_0-9A-Za-z]*/). |
gen_deeply_nested_operation(depth) |
{ id { id { ... } } } at the given depth (DoS surface). |
gen_multi_operation_document(r, max_depth) |
Document with multiple operations (sometimes duplicate names). |
gen_empty_selection_operation(r) |
{ }, query { }, etc. |
gen_nested_typed_inline_fragments(r, depth) |
... on A { ... on B { ... on C } } deep type-condition nesting. |
gen_nested_typeless_inline_fragments(r, depth) |
... { ... { ... { T } } } typeless inline chain. |
gen_complex_object_argument_query(r, depth) |
field(input: { nested: { deep: [1, 2, { x: "y" }] } }) complex args. |
gen_directive_on_variable_definition(r) |
query ($x: String @custom) { ... } directive on VARIABLE_DEFINITION. |
gen_unused_and_duplicate_variables(r) |
Unused + duplicate variable definitions in one operation. |
gen_described_schema(r) |
Schema with block-string + string descriptions on types, fields, enum values. |
gen_specified_directives_schema(r) |
Schema with all 5 specified directives (@skip, @include, @deprecated, @specifiedBy, @oneOf). |
gen_all_extension_kinds_schema(r) |
Schema with all 7 extension kinds (extend type/interface/union/enum/input/scalar/schema). |
gen_deeply_nested_input_schema(r) |
Schema with 5+ levels of nested input object types. |
Apollo Federation directive generation — both valid (grammar-correct for testing planners) and invalid (to test error-handling surfaces). Covers Federation 1.0, 2.0, 2.3, and the cost-control directives.
| Function | Description |
|---|---|
gen_federated_schema(r) |
Complete federated subgraph SDL with @link, entity types, @external/@requires/@provides/@shareable/@inaccessible/@override/@tag/@cost/@listSize. |
gen_federated_operation(r, max_depth) |
Operation querying entity types with fragments, nested federated field access, entity-key variables. |
gen_federated_schema_and_operation(r, max_depth) |
(schema, operation) pair for full lifecycle testing. |
gen_malformed_federation_cost_schema(r) |
Schema with only malformed @cost/@listSize (negative weights, wrong types, missing args). |
gen_entity_type(r) |
Entity type with @key + federated field directives. |
gen_federated_extension_type(r) |
Fed 1.0 extend type X @key(fields: "id") { field @external }. |
gen_federated_extends_type(r) |
Fed 2.0 type X @extends @key(fields: "id") { field @external }. |
gen_interface_object_type(r) |
Fed 2.3 type X @interfaceObject @key(fields: "id"). |
gen_field_set(r, depth) |
Field-set string for @key/@requires/@provides (flat, nested, deeply-nested, empty). |
gen_key_directive(r) |
@key(fields: "...") valid + invalid (empty, wrong type, missing args). |
gen_requires_directive(r) |
@requires(fields: "..."). |
gen_provides_directive(r) |
@provides(fields: "..."). |
gen_external_directive(r) |
@external. |
gen_extends_directive(r) |
@extends (Fed 1.0 syntax). |
gen_shareable_directive(r) |
@shareable. |
gen_inaccessible_directive(r) |
@inaccessible. |
gen_override_directive(r) |
@override(from: "...") valid + invalid (empty, missing from). |
gen_tag_directive(r) |
@tag(name: "..."). |
gen_link_directive(r) |
@link(url: "...", import: [...]) valid + invalid. |
gen_compose_directive(r) |
@composeDirective(name: "..."). |
gen_interface_object_directive(r) |
@interfaceObject (Fed 2.3). |
gen_cost_directive(r) |
@cost(weight: N) valid + invalid (negative, float, string, missing — KI-QP-PANIC-MALFORMED-COST-LISTSIZE). |
gen_listsize_directive(r) |
@listSize(...) valid + invalid (negative assumedSize, wrong type — KI-QP-PANIC-MALFORMED-COST-LISTSIZE). |
GraphQL-aware mutation operators. Each takes &[u8] and returns Vec<u8>
(never mutates in place). apply_mutation picks one at random.
| Operator | Target boundary |
|---|---|
apply_mutation(r, data) |
Pick one operator at random. |
truncate_at_selection_set |
Cut inside a { ... } body. |
truncate_at_argument_list |
Cut inside a (arg: 1, ...) list. |
truncate_at_directive |
Cut inside a @directive(...). |
truncate_at_variable_default |
Cut inside a variable default value. |
inject_invalid_utf8 |
Insert overlong/truncated/never-valid UTF-8 in a string. |
inject_lone_surrogate |
Corrupt \uXXXX into a lone surrogate. |
byteflip_at_structural_byte |
Flip a byte at {, }, (, ), [, ], :, !, @, $, ., ,, ` |
swap_brace_bracket |
Swap { ↔ [ and } ↔ ]. |
inject_deep_nesting |
Wrap a selection set in 100+ extra { ... }. |
inject_unbalanced_brace |
Insert unmatched { or (. |
truncate_at_field_set |
Cut inside a field's sub-selection (field { without closer). |
truncate_at_input_object |
Cut inside an input object literal ({ k: v }). |
swap_extension_keyword |
Remove/add extend before type definitions. |
inject_cyclic_fragment |
Make a fragment definition spread itself. |
inject_duplicate_field |
Duplicate a field inside a selection set. |
inject_duplicate_type |
Duplicate a type definition in SDL. |
Correctness assertions, implemented as catch_unwind wrappers returning
Result<T, GateFailure>.
| Function | Gate | Bug class caught |
|---|---|---|
no_panic(label, f) |
1. NO-PANIC | Truncation panics, sentinel-deref panics, assertion panics. |
output_valid(label, f) |
2. OUTPUT-VALID | Validator panics on malformed input. |
round_trip(label, input, parse, print) |
3. ROUND-TRIP | Printer/parser asymmetry, AST canonicalization bugs. |
determinism(label, input, parse) |
4. DETERMINISM | Map-iteration / stale-state / non-deterministic errors. |
deep_nesting_safe(label, input, parse) |
5. DEEP-NESTING-SAFE | Unbounded-recursion stack overflow (DoS). |
federation_valid(label, input, parse) |
6. FEDERATION-VALID | Federation-spec parser panics on malformed @cost/@listSize/@key (KI-QP-PANIC-MALFORMED-COST-LISTSIZE). |
run_suite(label, input, parse, print) |
1+3+4 combined | Convenience: no-panic + determinism + optional round-trip. |
| Function | Description |
|---|---|
all_seeds() |
Combined corpus (text + byte seeds) as Vec<Vec<u8>>. |
text_seeds() |
All valid-UTF-8 seeds. |
byte_seeds() |
Raw byte seeds with invalid UTF-8. |
operation_seeds() |
Valid and edge-case query/mutation/subscription docs. |
schema_seeds() |
SDL schemas (valid + edge cases + federation). |
federation_schema_seeds() |
Federation subgraph SDL (valid + malformed @cost/@listSize). |
federation_operation_seeds() |
Federation operations (entity queries, fragments on entity types). |
Constants: OPERATION_SEEDS, SCHEMA_SEEDS, VALUE_SEEDS, NAME_SEEDS,
BYTE_SEEDS, FEDERATION_SCHEMA_SEEDS, FEDERATION_OPERATION_SEEDS.
| Category | Cases |
|---|---|
| Block strings | """...""" with varying indentation, empty, nested triple-quotes, leading/trailing newlines, comment-like content. |
| String escapes | \n \t \r \b \f \" \\ \/ \uXXXX — ALL of them. |
| Surrogate pairs | Valid pairs (U+10000–U+10FFFF), lone high (\uD800), lone low (\uDC00), two-high (\uD800\uD800). |
| Raw multibyte UTF-8 | 2-byte (U+0080–U+07FF), 3-byte (U+0800–U+FFFF), 4-byte (U+10000+, emoji). |
| Invalid UTF-8 | Overlong 0xC0 0xAF, truncated 0xE0, never-valid 0xFF/0xFE, UTF-8-encoded surrogate. |
| BOM | U+FEFF at document start, inside strings. |
| Control characters | \u0000–\u001F (escaped). |
| Long strings | 50–100 chars, 10 KB+. |
| Category | Cases |
|---|---|
| Integers | 0, negative, i64::MAX/MIN, i64::MAX+1 (overflow), 100+ digit, leading zeros, -0. |
| Floats | 1.0, 1e10, 1E-10, 0.0, -0.0, 1e999 (overflow), 1e-999 (underflow), . without digits, trailing dot, multiple dots. |
| Non-numeric | Infinity, NaN, -Infinity (invalid in GraphQL but parsers may see them). |
| Category | Cases |
|---|---|
| Valid | Short, medium, underscore-prefixed, digit-containing. |
| Invalid | Empty, digit-leading, hyphen-containing, space-containing. |
| Keywords | query, mutation, on, true, false, null, fragment, schema, type, interface, implements, enum, union, input, directive, extend. |
| Reserved | __typename, __schema, __type, double-underscore prefix. |
| Unicode | Greek letters (invalid per spec — ASCII only). |
| BOM | BOM-prefixed name. |
| Long | 1 KB+ names. |
| Category | Cases |
|---|---|
| Deep nesting | 100+ level selection sets (DoS surface). |
| Fragment cycles | A→B→A, X→Y→Z→X. |
| Typed inline fragments | ... on A { ... on B { ... on C } } deep nesting. |
| Typeless inline fragments | ... { field } (no on T). |
| Empty selection sets | { }, query { }. |
| Empty containers | [], {} in variables. |
| Multiple operations | Duplicate names, unnamed + named. |
| Directives | On fields, fragments, operations, variable definitions; @skip/@include with $var and literal true/false. |
| Introspection | __schema, __type, __typename. |
| Extensions | All 7 kinds: extend type/interface/union/enum/input/scalar/schema. |
| Complex arguments | Nested object args field(input: { nested: { deep: [...] } }). |
| Unused/duplicate variables | $a: String, $a: Int, $unused: Boolean in one operation. |
| Directive | Valid shapes | Invalid shapes |
|---|---|---|
@key(fields: "...") |
Single, multi, nested field sets; resolvable: true/false; repeatable multiple keys. |
Empty "", nonexistent fields, wrong type 123, missing args. |
@requires(fields: "...") |
Flat + nested field sets on external fields. | Empty, missing args. |
@provides(fields: "...") |
Flat + nested field sets. | Empty, missing args. |
@external |
On fields. | — |
@extends |
Fed 1.0 + Fed 2.0 @extends directive syntax. |
— |
@shareable |
On fields + types; repeatable. | — |
@inaccessible |
On all 10 locations. | — |
@override(from: "...") |
from: "products". |
Empty "", missing from. |
@tag(name: "...") |
Repeatable on all locations. | Empty name, missing name. |
@link(url: "...", import: [...]) |
Federation 2.0/2.3/2.5, link, join URLs; deep import lists. | Missing url, wrong type. |
@composeDirective(name: "...") |
@composeDirective(name: "@custom"). |
Empty name, missing name. |
@interfaceObject |
Fed 2.3 on entity types. | — |
@cost(weight: N) |
Non-negative integers. | Negative -1, float 1.5, string "big", missing — KI-QP-PANIC-MALFORMED-COST-LISTSIZE. |
@listSize(...) |
assumedSize, slicingArguments, sizedFields, requireOneSlicingArgument. |
Negative assumedSize, float, wrong type — KI-QP-PANIC-MALFORMED-COST-LISTSIZE. |
| Category | Cases |
|---|---|
| Empty document | Empty, whitespace-only, comment-only. |
| BOM | U+FEFF at start. |
| Comments | # at every position, comments inside block strings. |
| Duplicate types | Same type name defined twice. |
| Descriptions | Block-string """...""" and string "..." on types, fields, enum values. |
| Specified directives | All 5: @skip, @include, @deprecated, @specifiedBy, @oneOf. |
# Build the library:
cargo build
# Run all self-tests (24 unit + 33 hive differential + 42 integration):
cargo test
# Run with more proptest cases (deeper fuzz):
PROPTEST_CASES=2048 cargo test
# Run clippy:
cargo clippy --all-targetssrc/
├── lib.rs — public API + re-exports
├── generator.rs — GraphQL operation/query generator (gen_operation, complex query generators)
├── schema.rs — SDL schema generator (gen_schema, gen_sdl_ish, descriptions, extensions, specified directives)
├── federation.rs — Apollo Federation directive generators (14 directives, federated schemas + operations)
├── variable.rs — GraphQL variable/value generator (gen_value, gen_variable_value)
├── mutate.rs — 16 GraphQL-aware mutation operators
├── gates.rs — 6 correctness gates (no_panic, output_valid, round_trip, determinism, deep_nesting_safe, federation_valid)
└── corpus.rs — seed corpus (operation/schema/value/name/federation + byte seeds)
tests/
├── integration.rs — full self-test suite (generator, mutation, gate, corpus, federation tests)
└── hive_differential.rs — differential fuzz harness against hive/router graphql-tools parser (known-issue reproducers + federation integration)
MIT.