Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

graphql-fuzz

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.

Why structure-aware fuzzing?

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.

Bug classes targeted

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+).

Quick start

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();
}

Proptest integration

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).

Manual gate wiring

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();

API reference

Generators

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.

Federation generators

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).

Mutations

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.

Gates

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.

Corpus

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.

Edge-case coverage

String/value edge cases

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+.

Numeric edge cases

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).

Name/identifier edge cases

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.

Structural edge cases

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.

Federation directive coverage

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.

Document-level edge cases

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.

How to run

# 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-targets

Package layout

src/
├── 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)

License

MIT.

About

Structure-aware GraphQL fuzzer: grammar-based query/operation/schema generators, GraphQL-aware mutations, and correctness gates for differential testing of GraphQL parsers (Rust).

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages