Map
โ†‘Supply Chain Security

Structure-Aware Fuzzing

Wiki conceptfuzzingsecuritytesting โ†ณ show in map Markdown
title
Structure-Aware Fuzzing
type
concept
summary
Fuzz a function by generating valid in-language values (structs, slices, pointers) instead of raw bytes; the fuzzer mutates bytes underneath and the runtime handles encode/decode
tags
fuzzing, security, testing
created
2026-05-12
updated
2026-05-12

The bytes-in interface of most coverage-guided fuzzers (libFuzzer's LLVMFuzzerTestOneInput(const uint8_t*, size_t), Go's f.Fuzz(func(t *testing.T, b []byte))) is the wrong shape when the target is an in-process API that takes a struct. The author has to write a wire-format wrapper that decodes a []byte into the struct, every field becomes a bug surface, and the fuzzer wastes mutations producing inputs that don't decode.

Structure-aware fuzzing lets the harness say: "the input is this type." The fuzzer keeps mutating bytes internally, but the runtime serializes the bytes into a valid value of the declared type on the way in. From the target's perspective, the input is already typed.

type Input struct {
    Data []byte
    S    string
    N    int
}

func FuzzStructInput(f *testing.F) {
    f.Add(Input{Data: []byte("hello"), S: "world", N: 42})
    f.Fuzz(func(t *testing.T, in Input) {
        Process(in)
    })
}

The fuzzer is still byte-driven underneath โ€” the corpus, the mutators, the coverage feedback all operate on bytes โ€” but the wrapper layer means a single random mutation can mean "add an element to Data," "change N by one bit," or "swap two characters in S," depending on where the bytes land.

Vanilla Go fuzzing's limit

Go's stdlib fuzzer is the basic case: f.Fuzz only accepts callbacks taking the primitive types ([]byte, string, integer types, bool, the float types). Anything composite โ€” a struct, a slice of structs, a pointer โ€” needs a hand-written decode in the harness body. gosentry generalizes this to arbitrary types via the LibAFL runner.

Rust / C++ analogues

The arbitrary crate (Rust) and FuzzedDataProvider (libFuzzer C++) are the same idea: a small library that takes a byte buffer and produces a typed value, deterministic enough that the fuzzer's mutations propagate through it. cargo-fuzz wires arbitrary into the fuzz_target! macro the same way gosentry wires its decoder into f.Fuzz.

When to reach for it instead of a grammar

grammar-based-fuzzing is the right tool when the input is bytes on a wire (a serialized RPC frame, a JSON document, source code in some language); the grammar describes the bytes. Structure-aware fuzzing is the right tool when the input is a value the API takes directly โ€” a Go struct passed to an internal function, a Rust enum, a C++ object. The two compose: a grammar generates the bytes that get parsed into a struct, structure-aware fuzzing then mutates the struct fields directly to exercise the post-parse code.