What does a Go fuzz target `func FuzzParseQuery(f *testing.F)` do, and what are `f.Add` and `f.Fuzz` for?
answer
- it lives in a _test.go file
- seeds first, mutations after
- one call registers, one call runs
- f.Add supplies the callback's arguments
- the callback starts with *testing.T
basics
~20 sA Go fuzz target is a FuzzXxx function in a _test.go file that takes *testing.F. f.Add registers seed inputs; f.Fuzz takes the callback the engine runs, first on those seeds and then on mutated variations of them.
solid answer
~40 sA fuzz target is written like a test: `func FuzzParseQuery(f *testing.F)` in a `_test.go` file, with a name starting `Fuzz`. Inside it you call `f.Add(...)` zero or more times to register seed inputs — known-interesting values such as one valid query and one empty string — and then exactly one `f.Fuzz(func(t *testing.T, query string, limit int) { ... })`. The callback body is the property under test: it is handed one input per call, and the target fails if it panics or calls `t.Error`/`t.Fatal`. Under a plain `go test` the target runs only against its seeds, like an ordinary unit test. Under `go test -fuzz=FuzzParseQuery` the fuzzing engine runs those seeds first, then mutates them, keeping inputs that reach new code coverage, and hunts for a panic or a failed assertion.
code
go · 14 linesfunc FuzzParseQuery(f *testing.F) {
f.Add("name = \"ada\" LIMIT 10", 10)
f.Add("", 0)
f.Fuzz(func(t *testing.T, query string, limit int) {
parsed, err := ParseQuery(query, limit)
if err != nil {
return // rejecting nonsense is correct behaviour, not a failure
}
if parsed.Limit > limit {
t.Errorf("parsed limit %d exceeds requested %d", parsed.Limit, limit)
}
})
}go deeper
Be ready to write the skeleton from memory: a FuzzXxx function taking *testing.F, one or more f.Add seed calls, then a single f.Fuzz whose callback starts with *testing.T.
Explain the division of labour: f.Add supplies starting points, the engine mutates them and keeps whatever reaches new coverage, and the callback holds the property that must hold for every input including nonsense.
Show judgment about the callback's contents — cheap, deterministic, no shared state, early return on legitimately invalid input, and assertions only about invariants that survive hostile bytes.
Own the policy: which untrusted-input parsers are worth a target at all, whether seed-only runs go on every change with a bounded search on a schedule, and who triages what the engine finds.
## What a fuzz target is Go's `testing` package supports three kinds of function in a `_test.go` file: `TestXxx(*testing.T)`, `BenchmarkXxx(*testing.B)`, and `FuzzXxx(*testing.F)`. A **fuzz target** is the third. It is discovered by name — the identifier must begin with `Fuzz` followed by a character that is not a lowercase letter — and it takes a single `*testing.F`. A fuzz target is not a test that loops over random values. It is a *registration* function: it tells the fuzzing engine what interesting inputs it already knows about, and what code to run for every input the engine invents. ## The two calls that make it up ```go func FuzzParseQuery(f *testing.F) { f.Add("name = \"ada\" LIMIT 10", 10) // a realistic query f.Add("", 0) // a degenerate one f.Fuzz(func(t *testing.T, query string, limit int) { parsed, err := ParseQuery(query, limit) if err != nil { return } if parsed.Limit > limit { t.Errorf("parsed limit %d exceeds requested %d", parsed.Limit, limit) } }) } ``` **`f.Add(args ...any)`** adds one *seed corpus entry*. Each call supplies one complete set of arguments for the callback, in the callback's parameter order and with exactly the callback's types. Seeds are the starting points the engine mutates: bytes flipped, values incremented, strings truncated and spliced. Good seeds are the difference between a fuzzer that reaches your parser's interesting branches in seconds and one that spends a day discovering that a query needs an `=` sign in it. **`f.Fuzz(ff any)`** takes the fuzz callback and runs it. Its first parameter must be `*testing.T`; the remaining parameters are the generated input. `f.Fuzz` must be called **exactly once** in a target, and it does not return until the run for that target is over — so any setup shared by every invocation goes *above* it, and code written after it effectively never runs during fuzzing. `*testing.F` methods must not be called from inside the callback. ## What counts as a failure The callback does not have to assert anything to be useful. The engine treats a **panic** — an index out of range, a nil dereference, a slice bounds error deep inside the parser — as a failure, and that is precisely the class of bug a hostile client finds in a parser that reads untrusted input. On top of that free property you add assertions with the usual `t.Error`/`t.Fatal`. An `error` *returned* by the code under test is not a failure: most generated input is not a valid query, so rejecting it is the parser doing its job, and the idiom is to `return` early. ## Two ways to run it With no `-fuzz` flag, `go test` treats the target as a normal test and calls the callback once per seed entry. Nothing is generated, it takes milliseconds, and it makes a fine permanent regression suite — every input that ever broke you keeps being replayed. With `go test -fuzz=FuzzParseQuery`, the engine enters its search: it runs a coverage baseline over the seeds, then spawns worker processes that mutate inputs, and keeps any input that exercises a code path not seen before. When it finds a panic or a failed assertion it stops, prints the failing input, and saves it so the failure becomes reproducible. ## The shape of a good target - **Small and fast.** The callback runs millions of times; every microsecond and every allocation is multiplied by the iteration count. - **Deterministic.** The same input must behave the same way, or a saved failure will not reproduce. - **Self-contained.** No network, no filesystem, no state carried between calls in a package-level variable. - **Honest about invalid input.** Return early on the errors your code is supposed to produce; assert only on what it accepted. ## Common first mistakes Registering seeds with `f.Fuzz` instead of `f.Add`; putting assertions *after* the `f.Fuzz` call, where they never run; calling `t.Fatal(err)` whenever the parser rejects input, which makes the very first mutation look like a crash; and expecting a plain `go test` to fuzz anything at all.
- How many times may f.Fuzz be called in one fuzz target, and what happens to code written after it?Exactly once. `f.Fuzz` does not return until the run for that target is finished, so it is effectively the last statement of the target: setup shared by every invocation belongs above it, and anything written after it runs at most once, after the whole search, which is almost never what the author intended. A second `f.Fuzz` call is an error, not a second search.
- What does a plain go test do with a fuzz target when the -fuzz flag is absent?It runs it as an ordinary test: the callback is invoked once per seed corpus entry and nothing is generated. That is why fuzz targets are cheap to keep in a normal pipeline — they act as a regression suite over known-interesting inputs — and why a developer or a scheduled job has to pass `-fuzz` explicitly to actually search for new ones.
- Why is calling t.Fatal whenever the parser returns an error a mistake inside the callback?Almost every mutated input is not a valid query, so an error return is correct behaviour rather than a defect. If a returned error fails the target, the first mutation the engine tries looks like a crash and the search stops before it has explored anything. Return early on the expected error and reserve failure for panics and violated invariants.
saying these in an interview costs you the question
- Describes a fuzz target as a test that loops over random values
- Registers seed inputs with f.Fuzz instead of f.Add
- Puts assertions after the f.Fuzz call instead of inside the callback
- Thinks plain go test generates inputs without the -fuzz flag
- Gives the callback *testing.F as its first parameter