gosentry is a security-focused fork of the Go toolchain. In a very simple phrasing, it's a copy of the Go compiler that finds bugs. If you are a security researcher auditing Go codebases, you should probably use this tool and consider it a great Swiss Army knife.
For now, it focuses on the following features:
- Integrating go-panikint: instrumentation that panics on integer overflow/underflow (and optionally on truncating integer conversions).
- Integrating LibAFL fuzzer: run Go fuzzing harnesses with LibAFL for better fuzzing performances.
- Proposing Grammar-based fuzzing using Nautilus: generate structured bytes/strings from a grammar.
- Panicking on user-provided function call: catching targeted bugs when certain functions are called (e.g.,
myapp.(*Logger).Error). - Git-blame-oriented fuzzing (based on this work): when fuzzing with LibAFL mode, you can orient the fuzzer toward recently added/edited lines.
- Detect race conditions, goroutine leaks, and timeout detection at fuzz-time: gosentry can replay newly found seeds (or timed-out executions) and treat these findings like bugs.
- Generate coverage reports from a fuzzing campaign.
It especially has two objectives:
- Being easy to use and UX-friendly (we're tired of complex tools),
- Helping to find bugs in Go codebases via built-in security implementations.
- Build
- Feature 1: Integer overflow and truncation issues detection
- Feature 2: Panic on selected functions
- Feature 3: LibAFL state-of-the-art fuzzing
- Feature 4: Git-blame-oriented fuzzing (experimental)
- Feature 5: Detect race conditions, goroutine leaks, and hangs at fuzz-time
- Feature 6: Grammar-based fuzzing (Nautilus)
- Feature 7: Generate fuzzing coverage reports from campaign
- Credits
cd src && ./make.bash # Produces `../bin/go` (or `./bin/go` from repo root). See `GOFLAGS` below.Tip
If youβre in src/, run the toolchain as ../bin/go ... (the binaries are in bin/ at repo root).
Start at docs/gosentry/index.md for:
- a code/architecture map (features β files),
- the recommended dev loop (fast feedback),
- CI entrypoints and benchmark scripts.
This work is inspired by the previously developed go-panikint. It adds overflow/underflow detection for integer arithmetic operations and (optionally) type truncation detection for integer conversions. When overflow or truncation is detected, a panic with a detailed error message is triggered, including the specific operation type and integer types involved.
Arithmetic operations: Handles addition +, subtraction -, multiplication *, and division / for both signed and unsigned integer types. For signed integers, covers int8, int16, int32. For unsigned integers, covers uint8, uint16, uint32, uint64. The division case specifically detects the MIN_INT / -1 overflow condition for signed integers. int64 and uintptr are not checked for arithmetic operations.
Type truncation detection: Detects potentially lossy integer type conversions. Covers all integer types: int8, int16, int32, int64, uint8, uint16, uint32, uint64. Excludes uintptr due to platform-dependent usage. This is disabled by default.
Overflow detection is enabled by default. To disable it, add GOFLAGS='-gcflags=-overflowdetect=false' before your ./make.bash. You can also enable truncation issues checker with: -gcflags=-truncationdetect=true
This feature patches the compiler SSA generation so that integer arithmetic operations and integer conversions get extra runtime checks that call into the runtime to panic with a detailed error message when a bug is detected. Checks are applied using source-location-based filtering so user code is instrumented while standard library files and dependencies (module cache and vendor/) are skipped.
You can read the associated blog post about it here.
Add a marker on the same line as the operation or the line immediately above to suppress a specific report:
- Overflow/underflow:
overflow_false_positive - Truncation:
truncation_false_positive
Example:
// overflow_false_positive
intentionalOverflow := a + b
// truncation_false_positive
x := uint8(big)
sum2 := a + b // overflow_false_positive
x2 := uint8(big) // truncation_false_positiveSometimes this might not work, that's because Go is inlining the function. If // overflow_false_positive isn't enough, add //go:noinline before the signature of your function.
When fuzzing targets, we may be interested in triggering a panic when certain functions are called. For example, some software may emit log.error messages instead of panicking, even though such conditions often indicate states that security researchers would want to detect during fuzzing.
However, these errors are usually handled internally (e.g., through retry or pause mechanisms, or by printing messages to logs), which makes them largely invisible to fuzzers. The objective of this feature is to address this issue.
Compile gosentry, then use the --panic-on flag.
./bin/go test -fuzz=FuzzHarness --use-libafl --focus-on-new-code=false --catch-races=false --catch-leaks=false --panic-on="test_go_panicon.(*Logger).Warning,test_go_panicon.(*Logger).Error"The example above would panic when either (*Logger).Warning or (*Logger).Error is called (comma-separated list).
How panic on selected functions feature works
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1) gosentry `go test` β
β - parses + validates `-panic-on=...` against packages being built β
β - forwards patterns to the compiler via `-panic-on-call=...` β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
v
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 2) `cmd/compile` β
β - prevents inlining of matching calls so the call stays visible β
β - SSA pass inserts a call to `runtime.panicOnCall(...)` β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
v
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 3) `runtime.panicOnCall` β
β - panics with: "panic-on-call: func-name" β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
In practice, this makes any matched call site behave like a crash/panic for fuzzers (note: only static call sites can be trapped).
LibAFL performs way better than the traditional Go fuzzer. When fuzzing (go test -fuzz=...), gosentry uses LibAFL by default (runner in golibafl/).
Important
go test -fuzz=... uses LibAFL by default, and you must explicitly pass --focus-on-new-code=..., --catch-races=..., and --catch-leaks=... (no implicit defaults). Use --use-libafl=false to switch back to Goβs native fuzzer.
When using LibAFL (default), you must explicitly choose whether to enable git-aware scheduling: --focus-on-new-code=true|false.
You must also explicitly choose whether to enable data race catching: --catch-races=true|false (see Feature 5).
You must also explicitly choose whether to enable goroutine leak catching: --catch-leaks=true|false (see Feature 5).
When a crash or failure is found, gosentry prints the Go backtrace above the LibAFL summary output (panic backtrace, and also stack traces for t.Fatal/t.Fatalf).
In multi-client mode, it also ensures the LibAFL run terminates cleanly even if one of the configured clients never connects (avoids CI hangs).
To opt out:
--use-libafl=false: use Go's native fuzzing engine instead of LibAFL.
More documentation in this Markdown file.
You can also pass an optional JSONC config file for LibAFL (including grammar fuzzing options), see here.
./bin/go test -fuzz=FuzzHarness --focus-on-new-code=false --catch-races=false --catch-leaks=false --libafl-config=path/to/libafl.jsonc # optional --libafl-configCoverage report generation from a LibAFL campaign corpus is documented in Feature 7.
Grammar-based fuzzing (Nautilus) is documented in Feature 6.
How Go + LibAFL are wired together
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1) gosentry `go test` β
β - captures `testing.F.Fuzz(...)` callback β
β - generates extra source file: `_libaflmain.go` β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
v
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 2) Generated bridge: `_libaflmain.go` β
β - provides libFuzzer-style C ABI entrypoints: β
β LLVMFuzzerInitialize β
β LLVMFuzzerTestOneInput β
β - adapts bytes -> Go types -> calls the captured fuzz callback β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
v
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 3) `libharness.a` (static archive on disk) contains: β
β - compiled objects for all test package (+ dependencies) β
β - generated `_testmain.go` + `_libaflmain.go` β
β - LLVMFuzzerInitialize β
β - LLVMFuzzerTestOneInput β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
v
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 4) `golibafl/` (Rust + LibAFL) β
β env: HARNESS_LIB=/path/to/libharness.a β
β fuzz loop: mutate input -> LLVMFuzzerTestOneInput(data) -> observe β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
In --use-libafl mode, gosentry builds libharness.a and the Rust golibafl runner drives it in-process via the libFuzzer entrypoints. Note: HARNESS_LIB can point to any harness archive name (for example libharness_race.a used by --catch-races).
Let's talk about the motivation behind using LibAFL. Fuzzing with go test -fuzz is far behind the state-of-the-art fuzzing techniques. A good example for this is AFL++'s CMPLOG/Redqueen. Those features allow fuzzers to solve certain constraints. Let's assume the following snippet
if input == "IMARANDOMSTRINGJUSTCMPLOGMEMAN" {
panic("this string is illegal")
}State-of-the-art (SOTA) fuzzers like AFL++ or LibAFL would find the panic instantly in that case. However, Go's native fuzzer wouldn't. That is a massive gap that restrains coverage exploration by a lot.
The benchmarks below show those limits. Note that those benchmarks can be reproduced and improved via the gosentry-bench-libafl repository.
The chart below is the evolution of the number of lines covered while fuzzing Google's UUID using LibAFL vs go native fuzzer.

