Fuzzing Test Skill (Go)¶
Generate high-signal Go fuzz tests only when targets are suitable.
Quick Reference¶
| When you need to… | Jump to |
|---|---|
| Determine if a target is suitable for fuzzing | §Applicability Gate (run first, always) |
| Choose among multiple candidate targets | §Target Priority Gate + load references/target-priority.md |
| Write the fuzz harness | §Harness Templates |
| Handle a discovered crash | §Crash Documentation + load references/crash-handling.md |
| Set up CI integration | Load references/ci-strategy.md |
| Diagnose a slow / ineffective fuzz run | Load references/advanced-tuning.md |
| Avoid common fuzzing mistakes | Load references/anti-examples.md |
Load References Selectively¶
When target suitability is ambiguous (borderline cases): → Load references/applicability-checklist.md for the full suitability decision tree with edge-case guidance.
When 3+ candidate targets need prioritization: → Load references/target-priority.md for the bug-finding-yield ranking criteria and tie-breaking rules.
When a fuzz run discovers a crash that needs documentation: → Load references/crash-handling.md for crash triage steps, corpus commit policy, and regression test patterns.
When the user requests CI integration for fuzz tests: → Load references/ci-strategy.md for GitHub Actions configuration, corpus caching, and time-budget settings.
When diagnosing an ineffective fuzz run, OOM, leaks, flaky failures, or performance issues: → Load references/advanced-tuning.md for seed quality analysis, skip-rate diagnosis, allocation profiling, and harness simplification patterns.
When you need to avoid or check for common fuzzing mistakes (trivial targets, missing oracle, bad seeds, OOM, global state, time-based assertions, dropped corpus): → Load references/anti-examples.md for 7 BAD/GOOD code patterns covering the most frequent harness mistakes.
Applicability Gate (Must Run First)¶
Before writing any fuzz code, evaluate suitability. If the target fails this gate, the entire remaining workflow is skipped — output the verdict, suggest alternatives, and stop.
Mark each item Pass / Fail:
- Target has meaningful input space (not trivial fixed-path logic).
- Target can be driven by Go fuzz-supported parameter types.
- Target has clear oracle/invariant:
- no panic for any input
- round-trip (
decode(encode(x)) == x) - differential consistency
- domain constraints/properties
- Target is mostly deterministic/local (not dominated by DB/network/clock/global mutable state).
- Target is fast enough for high-iteration fuzzing.
Hard stop:
- If item
2or3fails: - output
Applicability Verdict: Not suitable for fuzzing - list concrete failed checks with specific code references
- suggest alternative strategy (unit/integration/property tests)
- stop (do not write fuzz tests)
Additional Gates¶
Target Priority Gate¶
When multiple candidates exist, prioritize by bug-finding yield:
- Parsers/decoders/protocol handlers
- Serialization/deserialization round-trip paths
- State transitions with strict invariants
- Differential comparison candidates (new vs ref implementation)
If only low-yield targets exist, state that explicitly before writing broad fuzz suites.
Risk and Cost Gate¶
Classify fuzz effort:
Low: pure function, fast, localMedium: moderate CPU/memory, bounded guards neededHigh: expensive path, heavy allocations, strict budget required
Set budget policy per class:
Low: local fuzz 30-60sMedium: local fuzz 15-45s + stricter input guardsHigh: corpus-only in PR, fuzz run in scheduled/nightly jobs
Execution Integrity Gate¶
Never claim fuzz commands ran unless actually executed.
If not run, output: - Not run in this environment - reason - exact commands to run
Output Contract¶
Always start with:
Applicability VerdictWhy(2-6 concrete bullets)Action
Then:
- If unsuitable: stop.
- If suitable: implement fuzz tests and report execution status.
Implementation Workflow (Only If Suitable)¶
- Identify target and
Oracle/invariant. - Select fuzz mode:
- parser robustness
- round-trip
- differential
- multi-parameter
- Seed with
f.Add(...)— mine real data, do NOT invent fake seeds:
Seed mining strategy (run these before writing f.Add calls):
a. Grep existing unit tests for real inputs:
Grep for function calls to the fuzz target in *_test.go files
→ extract literal arguments as seeds
b. Scan testdata/ directories:
Glob for testdata/**/* and testdata/fuzz/**/*
→ use file contents as []byte seeds
c. Scan fixtures/examples in the repo:
Glob for fixtures/, examples/, samples/, *.golden
→ use as domain-representative seeds
d. Extract from production-like config/data files:
Read any .json, .yaml, .proto, .csv files that match the target's input type
→ use real payloads, not hallucinated ones
Seed categories (each f.Add should cover ≥3 of these): - valid inputs (mined from tests/testdata above) - boundary values (empty, max-length, single-element) - malformed/known-bad inputs (truncated, corrupted headers) - structurally distinct cases (different branches/variants) 4. Implement FuzzXxx in *_test.go. 5. Add harness guards: - add a Size guard - bound max length/size - skip impossible combos with t.Skip - avoid external side effects 6. Run checks: - corpus/regression: go test -run=^FuzzXxx$ . - short fuzz: go test -run=^$ -fuzz=^FuzzXxx$ -fuzztime=30s . 7. If crash found and fixed: - retain corpus under testdata/fuzz/FuzzXxx/ - add deterministic regression assertion if applicable
Crash Handling (Mandatory)¶
When fuzz finds a failure:
- Capture minimal reproducible command.
- Keep crashing input in corpus path.
- Record failure type:
- panic
- invariant violation
- timeout/resource blowup
- Fix with minimal code change.
- Re-run corpus regression and short fuzz run.
- Report root cause + prevention guard.
Use format in references/crash-handling.md.
CI Strategy¶
Use two-lane strategy (see references/ci-strategy.md):
- PR lane:
- run corpus replay (
go test -run=^Fuzz) - optional short fuzz only for low-cost targets
- Scheduled lane (nightly/periodic):
- run bounded fuzz time per package
- upload artifacts/crash corpus
Minimal Templates¶
Template A: Parser ([]byte)¶
func FuzzParseXxx(f *testing.F) {
f.Add([]byte{})
f.Add([]byte{0x01, 0x00})
f.Fuzz(func(t *testing.T, data []byte) {
if len(data) > 1<<20 {
t.Skip()
}
out, err := ParseXxx(data)
if err != nil {
return
}
if !isValid(out) {
t.Fatalf("invalid parsed result: %+v", out)
}
})
}
Template B: Round-Trip¶
func FuzzRoundTripXxx(f *testing.F) {
f.Add("seed", int32(1))
f.Fuzz(func(t *testing.T, a string, b int32) {
if len(a) > 1<<16 {
t.Skip()
}
orig := Obj{A: a, B: b}
enc, err := Encode(orig)
if err != nil {
t.Skip()
}
got, err := Decode(enc)
if err != nil {
t.Fatalf("decode(encode(x)) failed: %v", err)
}
if got != orig {
t.Fatalf("round-trip mismatch: got=%+v want=%+v", got, orig)
}
})
}
Template C: Differential¶
func FuzzDiffXxx(f *testing.F) {
f.Add("hello,world", ",")
f.Fuzz(func(t *testing.T, s, sep string) {
if sep == "" || len(s) > 1<<16 {
t.Skip()
}
got := ImplNew(s, sep)
want := ImplRef(s, sep)
if !equal(got, want) {
t.Fatalf("diff mismatch: got=%v want=%v", got, want)
}
})
}
Template D: Struct-Aware (Multi-Parameter with []byte Deserialize)¶
Use when the target needs a complex struct that exceeds Go's native fuzz parameter types. Feed []byte and deserialize into the struct inside the harness:
func FuzzProcessRequest(f *testing.F) {
// seed with known-good serialized inputs
seed1, _ := json.Marshal(Request{Method: "GET", Path: "/api/v1/users", Body: ""})
seed2, _ := json.Marshal(Request{Method: "POST", Path: "/api/v1/users", Body: `{"name":"x"}`})
f.Add(seed1)
f.Add(seed2)
f.Fuzz(func(t *testing.T, data []byte) {
if len(data) > 4096 {
t.Skip()
}
var req Request
if err := json.Unmarshal(data, &req); err != nil {
t.Skip() // invalid structure, not interesting
}
// now fuzz with a well-typed struct
resp, err := ProcessRequest(req)
if err != nil {
return // expected error path
}
if resp.StatusCode < 100 || resp.StatusCode > 599 {
t.Fatalf("invalid status code: %d", resp.StatusCode)
}
})
}
Key points: - t.Skip() on unmarshal failure to let the fuzzer focus on structurally valid inputs. - Seed with multiple structurally distinct valid inputs to help coverage-guided exploration. - Bound len(data) to avoid spending time on enormous payloads.
Deserialization strategy (choose by performance need):
| Method | Speed | When to use |
|---|---|---|
json.Unmarshal | Slow (~10-50 μs/op) | Quick prototyping, human-readable seeds, low-iteration targets |
encoding/gob | Medium (~2-10 μs/op) | Better throughput when seed readability is not needed |
encoding/binary.Read | Fast (~0.1-1 μs/op) | Performance-sensitive targets needing max execs/sec |
go-fuzz-headers GenerateStruct | Fast + structured | Complex structs with nested fields; see go-fuzz-headers bridge below |
For high-iteration fuzzing (targets <1 μs/call), prefer encoding/binary or go-fuzz-headers over JSON — the deserialization overhead can dominate total execution time and reduce bug-finding yield.
Fuzz vs Property-Based Testing¶
- Use fuzz when: inputs are byte/string-like, you want crash discovery, or target is a parser/decoder.
- Use property-based (
rapid/gopter) when: inputs need complex generators with domain constraints, ort.Skip-based filtering would waste >80% of iterations. - Use both when: fuzz for crash discovery + property-based for domain invariants on the same target.
Corpus Management¶
- Always commit crashing inputs under
testdata/fuzz/FuzzXxx/— these are regression tests. - Do not commit the Go fuzz cache (
$GOCACHE/fuzz/) — it's large and machine-specific. - Selectively commit high-value seed inputs that cover distinct code paths. Avoid committing hundreds of auto-generated entries.
- Clean cache:
go clean -fuzzcache
Go Version Gate¶
Check go.mod before generating native fuzz code:
| Go version | Guidance |
|---|---|
1.18 | Native testing.F is available. Baseline for this skill. |
1.20 | Prefer current corpus layout and CI patterns. |
1.21 | Re-check package performance and memory budgets before extending fuzz time. |
1.22 | Be explicit about loop variable semantics when adapting older code examples. |
If Go < 1.18, native fuzzing is unavailable — stop and recommend property tests or legacy go-fuzz only with explicit justification.
Race Detection + Fuzz¶
When the target touches goroutines, shared caches, or normalization pipelines with internal concurrency:
- run corpus replay with
go test -race -run=^FuzzXxx$ . - if runtime is acceptable, run a short fuzz burst with
-race - document when
-raceis skipped because the package is too slow for a bounded fuzz window
Fuzz Worker Parallelism¶
Tune concurrency deliberately:
- cap
GOMAXPROCSwhen CPU saturation hides determinism issues - use
-parallelcarefully; higher worker counts can reduceexecs/secon allocation-heavy targets - if a target is memory-heavy, lower worker count before increasing fuzz time
go-fuzz-headers bridge¶
For complex binary or protocol-heavy inputs, go-fuzz-headers can bootstrap structured data from bytes:
consumer := fuzz.NewConsumer(data)
var req Request
if err := consumer.GenerateStruct(&req); err != nil {
t.Skip()
}
Use GenerateStruct only when native fuzz parameter types are too limiting and the target still has a strong oracle.
Fuzz Performance Baseline¶
Record a baseline before scaling up:
- approximate
execs/sec - average allocation profile if known
- skip rate estimate
- time budget used for the measurement
If execs/sec is too low for meaningful exploration, simplify the harness before asking for longer fuzz windows.
Quality Scorecard¶
After generating fuzz tests, evaluate quality. Mark each item Pass / Fail.
Critical (all must pass for overall PASS)¶
| # | Check | Criteria |
|---|---|---|
| C1 | Applicability gate ran | Verdict documented before any code |
| C2 | Oracle/invariant present | Every f.Fuzz body has at least one t.Fatal/t.Errorf asserting a property |
| C3 | Size guard present | len(data) > N or equivalent bound in every []byte/string harness |
Standard (≥4/5 must pass)¶
| # | Check | Criteria |
|---|---|---|
| S1 | Seed quality | f.Add(...) includes ≥3 structurally distinct valid inputs |
| S2 | Fuzz mode matches target | Parser → robustness, codec → round-trip, migration → differential |
| S3 | Skip rate bounded | t.Skip() usage justified; estimated skip rate <50% |
| S4 | Harness isolation | No network/DB/clock/global-state dependency in harness body |
| S5 | Corpus policy stated | Where to commit, what to exclude, cache strategy |
Hygiene (≥3/4 must pass)¶
| # | Check | Criteria |
|---|---|---|
| H1 | Naming convention | FuzzXxx matches target name, file is *_test.go |
| H2 | Cost class assigned | Low/Medium/High with matching -fuzztime budget |
| H3 | t.Cleanup for resources | Fuzz target that opens resources uses t.Cleanup |
| H4 | Quick commands provided | Exact go test commands for corpus replay + short fuzz |
Scoring: - PASS: All Critical pass AND ≥4/5 Standard AND ≥3/4 Hygiene - FAIL: Any Critical fails → overall FAIL regardless of other scores
Guardrails¶
- Do not fuzz targets requiring live DB/network unless fully stubbed.
- Do not use flaky assertions tied to time/random/global state.
- Do not generate fuzz code when applicability gate fails.
- Keep memory/time bounded in harness.
- Do not commit fuzz cache (
$GOCACHE/fuzz/) to git — only committestdata/fuzz/. - If skip rate exceeds 50%, re-evaluate seed strategy before continuing.
Quick Commands¶
- One target fuzz:
go test -run=^$ -fuzz=^FuzzXxx$ -fuzztime=30s . - All fuzz targets in package:
go test -run=^$ -fuzz=^Fuzz -fuzztime=1m . - Corpus replay only:
go test -run=^FuzzXxx$ . - Clean fuzz cache:
go clean -fuzzcache
Skill Maintenance¶
Run regression checks for this skill with: