Fuzzing Tutorial

Purpose and Scope

Fuzzing is a testing technique in which the Go tool repeatedly calls a test function with generated inputs, looking for inputs that expose crashes, panics, invalid assumptions, or security-relevant edge cases. In Go’s tutorial flow, a developer writes ordinary code, adds a unit test, then adds a fuzz target and runs it with the go command. The public workflow is intentionally close to regular testing: the same package test files are used, seed inputs are supplied by the test, and failures are saved so that they can be reproduced later.

This page explains that workflow from the perspective of the Go repository implementation. The files in src/internal/fuzz are not application APIs, but they reveal the runtime model used behind go test fuzzing: a coordinator starts worker processes, seed values and generated values are represented as corpus entries, coverage is captured when supported, and crash-inducing inputs can be minimized and written to a corpus directory. Understanding these pieces helps you reason about why fuzzing behaves differently from a simple loop inside a unit test.

Sources: src/internal/fuzz/fuzz.go, src/internal/fuzz/encoding.go, src/internal/fuzz/coverage.go

Relevant Source Files

  • src/internal/fuzz/fuzz.go — defines the internal fuzzing package, CoordinateFuzzingOpts, and CoordinateFuzzing, which coordinates worker processes, limits, timeouts, seed corpus values, cache directories, and crash reporting.
  • src/internal/fuzz/encoding.go — implements the persisted corpus file encoding used for fuzz input values, including the go test fuzz v1 header and formatting for supported primitive types.
  • src/internal/fuzz/coverage.go — manages coverage snapshots, difference detection, bit counting, and coverage subset checks used to decide whether an input is interesting.
  • src/internal/fuzz/counters_supported.go — exposes instrumented edge counters as a byte slice on supported operating system and architecture combinations.
  • src/internal/fuzz/counters_unsupported.go — provides the fallback behavior when coverage instrumentation is not supported for the current platform.
  • src/internal/fuzz/mem.go — defines shared memory used by the coordinator and worker processes to exchange fuzz inputs efficiently.

Core Concepts

A fuzz target is a test-like function that accepts values generated by the fuzzing engine. The official Go tutorial introduces this by asking the reader to write a fuzz test for a simple function, run the go command, inspect a failing input, and then fix the code. The important distinction from a unit test is not the assertion style; it is the input source. Unit tests usually enumerate cases chosen by the developer, while fuzzing starts from seed values and explores additional values automatically.

The internal implementation reflects that split between human-provided seeds and machine-generated exploration. CoordinateFuzzingOpts includes Seed []CorpusEntry, described as values added by the fuzz target with testing.F.Add and values found in testdata. It also includes Types []reflect.Type, which must match the values in the seed corpus. That design means fuzzing is not typeless random byte injection at the public boundary; the engine knows the argument types for a target and must preserve those types while encoding, mutating, sending, and reproducing inputs.

Sources: src/internal/fuzz/fuzz.go, src/internal/fuzz/encoding.go

A corpus is the collection of input values known to the fuzzer. Some values are seed inputs chosen by the test author because they exercise normal behavior or known boundaries. Other values are discovered while fuzzing because they increase coverage or trigger a failure. The implementation distinguishes a CorpusDir, where files containing values that crash the code may be written, from a CacheDir, which contains additional interesting values that the fuzzer may derive from and update. That separation is useful for developers: crashers are reproducible test artifacts, while the fuzz cache is an optimization for future exploration.

Coverage-guided fuzzing needs a way to determine whether an input explored something new. In the supplied source, coverage.go snapshots 8-bit edge counters, rounds each counter down to a power of two, compares snapshots against a base, and counts newly observed coverage bits. This makes coverage more compact than a raw execution trace while still allowing the coordinator to decide that a generated input is interesting. The public tutorial’s note that coverage instrumentation is available only on certain architectures is backed by separate supported and unsupported counter implementations.

Sources: src/internal/fuzz/coverage.go, src/internal/fuzz/counters_supported.go, src/internal/fuzz/counters_unsupported.go

Tutorial Workflow

Start fuzzing the same way you start most Go examples: create a module, write a small function, and add tests. The official tutorial uses a new directory and go mod init before adding code, because fuzzing is integrated into the normal module-aware go command workflow. A minimal learning sequence is to write the function, add a table-driven unit test for obvious examples, then add a fuzz target that calls the same function with generated data and checks a property that should always hold.

A fuzz target should express an invariant rather than only one expected output. For example, if a function parses and then formats a value, the fuzz target might check that valid formatted output can be parsed again, or that invalid input returns an error instead of panicking. The related Go tutorial material on returning and handling errors is relevant here: a good fuzz target often treats expected invalid input as an ordinary error and treats panics, inconsistent results, or impossible states as bugs.

A typical command sequence for the public workflow looks like this:

mkdir fuzz
cd fuzz
go mod init example/fuzz
go test ./...
go test -fuzz=Fuzz ./...

