
Toolchain Go orientata alla sicurezza, focalizzata su capacità di fuzzing all'avanguardia.
gosentry è un fork della toolchain Go incentrato sulla sicurezza, che integra numerose funzionalità per campagne di fuzzing all'avanguardia su codebase Go. Se prima usavi go test -fuzz, dovresti usare gosentry come sostituto.
Include vari miglioramenti per il fuzzing e rilevatori di bug che non sono presenti nativamente nella toolchain Go. Vedi TLDR; qui sotto. Puoi anche leggere l'articolo del blog associato qui.
TLDR (funzionalità e opzioni):
struct (nessun parser personalizzato necessario). Aggiungi seed con f.Add(Input{N: 7, S: "hi"}) e poi f.Fuzz(func(t *testing.T, in Input) { ... }).X + Y - Z può diventare X / U + Z - 14 invece di X + Yè - Zcd src && ./make.bash # Produces ../bin/go. See GOFLAGS below.
> [!TIP]
> Documentazione per contributori: leggi `docs/gosentry/index.md` per una mappa del codice, il ciclo di sviluppo consigliato, i punti di ingresso della CI e gli script di benchmark.
> Questo fork utilizza la Pull GitHub App per aprire e unire automaticamente le PR da `golang/go:master` in `master`, assicurandoci di non rimanere mai indietro rispetto agli ultimi aggiornamenti della toolchain Go.
## Funzionalità 1: Fuzzing consapevole delle struct (fuzz di struct come input)
#### Panoramica
Il fuzzing nativo di Go (`go test -fuzz=...`) supporta solo un piccolo insieme di tipi scalari come parametri di fuzz (`[]byte`, `string`, numeri, ...). In gosentry, puoi anche fare fuzz di **tipi compositi** costruiti da quegli scalari: struct, array, slice e puntatori.
Questo è utile quando il tuo codice accetta naturalmente input strutturati e non vuoi costruire un encoder/decoder personalizzato solo per inizializzare e mutare il corpus.
Vedi `test/gosentry/examples/multiargs` e `test/gosentry/examples/composite` per esempi.
#### Esempio semplice```go
type Input struct {
Data []byte
S string
N int
OK bool
}
func FuzzStructInput(f *testing.F) {
// Seed the initial corpus with a Go struct (gosentry feature).
f.Add(Input{Data: []byte("A"), S: "B", N: 7, OK: true})
f.Fuzz(func(t *testing.T, in Input) {
if in.OK && in.N == 1337 && in.S == "BOOMMOOB" && bytes.Equal(in.Data, []byte("A")) {
t.Fatalf("boom")
}
})
}
f.Add) e il fuzzing delle struct (la colla creata)Il fuzzer nativo di Go non può fare fuzzing direttamente su un valore struct (sa solo mutare una piccola lista di tipi scalari). gosentry aggiunge un piccolo strato di colla: quando il tuo fuzz target usa tipi compositi (come Input), gosentry esegue il fuzzing su un singolo []byte dietro le quinte. A ogni esecuzione, decodifica quei byte nella tua struct (campo per campo, ricorsivamente per slice/array/puntatori) e poi chiama la callback f.Fuzz con il valore decodificato. La stessa codifica viene usata per i seed, quindi f.Add(Input{...}) diventa una voce di corpus []byte codificata che il fuzzer può riutilizzare e mutare come qualsiasi altro seed.
I fuzzer (incluso LibAFL) mutano byte grezzi, quindi vogliamo un decoder che possa trasformare qualsiasi slice di byte in un valore di struct "qualsiasi" e continuare. JSON/gob rifiuterebbero la maggior parte degli input casuali (male per la copertura), e inoltre non popolano i campi non esportati, mentre il fuzzing trae spesso beneficio dalla rottura degli invarianti. Questo formato personalizzato è piccolo, veloce, deterministico e tollerante ai dati malformati.
Dietro le quinte, usa il semplice formato binario di gosentry (non gob, non JSON). Il codice si trova in :
Questo lavoro trae ispirazione dal go-panikint sviluppato in precedenza. Aggiunge il rilevamento di overflow/underflow per le operazioni aritmetiche sugli interi e (opzionalmente) il rilevamento del troncamento di tipo per le conversioni tra interi. Quando viene rilevato overflow o troncamento, viene generato un panic con un messaggio di errore dettagliato, che include il tipo di operazione specifico e i tipi di interi coinvolti.
Operazioni aritmetiche: Gestisce addizione +, sottrazione -, moltiplicazione * e divisione / sia per i tipi interi con segno che senza segno. Per gli interi con segno, copre int8, int16, int32. Per gli interi senza segno, copre uint8, uint16, uint32, uint64. Il caso della divisione rileva specificamente la condizione di overflow MIN_INT / -1 per gli interi con segno. int64 e uintptr non sono controllati per le operazioni aritmetiche.
Rilevamento del troncamento di tipo: Rileva conversioni tra tipi interi potenzialmente con perdita di dati. Copre tutti i tipi interi: int8, int16, int32, int64, uint8, uint16, uint32, uint64. Esclude uintptr a causa dell'uso dipendente dalla piattaforma. Questa funzione è disabilitata per impostazione predefinita.
Il rilevamento di overflow è abilitato per impostazione predefinita. Per disabilitarlo, aggiungi GOFLAGS='-gcflags=-overflowdetect=false' prima del tuo ./make.bash. Puoi anche abilitare il controllo dei problemi di troncamento con: -gcflags=-truncationdetect=true
Questa funzionalità modifica la generazione SSA del compilatore in modo che le operazioni aritmetiche sugli interi e le conversioni tra interi ricevano controlli runtime aggiuntivi che chiamano il runtime per generare un panic con un messaggio di errore dettagliato quando viene rilevato un bug. I controlli vengono applicati usando un filtro basato sulla posizione nel codice sorgente, così il codice utente viene strumentato mentre i file della libreria standard e le dipendenze (cache dei moduli e vendor/) vengono saltati.
Puoi leggere il post del blog associato qui.
Aggiungi un marcatore sulla stessa riga dell'operazione o sulla riga immediatamente precedente per sopprimere un report specifico:
overflow_false_positivetruncation_false_positiveEsempio:```go // overflow_false_positive intentionalOverflow := a + b // truncation_false_positive x := uint8(big) sum2 := a + b // overflow_false_positive x2 := uint8(big) // truncation_false_positive
A volte questo potrebbe non funzionare, perché Go sta in-lineando la funzione. Se `// overflow_false_positive` non basta, aggiungi `//go:noinline` prima della firma della tua funzione.
## Funzionalità 3: Panico su funzioni selezionate
Quando si eseguono test di fuzzing sui target, potrebbe interessarci innescare un panico quando vengono chiamate determinate funzioni. Ad esempio, alcuni software possono emettere messaggi `log.error` invece di andare in panico, anche se tali condizioni spesso indicano stati che i ricercatori di sicurezza vorrebbero rilevare durante il fuzzing.
Tuttavia, questi errori sono solitamente gestiti internamente (ad esempio tramite meccanismi di retry o pausa, oppure stampando messaggi nei log), il che li rende in gran parte invisibili ai fuzzer. L'obiettivo di questa funzionalità è risolvere questo problema.
#### Come usarlo
Compila gosentry, poi usa il flag `--panic-on`.```bash
./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"
L'esempio precedente andrebbe in panic quando viene chiamata (*Logger).Warning o (*Logger).Error (elenco separato da virgole).
LibAFL offre prestazioni di gran lunga migliori rispetto al tradizionale fuzzer Go. Quando si esegue il fuzzing (go test -fuzz=...), gosentry utilizza LibAFL per impostazione predefinita (runner in golibafl/).
Nota sulla stabilità: in modalità LibAFL, gosentry forza GODEBUG=updatemaxprocs=0 (disabilita gli aggiornamenti automatici di GOMAXPROCS a runtime) per evitare un crash intermittente della CI su Linux ("sync: inconsistent mutex state"). I dettagli sono in misc/gosentry/USE_LIBAFL.md.
Quando si utilizza LibAFL (di default), è necessario scegliere esplicitamente se abilitare la pianificazione git-aware: --focus-on-new-code=true|false. Maggiori informazioni in questo file Markdown.
Puoi anche passare un file di configurazione JSONC opzionale per LibAFL (incluse le opzioni di fuzzing grammaticale), vedi qui.
Con "stop_all_fuzzers_on_panic": false, LibAFL salva ogni crash e riavvia il proprio client per continuare il fuzzing.```bash
./bin/go test -fuzz=FuzzHarness --focus-on-new-code=false --catch-races=false --catch-leaks=false --libafl-config=path/to/libafl.jsonc # optional --libafl-config
Use `-fuzztime=1m` per fermare una campagna LibAFL dopo un minuto.
La generazione del report di copertura dal corpus di una campagna LibAFL è documentata nella [Funzione 8](#feature-8-generate-go-coverage-reports-from-fuzzing-campaign).
Il fuzzing basato su grammatiche (Nautilus) è documentato nella [Funzione 7](#feature-7-grammar-based-fuzzing-nautilus).
<details>
<summary><strong>Come Go + LibAFL sono collegati tra loro</strong></summary>```text
┌───────────────────────────────────────────────────────────────────────────┐
│ 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 compila libharness.a e il runner Rust golibafl lo esegue in-process tramite gli entrypoint di libFuzzer. Nota: HARNESS_LIB può puntare a qualsiasi nome di archivio dell'harness (ad esempio libharness_race.a usato da --catch-races).
In modalità --use-libafl, gosentry compila l'harness Go con la strumentazione della copertura abilitata. Questa aggiunge piccoli contatori al codice che cambiano quando diverse parti del tuo programma vengono eseguite. Quando l'harness viene avviato all'interno di golibafl, il runtime Go espone questi contatori a LibAFL. LibAFL li legge dopo ogni input per vedere quale codice è stato eseguito e usa quella copertura per guidare le mutazioni successive.
Parliamo della motivazione dietro l'uso di LibAFL. Il fuzzing con go test -fuzz è molto indietro rispetto alle tecniche di fuzzing all'avanguardia. Un buon esempio è CMPLOG/Redqueen di AFL++. Queste funzionalità consentono ai fuzzer di risolvere determinati vincoli. Supponiamo il seguente frammento di codice```go
if input == "IMARANDOMSTRINGJUSTCMPLOGMEMAN" {
panic("this string is illegal")
}
SOTA fuzzers come AFL++ o LibAFL troverebbero il panic istantaneamente in quel caso. Tuttavia, il fuzzer nativo di Go non lo troverebbe. Questo è un divario enorme che limita **molto** l'esplorazione della copertura.
I benchmark qui sotto mostrano questi limiti. Nota che questi benchmark possono essere **riprodotti** e migliorati tramite il [repository gosentry-bench-libafl](https://github.com/kevin-valerio/gosentry-bench-libafl/tree/main).
##### Benchmark 1:
Il grafico seguente mostra l'evoluzione del numero di righe coperte durante il fuzzing di [UUID](https://github.com/google/uuid) di Google con LibAFL rispetto al fuzzer nativo di Go.

##### Benchmark 2:
Il grafico seguente mostra l'evoluzione del numero di righe coperte durante il fuzzing di [go-ethereum](https://github.com/ethereum/go-ethereum) con LibAFL rispetto al fuzzer nativo di Go.

#### Esempio
Puoi testarlo su alcuni harness di fuzzing in `test/gosentry/examples/`.```bash
cd test/gosentry/examples/reverse
../../../../bin/go test -fuzz=FuzzReverse --focus-on-new-code=false --catch-races=false --catch-leaks=false
Ferma la campagna di fuzzing con Ctrl+C.
gosentry memorizza lo stato della campagna di LibAFL (corpus, crash, ecc.) sotto la root della cache di fuzzing di Go (circa $(go env GOCACHE)/fuzz), in una directory deterministica derivata dallo stesso pacchetto + stesso target di fuzzing (e dalla stessa root del progetto).
Questo significa che fermando (Ctrl+C) e riavviando la stessa campagna di fuzzing, per impostazione predefinita si continuerà dal precedente corpus queue/ di LibAFL.
Il percorso viene stampato al termine dell'esecuzione:```text libafl output dir: /full/path/to/.../fuzz//libafl//
Note:
- `<harness>` è il nome del target di fuzzing quando `-fuzz` è un semplice identificatore come `FuzzXxx` (o `^FuzzXxx$`), altrimenti è `pattern-<hash>`.
- La generazione della coverage (`--generate-coverage`) usa la stessa regola per trovare il corpus `queue/` giusto, quindi deve essere eseguita dallo stesso package con lo stesso `-fuzz=...`.
## Funzionalità 5: fuzzing orientato a Git blame (sperimentale)
#### Panoramica
Il fuzzing guidato dalla coverage è ottimo per esplorare nuovi percorsi, ma tratta tutto il codice coperto come ugualmente interessante. Quando si esegue il fuzzing su codebase di grandi dimensioni, potrebbe essere utile orientare il fuzzer verso il codice modificato di recente, dove è più probabile che vengano introdotte regressioni e bug. In modalità LibAFL, gosentry può usare `git blame` per preferire input che eseguono righe modificate di recente (mantenendo comunque la guida della coverage come segnale primario).
Questo lavoro si basa su un precedente lavoro di [LibAFL-git-aware](https://github.com/kevin-valerio/LibAFL-git-aware). Tutti i dettagli tecnici approfonditi sono documentati lì.
#### Come usarlo
Abilita la schedulazione git-aware con `--focus-on-new-code=true`:```bash
./bin/go test -fuzz=FuzzHarness --use-libafl --focus-on-new-code=true --catch-races=false --catch-leaks=false
Questa modalità richiede git (per eseguire git blame) e go tool addr2line per mappare i contatori di copertura alla sorgente file:line.
git_recency_map.bin.go.fuzzcntrs è la sezione del linker che contiene i contatori di copertura a 8 bit in stile libFuzzer di Go (abilitati con -gcflags=all=-d=libfuzzer); ogni byte indica "quante volte è stato raggiunto questo punto instrumentato". Quando --focus-on-new-code=true, golibafl genera git_recency_map.bin:
go.o da libharness.a..go.fuzzcntrs per ottenere il numero di contatori N..text che referenziano i simboli di .go.fuzzcntrs per recuperare l'indirizzo di ciascun indice di contatore.Eseguito con misc/gosentry/bench_focus_on_new_code_geth.sh --trials 5 --warmup 600 --timeout 200.```text
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
</details>
## Funzionalità 6: Rilevare race condition, goroutine leak e blocchi (timeout) durante il fuzzing
##### Rilevamento di blocchi confermati (timeout di LibAFL)
Quando si esegue il fuzzing con LibAFL, un'esecuzione dell'harness può andare in **timeout** (ad esempio a causa di un deadlock / goroutine bloccate in attesa, o di un percorso estremamente lento).
Per ridurre i falsi positivi, gosentry tratta un timeout come un candidato blocco e lo conferma riproducendo l'input andato in timeout alcune volte con un timeout più ampio. In caso di blocco confermato, gosentry scrive l'input in `<libafl output dir>/hangs/` e interrompe la campagna di fuzzing (lo tratta come un bug/crash).
Prima di uscire, `golibafl` tenta di minimizzare l'input che causa il crash/blocco (best-effort; i blocchi sono limitati a ~60s in totale).
Nota: la conferma dei blocchi viene eseguita anche durante l'importazione/generazione iniziale del corpus, quindi i target che vanno in timeout su ogni input possono comunque essere rilevati in modo deterministico.
Questa funzionalità è configurata tramite `--libafl-config`:
- `catch_hangs` (predefinito: `true`)
- `hang_timeout_ms` (predefinito: `10000`)
- `hang_confirm_runs` (predefinito: `3`)
##### Rilevamento di data race (`--catch-races`)
gosentry può eseguire un ciclo di replay `-race` separato che osserva la directory `queue/` di LibAFL e riproduce i seed appena scoperti con `GORACE=halt_on_error=1`.
Il ciclo di replay compila un archivio separato dell'harness `-race` solo per il replay (senza strumentazione di copertura per il fuzzing).
Quando viene rilevata una data race durante il replay, gosentry stampa il report completo del race detector prima del riepilogo `catch-races:` e del comando di riproduzione.
Nota: il race detector di Go rileva solo data race **all'interno di una singola esecuzione dell'harness** (race tra goroutine nello stesso processo che accedono alla stessa memoria senza una corretta sincronizzazione). `--catch-races` non rileverà le race se il seed non innesca la concorrenza problematica e non rileva race tra processi diversi.
<details>
<summary><strong>Come funziona la modalità data race</strong></summary>
Questa modalità avvia un piccolo monitor all'interno di `go test` (stesso processo padre) e resta attivo per l'intera campagna di fuzzing.
- Quando: prima che venga avviato il processo principale di fuzzing LibAFL, gosentry compila l'harness di replay + il runner.
- Monitoraggio: prima che il fuzzing inizi, gosentry acquisisce un'istantanea del contenuto iniziale di `<libafl output dir>/queue/` in un set `seen`. Una goroutine interroga quindi `<libafl output dir>/queue/` ogni ~1s e riproduce solo i seed appena creati (ignora i file nascosti e i `*.metadata`).```text
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) │
└───────────────────────────────────────────────────────────────────────────┘
--catch-leaks)gosentry può anche eseguire un ciclo di replay di goleak che osserva la directory queue/ di LibAFL e ripete i seed appena scoperti con go.uber.org/goleak abilitato.
Quando viene rilevata una perdita di goroutine, gosentry stampa il percorso esatto del seed e lo copia in <libafl output dir>/leaks/.
Nota: goleak serve per le perdite di goroutine, non per le perdite di memoria.
Questa modalità avvia anche un piccolo monitor all'interno di go test (stesso processo padre) e rimane attivo per l'intera campagna di fuzzing.
<libafl output dir>/queue/ ogni ~1s e ripete ogni nuovo seed con GOSENTRY_LIBAFL_CATCH_LEAKS=1 (abilita go.uber.org/goleak dopo ogni esecuzione).```text
Legend: output/... = /...┌───────────────────────────────────────────────────────────────────────────┐
│ 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: │
│ (enables checks after each execution) │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 4) On "catch-leaks: detected goroutine leak" │
│ - copies seed to │
│ - stops the fuzz campaign (treat as bug/crash) │
└───────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────┐
│ 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 N times │
│ - fuzz loop: corpus seed -> grammar mutate -> exec harness │
│ - new coverage inputs are added to the on-disk corpus () │
└───────────────────────────────────────────────────────────────────────────┘
src/testing/libafl.golibaflMarshalInputs / libaflAppendValuelibaflUnmarshalArgs / libaflDecodeValueRegole di codifica (a livello generale):
bool: 1 byte (0 o 1)int/uint sono 8 byte)float32 = 4 byte, float64 = 8 byte)string: uvarint(len) poi byte grezzi della stringa[]byte: uvarint(len) poi byte grezziuvarint(len) poi ogni elemento codificato0 = nil, 1 = presente) poi il valore puntatofile:line usando go tool addr2line.git blame --line-porcelain per ottenere il committer-time per ogni riga.git_recency_map.bin come u64 head_time + u64 N + N * u64 timestamps (little-endian). Le voci non mappate usano timestamp 0.GOSENTRY_LIBAFL_CATCH_LEAKS=1 golibafl-leak run --input <seed>go.uber.org/goleakoutput/leaks/</details>
#### Come usare
Abilita il rilevamento delle perdite di goroutine con `--catch-leaks=true` o il rilevamento delle race con `--catch-races=true````bash
./bin/go test -fuzz=FuzzHarness --use-libafl --focus-on-new-code=false --catch-races=true --catch-leaks=true
Il fuzzing a livello di byte è ottimo, ma parser e formati di file spesso richiedono input strutturati. Con --use-grammar, gosentry usa il mutatore di grammatica Nautilus di LibAFL per generare e mutare input conformi a una grammatica fornita dall'utente (formato JSON) e li passa al tuo normale harness di fuzz Go (testing.F.Fuzz).
In modalità grammatica, LibAFL esegue comunque il normale loop guidato dalla copertura (seleziona un seed dal corpus → muta → esegui → mantieni gli input che aumentano la copertura). Il runner aggiunge la mutazione Nautilus (seed → albero di grammatica → muta → de-parsing) più (per impostazione predefinita) una fase guidata da CMPLOG, simile a I2S, che riscrive i terminali foglia di Nautilus in base ai confronti a runtime. Questo mantiene gli input validi per la grammatica (non esegue le fasi raw havoc/token a livello di byte in modalità grammatica). Puoi disabilitare la fase CMPLOG/I2S in --libafl-config tramite nautilus_cmplog_i2s=false (il fuzzing a livello di byte mantiene comunque CMPLOG/I2S sempre attivo).
[!NOTE] La modalità grammatica è solitamente più lenta del fuzzing a livello di byte. È un compromesso: più struttura vs meno esecuzioni al secondo.
Per ottenere i migliori risultati, usa un callback di fuzz con un solo argomento che accetti uno slice di byte ([]byte) o una string:```go
f.Fuzz(func(t testing.T, data []byte) { / parse data */ })
// or:
f.Fuzz(func(t testing.T, s string) { / parse s */ })
La modalità Grammar funziona meglio con un singolo argomento di input (`[]byte` o `string`). I callback di fuzz multi-argomento portano gosentry a decodificare il buffer di byte sottostante in valori separati, quindi il testo originale generato dalla grammatica non rimarrà intatto.
> [!NOTE]
> La modalità Grammar genera comunque **byte/stringhe**. Se hai bisogno di input strutturati (o stai facendo differential fuzzing), è nell'harness che converti `data` in valori di dominio (parse/unmarshal). (Fuori dalla modalità Grammar, gosentry può anche fare fuzzing su tipi Go > composti decodificandoli dai byte; vedi [Feature 1](#feature-1-struct-aware-fuzzing-fuzz-structs-as-inputs).)
Puoi regolare Nautilus tramite `--libafl-config` (usato solo con `--use-grammar`): `nautilus_max_len` e `nautilus_cmplog_i2s` (vedi `misc/gosentry/libafl.config.jsonc`).
<details>
<summary><strong>Benchmark: fase CMPLOG/I2S della grammatica Nautilus (attivo vs disattivo)</strong></summary>
Eseguito il 17 febbraio 2026 utilizzando l'esempio di grammatica JSON del repository (`test/gosentry/examples/grammar_json`, `FuzzGrammarJSON`, grammar `testdata/JSON.json`).
Risultati (LibAFL `UserStats`):
| modalità | `nautilus_cmplog_i2s` | tempo di esecuzione | esecuzioni | esec/sec | edges |
|---|---:|---:|---:|---:|---:|
| attivo | `true` | 1m-5s | 103818 | 1.586k | 388/8008 (4%) |
| disattivo | `false` | 1m-0s | 256659 | 4.251k | 388/8008 (4%) |
Nota: `edges` sono gli archi della coverage map di LibAFL, non le righe del sorgente Go.
</details>
Imposta `GOSENTRY_VERBOSE_AFL=1` per stampare alcuni input generati. Imposta `GOSENTRY_VERBOSE_AFL_ALL_INPUTS=1` per stampare **ogni** esecuzione in modalità Grammar come `GOLIBAFL_MUTATED_INPUT "..."` (molto rumoroso).
#### Helper per la creazione di grammatiche
Se devi creare una nuova grammatica JSON Nautilus per il tuo formato/protocollo di destinazione, gosentry include:
- Un prompt pronto per LLM: [misc/gosentry/nautilus/prompt.md](https://github.com/trailofbits/gosentry/blob/HEAD/misc/gosentry/nautilus/prompt.md)
- Un piccolo set di grammatiche di esempio: [misc/gosentry/nautilus/examples/](https://github.com/trailofbits/gosentry/blob/HEAD/misc/gosentry/nautilus/examples/)
<details>
<summary><strong>Esempio di harness Go per fuzzing (JSON)</strong></summary>```go
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")
}
})
}
Bozza di harness di fuzzing differenziale (due parser):```go 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 })
</details>
<details>
<summary><strong>Esempio: fuzzing grammaticale di un "linguaggio di input reale" (senza encoder personalizzato)</strong></summary>
Questo esempio sottopone a fuzzing un piccolo valutatore di espressioni aritmetiche generando **espressioni valide** a partire da una grammatica. Non c'è alcuna codifica ad hoc del tipo “struct to bytes”: il fuzzer produce lo stesso tipo di input che il tuo codice normalmente analizzerebbe.
Harness (l'input `string` a 1 argomento funziona meglio in modalità grammatica):```go
func FuzzExprEval(f *testing.F) {
f.Add("1+2")
f.Add("(3*4)-5")
f.Fuzz(func(t *testing.T, expr string) {
// Parse+eval your language/protocol.
// You can be **sure** that `expr` will always be a valid math operation. Just decode/parse/unmarshall it afterwards.
_, _ = Eval(expr)
})
}
Schema grammaticale (formato JSON Nautilus):```json [ ["Expr", "{Term}"], ["Expr", "{Term}+{Expr}"], ["Expr", "{Term}-{Expr}"], ["Term", "{Factor}"], ["Term", "{Factor}*{Term}"], ["Factor", "{Num}"], ["Factor", "({Expr})"], ["Num", "0"], ["Num", "1"], ["Num", "2"], ["Num", "3"] ]
</details>
<details>
<summary><strong>Esempio di grammatica JSON di Nautilus (piccolo sottoinsieme JSON)</strong></summary>
Questo è il formato di file previsto da `--grammar=...`:
- La grammatica è un array JSON di regole: `["NonTerm", "RHS"]`.
- I nomi dei non terminali devono iniziare con una lettera maiuscola (`Value`, `Object`, ...).
- Usa `{NonTerm}` nella RHS per fare riferimento a un'altra regola.
- Le parentesi graffe `{` e `}` sono riservate ai riferimenti non terminali; per emettere parentesi letterali, usa `\\{` e `\\}` nella stringa RHS.```json
[
["Json", "{Value}"],
["Value", "null"],
["Value", "{String}"],
["String", "\"{Chars}\""],
["Chars", ""],
["Chars", "{Char}{Chars}"],
["Char", "a"],
["Char", "b"]
]
generateoutput/queue/</details>
Limitazioni (attuale collante):
- La modalità grammatica funziona meglio con un singolo argomento di input; i target di fuzzing multi-argomento decodificheranno il buffer di byte sottostante in valori separati.
- Nessuna ricombinazione/crossover della grammatica tra due seed del corpus per ora (la mutazione è a seed singolo).
## Funzionalità 8: Generare report di copertura Go dalle campagne di fuzzing
Dopo (o durante) l'esecuzione di una campagna di fuzzing LibAFL, gosentry può generare un report di copertura Go riproducendo l'attuale **queue corpus** di LibAFL (senza fuzzing).```bash
# Same package + same fuzz target as your fuzz campaign:
./bin/go test -fuzz=FuzzHarness --generate-coverage .
This replays inputs from <libafl output dir>/queue/ and writes cover.out and cover.html.
Questi bug sono stati trovati eseguendo una campagna di differential-fuzzing utilizzando la funzione di grammar fuzzing di gosentry.