Map
โ†‘Supply Chain Security

gosentry โ€” Trail of Bits' Go Fuzzing Fork

Wiki summarygolangfuzzingsecuritytrail-of-bits โ†ณ show in map Markdown
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
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 []byte or 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-on flag 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
  • 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