# gosentry — Trail of Bits' Go Fuzzing Fork

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]]):

```go
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-fuzzer|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 [[differential-fuzzing|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

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

Coverage reporting:

```bash
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 [[toolbox/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
