gosentry โ Trail of Bits' Go Fuzzing Fork
- title
- gosentry โ Trail of Bits' Go Fuzzing Fork
- type
- summary
- summary
- Kevin Valerio (Trail of Bits) introduces gosentry, a Go toolchain fork that wraps the standard testing.F API around a LibAFL+Nautilus engine for grammar, struct, race, leak, and overflow detection
- parent
- supply-chain-security
- sources
- gosentry-go-fuzzing-fork
- tags
- golang, fuzzing, security, trail-of-bits
- created
- 2026-05-12
- updated
- 2026-05-12
Kevin Valerio's trail-of-bits blog post announces gosentry, a fork of the Go toolchain aimed at the gap between Go's built-in fuzzer and what Rust/C/C++ security researchers expect from a fuzzing campaign. The pitch is "same harness, stronger engine" โ write func FuzzX(f *testing.F) exactly as before, run a forked go test -fuzz=โฆ, get LibAFL underneath.
Why a fork
The Go team at Trail of Bits hit recurring limits with vanilla go test -fuzz:
- Complex conditional branches produced path constraints the standard mutator could not solve
- No grammar-based input generation (structurally valid inputs were a manual problem)
- No struct/slice/array/pointer fuzzing โ input had to be
[]byteor a basic type, anything else required custom encoding - Several Go-specific bug classes silently slipped past: signed integer overflows, goroutine leaks, data races, infinite loops
- Reporting was minimal; campaign analysis took external tooling
- Catching application-defined error conditions required source modification
Instead of writing a new fuzzer with a new API, gosentry intercepts the existing f.Fuzz callback, builds a Go archive exposing libFuzzer-style entry points, and runs it in-process under a Rust libafl-based runner.
What it adds
Bug detection that vanilla Go misses:
- Compiler-inserted integer overflow checks; go-panikint integration for truncation
- The native Go race detector wired into the fuzzing loop
- goleak integration for goroutine-leak detection
- Timeout detection (catches infinite loops)
--panic-onflag turning specific log lines or error conditions into a fuzzing crash, for codebases that log instead of panic
Better inputs (structure-aware-fuzzing):
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 mutates bytes internally and handles encoding/decoding for arbitrary struct types โ no hand-written wire format wrapper around the target function.
Grammar-based fuzzing (grammar-based-fuzzing) via Nautilus: grammar rules in a JSON-array format, used to generate inputs that pass early parsing stages instead of dying at the first byte. The example walks through a JSON grammar that the fuzzer uses to produce values matching {"postOfficeBox": <digits>}.
What it has found
Early campaigns, primarily using grammar-based differential fuzzing between client implementations, produced bugs in production crypto infrastructure:
- Optimism / Kona Protocol โ unknown batch type handling produced a DoS panic
- Optimism โ Brotli channel disagreement between the Kona and op-node implementations of the same spec
- Optimism Stack โ frame parsing diverged from the spec
- Revm โ failed deposit handling didn't bump the nonce, producing a state-root mismatch with other Ethereum clients
These are all consensus bugs, where one implementation accepts what another rejects โ exactly what grammar + differential fuzzing was invented to catch and what Go's []byte-in random-mutation loop is poorly suited for.
Usage
./bin/go test -fuzz=FuzzHarness \
--focus-on-new-code=false \
--catch-races=true \
--catch-leaks=true
Coverage reporting:
go test -fuzz=FuzzTarget --generate-coverage
The CLI surface is the regular go test -fuzz interface plus a small set of new flags โ the migration cost from upstream Go is meant to be near zero.
Tradeoffs the post doesn't dwell on
The article is an introduction, not an audit. A few costs are implicit:
- It's a toolchain fork โ staying in sync with upstream Go releases is ongoing maintenance, and the fuzzed binary won't be byte-identical to the production build
- LibAFL runner means a Rust toolchain in the build environment
- Coverage instrumentation, race detection, and goleak all together carry significant runtime overhead โ fine for fuzzing, but campaign tuning is non-trivial
- Bus factor on Trail of Bits โ see gosentry for the watchlist entry
Related
- t-context-go-testing โ the other Go-testing-API note in the wiki, on
T.Context()lifetime scoping - clippy-stricter-config โ the Rust analogue of "the stock tooling is too permissive, here's the tighter config"; gosentry is the stronger version of the same instinct (fork instead of configure)
- supply-chain-security โ fuzzing as a pre-release defense layer, especially around third-party-implemented protocol code