Testland
Browse all skills & agents

go-native-fuzzing

Author and run Go's native fuzzing (Go 1.18+) - coverage-guided fuzzing built into the standard testing package via FuzzXxx functions. Covers f.Add seed-corpus declaration, f.Fuzz callback signature with typed parameters, testdata/fuzz/<FuzzXxx>/ directory layout for seeds + regression cases, the -fuzz flag for `go test`, and CI integration via short smoke runs. Use for fuzz testing Go libraries - Go's native approach integrates seamlessly with standard `go test` rather than requiring a separate toolchain like AFL++.

go-native-fuzzing

Overview

Per go.dev/doc/security/fuzz, unlike libFuzzer / AFL++, Go's native fuzzer is:

  • Native: no separate toolchain
  • Typed: f.Fuzz(func(t *testing.T, s string, n int) { ... })
  • Integrated: failing inputs auto-saved as test fixtures

For sanitiser pairing: Go uses the race detector (-race) as its TSan-equivalent; ASan-equivalent comes via gcflags (limited).

For corpus discipline see corpus-management-reference.

When to use

  • Fuzzing a Go library function (parser, encoder, validator).
  • A function with typed scalar / string / byte-slice inputs (Go's native fuzzer handles these without a FuzzedDataProvider).
  • Lightweight CI fuzz pass alongside go test.

For binary-level fuzzing of Go programs, AFL++ in -Q mode also works.

Authoring

Define a fuzz target

In any _test.go file alongside your unit tests:

package parser

import "testing"

func FuzzParseQuery(f *testing.F) {
    f.Add("SELECT * FROM users WHERE id = 1")
    f.Add("INSERT INTO foo VALUES (1, 'bar')")
    f.Add("")

    f.Fuzz(func(t *testing.T, q string) {
        result, err := ParseQuery(q)
        if err != nil {
            return
        }
        if result == nil {
            t.Fatalf("ParseQuery returned nil result with no error for %q", q)
        }
    })
}

Per go.dev/doc/security/fuzz:

  • The function name must start with Fuzz
  • The parameter is *testing.F
  • f.Add(seed1, seed2, ...) adds seed inputs
  • f.Fuzz(fn) registers the fuzz callback; fn takes *testing.T followed by typed parameters

Supported parameter types

Go's fuzzer supports these types as fuzz parameters:

TypeNotes
[]byteVariable-length byte slices
stringVariable-length strings
boolSingle byte
byte, runeIntegers
int, int8, int16, int32, int64Signed integers
uint, uint8, uint16, uint32, uint64Unsigned integers
float32, float64Floats

Multi-parameter functions are fuzzed jointly:

f.Fuzz(func(t *testing.T, port int, host string, body []byte) {
    handleRequest(host, port, body)
})

The fuzzer mutates all parameters together.

Seed corpus

Two sources for seeds:

  1. Inline f.Add(...) calls - versioned in code, executed on every test run.
  2. Files in testdata/fuzz/FuzzXxx/ - versioned text files, one per seed, in the same package.

The seed file format (per Go docs):

go test fuzz v1
string("SELECT * FROM users WHERE id = 1")
int(42)

For multi-parameter targets, each line corresponds to a parameter in order.

Failure auto-save

When go test -fuzz=Xxx finds a failure, it writes the failing input to testdata/fuzz/FuzzXxx/<sha256>. On the next go test run, this file becomes a regular regression test that must pass - no more -fuzz flag needed.

This is the unique strength of Go's approach: failing inputs become permanent regression coverage as part of the test fixture.

Running

Fuzz a target

# Run the unit tests + seeds (no exploration)
go test ./parser/

# Fuzz a specific target for 30 seconds
go test -fuzz=FuzzParseQuery -fuzztime=30s ./parser/

# Fuzz indefinitely (CI long-running)
go test -fuzz=FuzzParseQuery ./parser/

Common flags

FlagEffect
-fuzz=NAMERun the fuzz target with the given name
-fuzztime=DURATIONStop after duration (e.g., 30s, 1h) or -1 for indefinite
-fuzzminimizetime=DURATIONTime spent minimising failures (default 1m)
-fuzzcachedir=PATHWhere to cache mutations (default $GOCACHE/fuzz/)
-parallel=NConcurrent workers
-raceEnable race detector (TSan-equivalent)

Race detector

Pair fuzzing with the race detector for thread-safety bugs:

go test -race -fuzz=FuzzConcurrentAccess -fuzztime=10m ./...

Parsing results

When a failure occurs, Go prints:

--- FAIL: FuzzParseQuery (3.45s)
    --- FAIL: FuzzParseQuery/c1d4e1...
    fuzz: minimizing 50-byte failing input file
    --- FAIL: FuzzParseQuery (0.00s)
        parser_test.go:18: ParseQuery returned nil result with no error for "..."

    Failing input written to testdata/fuzz/FuzzParseQuery/c1d4e1abc...

    To re-run:
    go test -run=FuzzParseQuery/c1d4e1abc... ./parser/

Per the Go docs, the failing input lives at testdata/fuzz/FuzzXxx/<hash> - commit it as part of the fix to lock in regression coverage.

Reproducing a saved failure

go test -run=FuzzParseQuery/c1d4e1abc... ./parser/

This treats the saved file as a regular t.Run sub-test, no fuzzing.

CI integration

- uses: actions/setup-go@v5
  with: { go-version: '1.22' }
- name: Run tests (incl. seeds)
  run: go test -race ./...
- name: Smoke fuzz (3 min per target)
  run: |
    for target in $(grep -rh "^func Fuzz" --include="*_test.go" | \
                    awk '{print $2}' | sed 's/(.*//'); do
      echo "Fuzzing $target"
      go test -fuzz=$target -fuzztime=180s ./... || true
    done
- name: Commit any new regression fixtures
  if: always()
  run: |
    if git diff --quiet testdata/; then exit 0; fi
    git config user.name "fuzz-bot"
    git config user.email "fuzz-bot@example.com"
    git add testdata/
    git commit -m "Add fuzz failure fixtures"
    # PR or push — per team convention

Anti-patterns

Anti-patternWhy it failsFix
Missing f.Add callsFuzzer starts from empty corpus; slow path discoveryAdd 3-10 representative seeds
Skipping t.Fatalf for invariant violationsFuzzer can't detect logical bugsAssert invariants explicitly
Not committing testdata/fuzz/Lose regression coverage on next runCommit alongside the fix
-fuzztime=10s in CIToo short to find anything newUse 1-5 min smoke; long campaigns separate
One huge fuzz targetSlow iteration; unclear coverage attributionSplit per function
Fuzz target without -raceMisses concurrency bugs in concurrent codego test -race -fuzz=...
Ignoring testdata/ after CI fuzzNew regression fixtures lostCommit + PR them automatically

Limitations

  • Go-only. Doesn't fuzz CGo or C dependencies; for those, use AFL++ or libFuzzer with a Go wrapper.
  • Typed parameter only. No []byte → struct mutation beyond the supported types; complex inputs need manual unmarshalling in the fuzz body.
  • No native ASan equivalent. Go's GC + memory safety make many ASan-style bugs impossible; race detector covers concurrency.
  • Slower than libFuzzer for tight loops - coverage instrumentation is per-block via gcflags.
  • -fuzz is single-target. Can't fuzz multiple FuzzXxx simultaneously in one go test invocation; loop or use -jobs.

References