Where does `go test -fuzz` save an input that makes a fuzz target fail, and what replays it later?
answer
- written into your source tree, not a cache
- package directory, under a testdata subfolder
- one file per failing input, named by hash
- plain go test replays it as a subtest
- committing it makes a permanent regression test
basics
~20 sThe fuzzing engine writes the failing input into a new file under testdata/fuzz/<FuzzName>/ in the package's own directory. That file becomes a seed corpus entry, so every later plain go test run replays it, with no -fuzz flag needed.
solid answer
~50 sWhen `go test -fuzz=FuzzParseFrame ./proxy` finds an input that panics or fails the target, it minimizes the input and writes it to `proxy/testdata/fuzz/FuzzParseFrame/<hash>` in the package's source directory, then prints that path and a `-run` selector that replays just that entry. Files under `testdata/fuzz/<FuzzName>/` are the seed corpus: an ordinary `go test ./proxy` with no `-fuzz` flag runs each of them once, as a subtest named after the file. So the crasher becomes a permanent, deterministic regression test the moment you commit it. The file is small and textual - a `go test fuzz v1` header line followed by one line per fuzz argument written as `type(value)` - so it reviews like source rather than like a binary blob. Because it lands in your working tree and not in a cache, it survives only if you actually commit or archive it.
code
text · 2 linesgo test fuzz v1
[]byte("\x02\x00\x00\x00\xff")go deeper
Be ready to say that the failing input is written into testdata/fuzz under the package, and that committing that file is what turns a one-off crash into a test that runs forever.
Explain the mechanics: the entry is a text file with a go test fuzz v1 header, it becomes a subtest named after the file, and a plain go test executes it with no fuzzing involved.
Show that you treat the file as the output of the fuzz run - it has to be archived or committed before the workspace disappears, and it should land in the same change as the fix.
Own the consequence: every committed entry runs on every build from then on, and in a public repository it publishes a working trigger for a bug your users may not have patched yet.
### What a fuzz failure actually produces A Go fuzz target is a test function the toolchain can call with generated inputs. Running `go test -fuzz=FuzzParseFrame ./proxy` on a package that parses a binary protocol frame will eventually feed it a byte sequence that makes it panic or report an error. Two things then happen, in this order. First the engine **minimizes** the input: it re-runs the target on smaller and simpler variants, keeping ones that still fail, so that what you get is a compact repro rather than the four kilobytes of random noise that happened to trip the parser. Second it **writes the minimized input into your source tree**, at `proxy/testdata/fuzz/FuzzParseFrame/<hash>`, where the directory is named after the fuzz function and the file is named by a hash of its contents. The failure output names the path it wrote, and also prints the `go test -run=...` command that replays that single entry. The important word is *source tree*. The file is created in the package directory of the working copy the test ran in - not in a cache, not in a temporary directory. On a laptop, `git status` shows it as a new untracked file the moment the run finishes. ### What is inside the file A corpus entry is plain text. The first line is a format marker, `go test fuzz v1`. Each following line is one argument to the fuzz function, written as a Go-style typed literal - `[]byte("...")`, `string("...")`, `int(7)`, `bool(true)` and so on. The number and order of those lines must match the target's argument list, which is why a corpus file for a two-argument target has two value lines. Because entries are text, they diff cleanly, review like source, and can be hand-written. If you know a frame with a length prefix larger than the remaining bytes should be rejected, you can add that case as a file yourself rather than waiting for the engine to stumble on it. ### Why `testdata/fuzz` is the magic path `testdata` is the conventional directory name that the `go` tool skips when it looks for packages, so anything under it is data rather than code. Inside it, `fuzz/<FuzzName>/` has a defined meaning to the testing package: every file in that directory is a **seed corpus entry** for that fuzz target. Seed entries are used in two distinct situations: - During a `-fuzz` run they are executed first, before any generated input, as the starting points for the coverage-guided search. - During an **ordinary** `go test` - no `-fuzz` flag anywhere, including whatever your CI already runs - the fuzz target still executes, once per seed entry, as a normal deterministic test. Each entry appears as a subtest named after its file. That second bullet is the whole payoff. A committed crasher is not "some fuzzing artefact"; it is a regression test that runs in milliseconds on every build, forever, without anyone needing to enable fuzzing. ### Replaying one entry Because each entry is a subtest, the standard `-run` selector picks it out: `go test -run='FuzzParseFrame/1f0a9c2e' ./proxy` No `-fuzz`, no randomness, no time budget - it feeds exactly that one saved input and reports pass or fail. This is the fastest possible loop while you are fixing the bug, and it is what the failure output tells you to run. ### If you never commit it The entry only exists in the working tree it was written into. A discarded container, a `git clean`, or a colleague reproducing on their own machine all leave you with nothing but a log line. The coverage-guided corpus that led the engine to the input is separate again and machine-local, so re-running fuzzing may take a very long time to rediscover the same case, or never do so. Treat the file as the deliverable of the fuzz run: commit it beside the fix, or at minimum archive it, before the machine that produced it goes away.
- How do you re-run only that one saved crasher instead of the whole fuzz target?Each file in `testdata/fuzz/FuzzParseFrame` runs as a subtest named after the file, so `go test -run='FuzzParseFrame/1f0a9c2e' ./proxy` executes just that entry with no fuzzing at all. The failure output prints exactly that command when it saves the file. Since no `-fuzz` flag is involved it is fast, deterministic and works anywhere, including an ordinary CI test job.
- What is actually inside one of those corpus files?Plain text. The first line is `go test fuzz v1`, identifying the format, and each following line is one argument written as a Go-style typed literal such as `[]byte("...")` or `int(7)`, in the same order as the target's parameters. Because it is text it diffs and reviews like source, and you can hand-write an entry to pin a case you care about.
- If nobody commits the file, is the input lost?Effectively yes. It exists only in the working tree that produced it, so a discarded container or a `git clean` takes it away. The coverage-guided corpus that led the engine there lives in the build cache, which is machine-local too, so a fresh fuzzing run may need a very long time to rediscover the same input - or may never reach it.
saying these in an interview costs you the question
- Says the crasher is stored in the module cache
- Thinks a plain go test ignores testdata/fuzz entries
- Believes the saved entry is an opaque binary blob
- Assumes a later fuzzing run will rediscover it automatically
- Expects to reproduce the bug from the log line alone