The first go test run verifies that normal tests pass before random exploration begins. The fuzzing run selects fuzz targets matching the -fuzz pattern and then coordinates generated executions. Internally, CoordinateFuzzing validates the context, fills in defaults such as discarding logs when no writer is provided, chooses a parallelism level from GOMAXPROCS when Parallel is zero, and avoids starting more workers than the configured generated-input limit requires. If a timeout is configured, it wraps the fuzzing context with a deadline.

Sources: src/internal/fuzz/fuzz.go

When a failure occurs, the useful next step is reproduction, not guesswork. The implementation supports returning an error with crash information, and the corpus encoding code shows why saved inputs can be replayed: corpus files have a deterministic textual format beginning with go test fuzz v1, followed by typed values. Supported values include integer and unsigned integer forms, booleans, floats, strings, runes, bytes, and byte slices. Special handling preserves unusual floating-point NaN bit patterns, infinities, negative zero, invalid runes, and byte-oriented data so that failures depending on representation are not accidentally normalized away.

Sources: src/internal/fuzz/encoding.go

System-to-Code Mapping

ConceptImplementation evidenceDeveloper meaning
Fuzz coordinatorCoordinateFuzzing(ctx context.Context, opts CoordinateFuzzingOpts)Drives workers, applies limits and deadlines, and reports crashes.
Seed corpusCoordinateFuzzingOpts.Seed and TypesInputs added by the fuzz target and testdata must match the target argument types.
Crash corpusCoordinateFuzzingOpts.CorpusDirReproducible failing values may be written to a package corpus directory.
Fuzz cacheCoordinateFuzzingOpts.CacheDirInteresting non-crashing values can guide future fuzzing.
Coverage signalResetCoverage, SnapshotCoverage, diffCoverage, countNewCoverageBitsThe engine decides whether an input reached new code paths.
Platform supportcounters_supported.go and counters_unsupported.goCoverage instrumentation depends on the build target.
Worker exchangesharedMem, sharedMemHeader, sharedMemTempFileCoordinator and workers pass fuzz values through mapped temporary files.

The coordinator-worker design explains why fuzzing can use parallel CPU resources without turning the test function itself into concurrent application code. Workers run the same binary with a fuzz-worker flag prepended, while the coordinator manages cancellation and result collection. Shared memory gives those processes an efficient data channel for generated values, but the source explicitly notes that the shared memory object provides no synchronization on its own. Coordination is therefore a protocol layered above the memory mapping, not a guarantee provided by the byte slice.

Sources: src/internal/fuzz/fuzz.go, src/internal/fuzz/mem.go

Corpus Encoding and Supported Values

Corpus files are meant to be understandable and stable enough for reproduction. marshalCorpusFile writes a version line and then one typed expression per argument. For strings and byte slices it uses quoted forms, for numeric values it preserves type names, and for floating-point edge cases it chooses encodings that can reconstruct values exactly enough for fuzz failure reproduction. This matters when the bug depends on a boundary value rather than on a broad category such as “some NaN” or “some byte slice.”

The implementation also shows why the public documentation says Go fuzzing supports a subset of built-in types. The encoder has explicit cases for the supported set and panics for unsupported types. From a tutorial perspective, that means a first fuzz target should use simple argument lists such as string, []byte, integers, booleans, or floating-point values. If the code under test needs a structured object, construct that object inside the fuzz target from supported primitive inputs, then check the property you care about.

Sources: src/internal/fuzz/encoding.go

Coverage, Platforms, and Execution Limits

Coverage is optional at the platform level but central to efficient fuzzing when available. On supported targets, coverage() returns the region between linker-known _counters and _ecounters symbols as a byte slice. On unsupported targets, coverage() returns nil. coverage.go then derives coverageEnabled from the length of that slice, so higher-level fuzzing logic can operate with a clear signal about whether instrumentation is present. The unsupported file also documents the need to keep platform constraints aligned with internal/platform.FuzzInstrumented.

Execution controls exist to make fuzzing practical in local development and continuous testing. CoordinateFuzzingOpts includes a wall-clock Timeout, a generated-input Limit, minimization controls, and Parallel. Minimization is the process of shrinking a failing input while keeping the failure reproducible; it is disabled when both minimization timeout and minimization limit are zero. As a reader, use short fuzz runs while developing the target, then increase duration once the invariant and seed corpus are useful.

Sources: src/internal/fuzz/fuzz.go, src/internal/fuzz/coverage.go, src/internal/fuzz/counters_supported.go, src/internal/fuzz/counters_unsupported.go

Practical Next Steps

To write a useful fuzz test, begin with a property that should hold for every supported input, not just with a list of examples. Add a few seed values that represent normal inputs, empty inputs, boundary cases, and previously fixed bugs. Run ordinary tests first, then run fuzzing and let the go command explore. When it finds a failing input, keep the saved corpus file, reproduce the failure, fix the underlying code, and leave the input in the corpus so the bug remains covered.

After this tutorial, read the testing page for how go test organizes package tests, examples, benchmarks, caching, and output. If your fuzz target crosses package or module boundaries, review the modules overview and dependency-management pages so the test environment is reproducible. For performance-sensitive fuzzing, continue to the diagnostics, coverage, race detector, and execution trace pages; fuzzing often uncovers bugs fastest when combined with the rest of Go’s diagnostic toolchain.