Structure-Aware Fuzzing
- 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
- parent
- supply-chain-security
- 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.