Safeguard
Developer Tools•

A checklist for a good fuzz target

Fast, deterministic, no exits, no global state, bounded memory. A short checklist for fuzz targets that find bugs instead of stalling the engine.

Safeguard Security Team
4 min read

A good fuzz target is fast, deterministic, tolerant of any input, and narrow in scope. It does not exit, print, touch the disk, or keep state between runs. Most stalled fuzzing campaigns come from a target that breaks one of those rules, not from a lack of CPU.

The checklist

Use this before you start a long run.

  1. Accept any input. The Google fuzzing guidance says a target must tolerate any kind of input: empty, huge, malformed.
  2. Never exit or abort. Terminating the process is treated as a bug and hides the real ones.
  3. Be deterministic. Behaviour should depend only on the input bytes. Avoid unseeded randomness, clocks, and anything that varies between runs.
  4. Be fast. The same guidance suggests roughly 1,000 executions per second per core as a rule of thumb, more for light targets, and treats under 10 per second as a sign of trouble.
  5. Keep memory small. A heavy target slows coverage growth, and leaks or large allocations end the run early.
  6. No input or output. Do not print, write files, or read files, except for one time setup.
  7. Minimal global state. State that survives between calls makes crashes hard to reproduce. Go's fuzzing docs say the state of a target should not persist past each call and it should not depend on global state.
  8. Do setup once. Build expensive objects at initialization, not on every input.
  9. Bound the input size. Reject inputs outside a sensible range early.
  10. Stay narrow. One target per format. Split a big API into subsets.

Why speed matters so much

Coverage-guided fuzzing is a numbers game. If a target runs 100 executions per second, it tries 360,000 inputs an hour. At 10,000 per second it tries 36 million. The second run explores far more of the program in the same time, which is why trimming wasteful work is often a better use of a day than buying a bigger machine.

Determinism and reproducibility

When the fuzzer saves a crashing input, you must be able to replay it and see the same crash. If the target reads the current time or a random seed, the replay may not fail, and you lose the bug. The same goes for threads: Google's guidance suggests creating thread pools at init, not per input, and joining any threads before returning.

Scope and structure

A target that tries to parse five formats at once spends its effort on dispatch. Split it. A fuzzer working on one parser builds a corpus that fits that parser, which also makes coverage numbers readable.

For formats with checksums or strict grammars, plan on a dictionary or a structure-aware approach, because plain byte mutation will not get past validation. Those have their own posts.

Keep it in the repo

The strongest setup keeps targets in the project, built by the project, and run over the saved corpus in ordinary tests. The OSS-Fuzz guidance describes this ideal integration: targets maintained by code owners in version control, a seed corpus checked in, and regression runs in CI. When a fix lands, add the bug-triggering input to the corpus so it cannot come back.

Quick check

Is a slow fuzz target a problem if it finds bugs?

Yes, because it finds them slowly and misses the ones a faster run would reach. Profile it and remove logging, file access and repeated setup.

Can a harness call a function that uses threads?

It can, but keep thread creation out of the per-input path and join threads before returning, or results become hard to reproduce.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.