CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/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++.

80

Quality

100%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

SKILL.md

name:
go-native-fuzzing
description:
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

  • Go native fuzzing - go.dev/doc/security/fuzz.
  • Go testing package - pkg.go.dev/testing#F.
  • Composes: corpus-management-reference.
  • Sibling fuzzers: libfuzzer-cpp, afl-plus-plus, cargo-fuzz-rust, atheris-python-fuzzing, jazzer-jvm-fuzzing, ossfuzz-integration.
  • Dispatcher: fuzz-tool-selector.

SKILL.md

tile.json