Map

Go fuzzing was missing half the toolkit. We forked the toolchain to fix it.

author
Kevin Valerio
publisher
Trail of Bits
date
2026-05-12
captured
2026-05-12

Go fuzzing was missing half the toolkit. We forked the toolchain to fix it.

By Kevin Valerio | May 12, 2026

Introduction

Go's native fuzzing capabilities lag significantly behind advanced tooling available in the Rust, C, and C++ ecosystems. The Go team at Trail of Bits recognized critical gaps in path constraint solving, structured input handling, and bug detection capabilities. In response, they created gosentry, a purpose-built fuzzing fork of the Go toolchain that maintains familiar developer workflows while leveraging more sophisticated fuzzing infrastructure underneath.

Why we built gosentry

The development team encountered recurring obstacles when using Go's standard fuzzer for security research:

  • Complex conditional branches created unsolvable path constraints
  • Grammar-based fuzzing approaches were unavailable
  • Type-aware fuzzing demanded manual intervention
  • Several Go-specific bug classes—integer overflows, goroutine leaks, data races, and execution timeouts—wouldn't trigger failures automatically
  • Campaign analysis and reporting proved cumbersome
  • Detecting critical error conditions required source code modifications

These limitations motivated building a more capable alternative that preserves the familiar testing interface developers already know.

Same harness, stronger engine

Gosentry maintains backward compatibility with existing Go fuzzing patterns. Developers write fuzz targets using the standard testing.F API:

func FuzzExample(f *testing.F) {
    f.Add(exampleValue)
    f.Fuzz(func(t *testing.T, input []byte) {
        TargetFunction(input)
    })
}

The implementation difference is significant: gosentry "captures the fuzz callback, builds a Go archive with libFuzzer-style entry points, and runs it in-process through a Rust-based LibAFL runner." This architectural choice allows teams to leverage existing harnesses without porting them to new frameworks.

More bugs become visible

Gosentry detects failure modes that vanilla Go fuzzing misses. The toolchain includes compiler-inserted integer overflow checks and integrates go-panikint for truncation detection. Additional capabilities include:

  • Race condition detection via the native Go race detector
  • Goroutine leak identification through goleak integration
  • Timeout detection for identifying infinite loops
  • Custom panic triggers using the --panic-on flag for codebases that log errors without crashing

Better inputs

Struct-aware fuzzing

Native Go fuzzing restricts inputs to basic types, excluding composite structures. Gosentry extends support to structs, slices, arrays, and pointers:

type Input struct {
    Data []byte
    S    string
    N    int
}

func FuzzStructInput(f *testing.F) {
    f.Add(Input{Data: []byte("hello"), S: "world", N: 42})
    f.Fuzz(func(t *testing.T, in Input) {
        Process(in)
    })
}

The fuzzer still mutates bytes internally but handles encoding and decoding transparently, eliminating the need for custom wire format development.

Grammar-based fuzzing

Gosentry integrates Nautilus for grammar-guided input generation. Rather than producing random mutations that fail early parsing stages, grammar mode generates structurally valid inputs matching defined rules:

func FuzzGrammarJSON(f *testing.F) {
    f.Add(`{"postOfficeBox":123}`)
    f.Fuzz(func(t *testing.T, jsonInput string) {
        ParseJSONFromString(jsonInput)
    })
}

Grammar rules are specified as JSON arrays:

[
  ["Json", "\\{\"postOfficeBox\":{Number}\\}"],
  ["Number", "{Digit}"],
  ["Number", "{Digit}{Number}"],
  ["Digit", "0"],
  ["Digit", "1"],
  ...
]

What it has found already

Early campaigns using grammar-based differential fuzzing uncovered vulnerabilities in critical infrastructure projects:

  • Optimism/Kona Protocol: Unknown batch type handling caused denial-of-service panics
  • Optimism Projects: Brotli channel disagreement between Kona and op-node implementations
  • Optimism Stack: Frame parsing mismatches against specification
  • Revm: Failed deposit handling failed to bump nonce, causing state root mismatches with other clients

These represent exactly the failure modes that standard Go fuzzing would struggle to discover.

Usage and availability

The basic usage pattern leverages familiar CLI conventions:

./bin/go test -fuzz=FuzzHarness --focus-on-new-code=false --catch-races=true --catch-leaks=true

Coverage reports can be generated from existing campaigns:

go test -fuzz=FuzzTarget --generate-coverage

Gosentry is available on GitHub with comprehensive documentation for each feature.