The chart below is the evolution of the number of lines covered while fuzzing go-ethereum using LibAFL vs go native fuzzer.

You can test it on some fuzzing harnesses in test/gosentry/examples/.
cd test/gosentry/examples/reverse
../../../../bin/go test -fuzz=FuzzReverse --focus-on-new-code=false --catch-races=false --catch-leaks=falseStop the fuzz campaign with Ctrl+C.
gosentry stores LibAFLβs campaign state (corpus, crashes, etc.) under Goβs fuzz cache root (roughly $(go env GOCACHE)/fuzz), in a deterministic directory derived from the same package + same fuzz target (and the same project root).
This means that stopping (Ctrl+C) and restarting the same fuzz campaign will, by default, continue from the previous LibAFL queue/ corpus.
The path is printed at the end of the run:
libafl output dir: /full/path/to/.../fuzz/<pkg import path>/libafl/<project>/<harness>
Notes:
<harness>is the fuzz target name when-fuzzis a simple identifier likeFuzzXxx(or^FuzzXxx$), otherwise itβspattern-<hash>.- Coverage generation (
--generate-coverage) uses the same rule to find the rightqueue/corpus, so it must be run from the same package with the same-fuzz=....
Coverage-guided fuzzing is great at exploring new paths, but it treats all covered code as equally interesting. When fuzzing large codebases, you may want to bias the fuzzer toward recently modified code, where regressions and bugs are more likely to be introduced. In LibAFL mode, gosentry can use git blame to prefer inputs that execute recently changed lines (while keeping coverage guidance as the primary signal).
This work is based on previous work from LibAFL-git-aware. All the technical in-depth details are documented there.
Enable git-aware scheduling with --focus-on-new-code=true:
./bin/go test -fuzz=FuzzHarness --use-libafl --focus-on-new-code=true --catch-races=false --catch-leaks=falseThis mode needs git (to run git blame) and go tool addr2line to map coverage counters back to source file:line.
How git-blame-oriented fuzzing works
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1) gosentry `go test -fuzz` β
β - builds `libharness.a` (contains `go.o` + `.go.fuzzcntrs`) β
β - runs `golibafl` with `GOLIBAFL_FOCUS_ON_NEW_CODE=1` β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
v
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 2) `golibafl` generates a cached "git recency map" β
β - maps coverage counters -> (file:line) via `go tool addr2line` β
β - runs `git blame` to get a timestamp per line β
β - stores timestamps in `git_recency_map.bin` β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
v
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 3) LibAFL scheduler uses the recency map β
β - coverage decides what enters the corpus β
β - among the corpus, prioritize inputs that hit newer lines β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
How gosentry builds git_recency_map.bin
.go.fuzzcntrs is the linker section that holds Go's libFuzzer-style 8-bit coverage counters (enabled by -gcflags=all=-d=libfuzzer); each byte is "how many times this instrumented spot was hit". When --focus-on-new-code=true, golibafl generates git_recency_map.bin by:
- Extracting
go.ofromlibharness.a. - Reading the
.go.fuzzcntrssection size to get the counter countN. - Scanning
.textrelocations that reference.go.fuzzcntrssymbols to recover the address for each counter index. - Resolving each address to
file:lineusinggo tool addr2line. - Running
git blame --line-porcelainto getcommitter-timeper line. - Writing
git_recency_map.binasu64 head_time+u64 N+N * u64 timestamps(little-endian). Unmapped entries use timestamp0.
Benchmark 1 (go-ethereum / geth): baseline vs git-aware
Executed with misc/gosentry/bench_focus_on_new_code_geth.sh --trials 5 --warmup 600 --timeout 200.
gitaware_5: crash (7122ms)
baseline results:
trial 1: crash (107747ms)
trial 2: crash (146415ms)
trial 3: crash (37902ms)
trial 4: crash (154034ms)
trial 5: timeout (200000ms)
baseline crashes: 4/5 (timeouts=1, errors=0)
baseline median (capped to timeout): 146.415s
git-aware results:
trial 1: timeout (200000ms)
trial 2: crash (87432ms)
trial 3: crash (61733ms)
trial 4: crash (157540ms)
trial 5: crash (7122ms)
git-aware crashes: 4/5 (timeouts=1, errors=0)
git-aware median (capped to timeout): 87.432s
When fuzzing with LibAFL, a harness execution can timeout (for example because of a deadlock / goroutines stuck waiting, or an extremely slow path).
To reduce false positives, gosentry treats a timeout as a hang candidate and confirms it by replaying the timed-out input a few times with a larger timeout. On a confirmed hang, gosentry writes the input to <libafl output dir>/hangs/ and stops the fuzz campaign (treats it like a bug/crash).
Before exiting, golibafl attempts to minimize the crashing/hanging input (best-effort; hangs are capped to ~60s total).
Note: hang confirmation also runs during initial corpus import/generation, so targets that time out on every input can still be detected deterministically.
This is configured via --libafl-config:
catch_hangs(default:true)hang_timeout_ms(default:10000)hang_confirm_runs(default:3)
gosentry can run a separate -race replay loop that watches the LibAFL queue/ directory and replays newly discovered seeds with GORACE=halt_on_error=1.
The replay loop builds a separate -race harness archive for replay-only (no fuzz coverage instrumentation).
When a data race is detected during replay, gosentry prints the full race detector report before the catch-races: summary and repro command.
Note: Goβs race detector only detects data races inside a single harness execution (races between goroutines in the same process accessing the same memory without proper synchronization). --catch-races will miss races if the seed does not trigger the racy concurrency, and it does not detect cross-process races.
How data race mode works
This mode starts a small monitor inside go test (same parent process), and it runs for the whole fuzz campaign.
- When: before the main LibAFL fuzzing process is started, gosentry builds the replay harness + runner.
- Monitoring: before fuzzing starts, gosentry snapshots the initial contents of
<libafl output dir>/queue/into aseenset. A goroutine then polls<libafl output dir>/queue/every ~1s and only replays newly created seeds (skips dotfiles and*.metadata).
Legend: output/... = <libafl output dir>/...
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1) Main LibAFL fuzzing run β
β - `golibafl` writes new seeds to `output/queue/` β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
v
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 2) `--catch-races` sidecar setup β
β - builds replay harness: `libharness_race.a` (`go test -race ...`) β
β - builds replay runner: `golibafl-race` (linked against race harness) β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
v
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 3) Replay loop β
β - polls `output/queue/` for new seeds β
β - runs: `GORACE=halt_on_error=1 golibafl-race run --input <seed>` β
β (2 workers Γ 3 repeats per seed) β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
v
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 4) On "DATA RACE" β
β - prints the race detector report β
β - copies seed to `output/races/` β
β - stops the fuzz campaign (treat as bug/crash) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
gosentry can also run a goleak replay loop that watches the LibAFL queue/ directory and replays newly discovered seeds with go.uber.org/goleak enabled.
On a detected goroutine leak, gosentry prints the exact seed path and copies it into <libafl output dir>/leaks/.
Note: goleak is for goroutine leaks, not memory leaks.
How goroutine leaks mode works
This mode also starts a small monitor inside go test (same parent process), and it runs for the whole fuzz campaign.
- Monitoring: a goroutine polls
<libafl output dir>/queue/every ~1s and replays each new seed withGOSENTRY_LIBAFL_CATCH_LEAKS=1(enablesgo.uber.org/goleakafter each execution).
Legend: output/... = <libafl output dir>/...
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1) Main LibAFL fuzzing run β
β - `golibafl` writes new seeds to `output/queue/` β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
v
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 2) `--catch-leaks` sidecar setup β
β - builds replay runner: `golibafl-leak` (linked against the harness) β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
v
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 3) Replay loop β
β - polls `output/queue/` for new seeds β
β - runs: `GOSENTRY_LIBAFL_CATCH_LEAKS=1 golibafl-leak run --input <seed>`β
β (enables `go.uber.org/goleak` checks after each execution) β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
v
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 4) On "catch-leaks: detected goroutine leak" β
β - copies seed to `output/leaks/` β
β - stops the fuzz campaign (treat as bug/crash) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Enable goroutine leak catching with --catch-leaks=true or race catching with --catch-races=true
./bin/go test -fuzz=FuzzHarness --use-libafl --focus-on-new-code=false --catch-races=true --catch-leaks=trueByte-level fuzzing is great, but parsers and file formats often need structured inputs. With --use-grammar, gosentry uses LibAFLβs Nautilus grammar mutator to generate and mutate inputs that conform to a user-provided grammar (JSON format), and feeds them to your regular Go fuzz harness (testing.F.Fuzz).
In grammar mode, LibAFL still runs the normal coverage-guided loop (pick a corpus seed β mutate β execute β keep inputs that increase coverage). The runner adds Nautilus mutation (seed β grammar tree β mutate β unparse) plus (by default) a CMPLOG-guided, I2S-like stage that rewrites Nautilus leaf terminals based on runtime comparisons. This keeps inputs grammar-valid (it does not run the raw byte-level havoc/token stages in grammar mode). You can disable the CMPLOG/I2S stage in --libafl-config via nautilus_cmplog_i2s=false (byte-level fuzzing still keeps CMPLOG/I2S always on).
Note
Grammar mode is usually slower than byte-level fuzzing. It is a trade-off: more structure vs fewer executions per second.
For best results, use a one-arg fuzz callback that takes either a byte slice ([]byte) or a string:
f.Fuzz(func(t *testing.T, data []byte) { /* parse data */ })
// or:
f.Fuzz(func(t *testing.T, s string) { /* parse s */ })Grammar mode works best with a single input argument ([]byte or string). Multi-arg fuzz callbacks cause gosentry to decode the underlying byte buffer into separate values, so the original grammar-generated text wonβt stay intact.
Grammar mode still generates bytes/strings. If you need structured inputs (or youβre doing differential fuzzing), your harness is where you convert data into domain values (parse/unmarshal) and compare behaviors. (Outside of grammar mode, gosentry can also fuzz some composite Go types by decoding them from bytes.)
Requirements: no extra dependencies beyond the Rust toolchain already needed for LibAFL mode.
You can tune Nautilus via --libafl-config (only used with --use-grammar): nautilus_max_len and nautilus_cmplog_i2s (see misc/gosentry/libafl.config.jsonc).
Benchmark: Nautilus grammar CMPLOG/I2S stage (on vs off)
Executed on Feb 17, 2026 using the repoβs JSON grammar example (test/gosentry/examples/grammar_json, FuzzGrammarJSON, grammar testdata/JSON.json).
Results (LibAFL UserStats):
| mode | nautilus_cmplog_i2s |
run time | executions | exec/sec | edges |
|---|---|---|---|---|---|
| on | true |
1m-5s | 103818 | 1.586k | 388/8008 (4%) |
| off | false |
1m-0s | 256659 | 4.251k | 388/8008 (4%) |
Note: edges is LibAFLβs coverage map edges, not Go source lines.
Set GOSENTRY_VERBOSE_AFL=1 to print a few generated inputs. Set GOSENTRY_VERBOSE_AFL_ALL_INPUTS=1 to print every grammar-mode execution as GOLIBAFL_MUTATED_INPUT "..." (very noisy).
If you need to create a new Nautilus JSON grammar for your own target format/protocol, gosentry ships:
- An LLM-ready prompt: misc/gosentry/nautilus/prompt.md
- A small set of example grammars: misc/gosentry/nautilus/examples/
Go fuzz harness example (JSON)
package mypkg
import (
"bytes"
"encoding/json"
"io"
"testing"
)
func FuzzGrammarJSON(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
dec := json.NewDecoder(bytes.NewReader(data))
dec.UseNumber()
var v any
if err := dec.Decode(&v); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if err := dec.Decode(&struct{}{}); err != io.EOF {
t.Fatalf("invalid JSON: trailing data")
}
})
}Differential fuzzing harness sketch (two parsers):
f.Fuzz(func(t *testing.T, data []byte) {
gotA, errA := ParseA(data)
gotB, errB := ParseB(data)
if (errA == nil) != (errB == nil) {
t.Fatalf("parser disagreement: A=%v B=%v", errA, errB)
}
_ = gotA
_ = gotB
})Nautilus JSON grammar example (small JSON subset)
This is the file format expected by --grammar=...:
- The grammar is a JSON array of rules:
["NonTerm", "RHS"]. - Nonterminal names must start with a capital letter (
Value,Object, ...). - Use
{NonTerm}in the RHS to reference another rule. {and}are reserved for nonterminal references; to emit literal braces, use\\{and\\}in the RHS string.
[
["Json", "{Value}"],
["Value", "null"],
["Value", "{String}"],
["String", "\"{Chars}\""],
["Chars", ""],
["Chars", "{Char}{Chars}"],
["Char", "a"],
["Char", "b"]
]How grammar fuzzing works in gosentry
Legend: output/... = <libafl output dir>/...
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 0) gosentry `go test -fuzz=FuzzXxx` (LibAFL + --use-grammar) β
β - captures your `testing.F.Fuzz` callback + its parameter types β
β - builds `libharness.a` (libFuzzer-style entrypoints for LibAFL) β
β - runs `golibafl fuzz ... --use-grammar --grammar ...` β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
v
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1) `golibafl` (Rust + LibAFL) fuzzes the Go harness in-process β
β - loads `libharness.a` via `HARNESS_LIB=...` β
β - observers: edges + time (+ cmplog for comparisons) β
β - feedback/objective: coverage/time/crash (and optional hang handling) β
β - scheduler selects a corpus seed (coverage-guided) β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
v
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 2) Nautilus (in-process, per client) β
β - loads the JSON grammar into a Nautilus context β
β - fuzz loop stage: parse seed -> mutate tree -> unparse to bytes β
β - if the seed is not parseable: fall back to generation-from-scratch β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
v
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 3) Grammar mode stages β
β - initial corpus: if input dir empty, call `generate` N times β
β - fuzz loop: corpus seed -> grammar mutate -> exec harness β
β - new coverage inputs are added to the on-disk corpus (`output/queue/`) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Limitations (current glue):
- Grammar mode works best with a single input argument; multi-arg fuzz targets will decode the underlying byte buffer into separate values.
- No grammar recombination/crossover between two corpus seeds yet (mutation is single-seed).
After (or while) running a LibAFL fuzz campaign, gosentry can generate a Go coverage report by replaying the current LibAFL queue corpus (no fuzzing).
# Same package + same fuzz target as your fuzz campaign:
./bin/go test -fuzz=FuzzHarness --generate-coverage .Notes:
- Use the same
-fuzzspelling you used for the campaign you want to replay. gosentry uses-fuzzto locate the LibAFL output directory (and itsqueue/corpus).-fuzz=FuzzHarnessand-fuzz='^FuzzHarness$'refer to the same campaign.- If you fuzzed with a broader regexp (example:
-fuzz='Fuzz.*Parser'), reuse the exact same regexp for coverage.
-fuzzmust match exactly one fuzz target, otherwise gosentry canβt select which campaign to replay.
This replays inputs from <libafl output dir>/queue/ and writes:
<libafl output dir>/coverage/cover.out(Go coverprofile format)<libafl output dir>/coverage/cover.html(HTML report)
At the end, gosentry prints the full paths to both files.
Note
For large corpora, consider -timeout=0.