Fuzzing
Fuzzing lets the toolchain invent inputs for your function and saves any input that makes it fail as a permanent test.
func FuzzXxx(f *testing.F) { f.Add(seed); f.Fuzz(func(t *testing.T, in string) { ... }) }A normal test checks the inputs you thought of. Fuzzing checks the ones you did not. The toolchain generates input, calls your function with it, and watches for a panic or a failed check. When it finds one, it saves that input so the failure never gets lost. Fuzzing has been part of the standard toolchain since Go 1.18.
Writing a fuzz target
A fuzz target is a function whose name begins with Fuzz and that takes f *testing.F. It does two things. f.Add(...) supplies a starting input, called a seed; the generator mutates seeds rather than starting from nothing, so good seeds matter. f.Fuzz(func(t *testing.T, in string) { ... }) supplies the function to run against every input. f.Fuzz is called exactly once, and the arguments given to f.Add must match the fuzz function's parameters in number and type, or it fails at run time. The allowed parameter types are []byte, string, bool, rune, the integer types and the float types.
Inside, you cannot know the expected answer, because you did not choose the input. So you check a property that must hold for every input: it does not panic, decoding what you encoded returns the original, the output length matches, the result is still valid UTF-8.
Running it
Seeds live in f.Add calls, or as files under testdata/fuzz/FuzzName/; the two are the same corpus from different places. go test -fuzz=FuzzName -fuzztime=30s generates new input. A failing input is written into testdata/fuzz/FuzzName/ as a small text file. Commit it: from then on it is a regression test that plain go test replays.
Example
// File: codec_test.go — fuzz it with: go test -fuzz=FuzzQuoteRoundTrip -fuzztime=10s
package codec
import (
"strconv"
"testing"
)
// Quote renders s as a Go string literal, escaping what needs escaping.
func Quote(s string) string { return strconv.Quote(s) }
func FuzzQuoteRoundTrip(f *testing.F) {
// Seeds: the generator mutates these rather than starting from nothing.
f.Add("hello")
f.Add("a b")
f.Fuzz(func(t *testing.T, in string) {
quoted := Quote(in)
back, err := strconv.Unquote(quoted)
if err != nil {
t.Fatalf("Unquote(%q): %v", quoted, err)
}
// The property: quoting then unquoting must return the original.
if back != in {
t.Errorf("round trip turned %q into %q", in, back)
}
})
}
// Output (counts vary; this run found no failure):
// fuzz: elapsed: 0s, gathering baseline coverage: 0/2 completed
// fuzz: elapsed: 0s, gathering baseline coverage: 2/2 completed, now fuzzing with 8 workers
// fuzz: elapsed: 10s, execs: 1035400 (103534/sec), new interesting: 11 (total: 13)
// PASS
When to use it
- Harden a parser that reads data from outside your system, where a panic means a crashed server.
- Check that an encode and decode pair round-trips for every input, not just the three you wrote by hand.
- Turn each crash the fuzzer finds into a committed regression test under testdata/fuzz.
More examples
Fuzzing a parser for inputs it should have rejected
This finds a real bug within seconds: the input ":x" contains a colon, so ParseHeader reports success with an empty name. Fuzzing is at its best on anything that reads untrusted bytes — parsers, decoders, path and URL handling — where the interesting inputs are the ones no person would type.
package header
import (
"bytes"
"testing"
)
// ParseHeader splits one header line into its name and value.
func ParseHeader(line []byte) (name, value string, ok bool) {
n, v, found := bytes.Cut(line, []byte(":"))
if !found {
return "", "", false
}
return string(bytes.TrimSpace(n)), string(bytes.TrimSpace(v)), true
}
func FuzzParseHeader(f *testing.F) {
f.Add([]byte("Host: example.com"))
f.Fuzz(func(t *testing.T, data []byte) {
name, _, ok := ParseHeader(data)
if !ok {
return
}
if name == "" {
t.Errorf("accepted %q but returned an empty name", data)
}
})
}What a failure leaves behind
The saved file is plain text: a version line, then one line per parameter of the fuzz function. Commit it with the fix. Because it now sits in the seed corpus, every later go test replays that exact input, so the bug cannot come back unnoticed. You can also hand-write such a file to add a case you know is awkward.
--- FAIL: FuzzParseHeader (0.03s)
--- FAIL: FuzzParseHeader (0.00s)
header_test.go:22: accepted ":0" but returned an empty name
Failing input written to testdata/fuzz/FuzzParseHeader/a1b2c3d4e5f60718
To re-run:
go test -run=FuzzParseHeader/a1b2c3d4e5f60718
# testdata/fuzz/FuzzParseHeader/a1b2c3d4e5f60718 contains:
go test fuzz v1
[]byte(":0")More than one generated argument
A fuzz target can take several generated values, and f.Add must then pass the same number of arguments in the same types. t.Skip discards an input that is outside the range the function is documented to accept — strings.Repeat panics on a negative count by design — so the run is not drowned in failures you do not care about. Skip the inputs that are genuinely invalid, not the ones that look awkward.
package repeat
import (
"strings"
"testing"
)
func FuzzRepeat(f *testing.F) {
// Two arguments here means two parameters in the fuzz function.
f.Add("ab", 3)
f.Fuzz(func(t *testing.T, s string, n int) {
if n < 0 || n > 100 {
t.Skip()
}
got := strings.Repeat(s, n)
if len(got) != len(s)*n {
t.Errorf("Repeat(%q, %d) length = %d, want %d", s, n, len(got), len(s)*n)
}
})
}
Discussion