
gosentry v0.4.1
Chaîne d'outils Go orientée sécurité, axée sur des capacités de fuzzing de pointe.
gosentry
gosentry est un fork de la chaîne d'outils Go axé sur la sécurité, intégrant de nombreuses fonctionnalités pour des campagnes de fuzzing de pointe sur des codebases Go. Si vous utilisiez go test -fuzz auparavant, vous devriez utiliser gosentry comme remplacement.
Il est fourni avec diverses améliorations de fuzzing et détecteurs de bogues qui ne sont pas présents nativement dans la chaîne d'outils Go. Voir TLDR ; ci-dessous. Vous pouvez également lire l'article de blog associé ici.
TLDR (fonctionnalités et options) :
- Fuzzer directement les entrées
struct(aucun analyseur personnalisé n'est nécessaire). Ajoutez des graines avecf.Add(Input{N: 7, S: "hi"})puisf.Fuzz(func(t *testing.T, in Input) { ... }). - Paniquer en cas de débordement d'entier et détecter les problèmes arithmétiques
- Fuzzer avec LibAFL pour des techniques de fuzzing de pointe comme la résolution de contraintes de chemin
- Générer/muter des entrées à partir d'une grammaire pour éviter les mutations inutiles. La mutation génère une opération mathématique valide comme
X + Y - Zpeut devenirX / U + Z - 14au lieu deX + Yè - Z - Paniquer sur des fonctions sélectionnées (comme les enregistreurs d'erreurs critiques) et planter lorsqu'elle est appelée
- Concentrer le fuzzer sur les lignes récemment modifiées ET sur les nouvelles couvertures pour cibler principalement les nouveaux commits
- Détecter les courses de données au moment du fuzzing
- Détecter les fuites Go au moment du fuzzing
- Détecter les exécutions bloquées avec des délais d'attente au moment du fuzzing
- Générer un rapport de couverture HTML à partir d'un corpus de campagne de fuzzing avec un seul CLI
Table des matières
- Compilation
- Fonctionnalité 1 : Fuzzing adapté aux structs (fuzzer des structs comme entrées)
- Fonctionnalité 2 : Détection des débordements d'entiers et des problèmes de troncature
- Fonctionnalité 3 : Panique sur des fonctions sélectionnées
- Fonctionnalité 4 : Fuzzing de pointe avec LibAFL
- Fonctionnalité 5 : Fuzzing orienté git-blame (expérimental)
- Fonctionnalité 6 : Détection des courses, des fuites de goroutines et des blocages au moment du fuzzing
- Fonctionnalité 7 : Fuzzing basé sur une grammaire (Nautilus)
- Fonctionnalité 8 : Générer des rapports de couverture de fuzzing à partir d'une campagne
- Trophées
- Crédits
Compilation```bash
cd src && ./make.bash # Produces ../bin/go. See GOFLAGS below.
> [!TIP]
> Documentation contributeur : consultez `docs/gosentry/index.md` pour une carte du code, une boucle de développement recommandée, les points d'entrée CI et les scripts de benchmark.
> Ce fork utilise l'application GitHub Pull pour ouvrir et fusionner automatiquement les PR de `golang/go:master` vers `master`, garantissant que nous ne restons jamais en retard sur les dernières mises à jour de la chaîne d'outils Go.
## Fonctionnalité 1 : Fuzzing conscient des structures (fuzzer des structures en entrée)
#### Aperçu
Le fuzzing natif de Go (`go test -fuzz=...`) ne prend en charge qu'un petit ensemble de types scalaires comme paramètres de fuzzing (`[]byte`, `string`, nombres, ...). Dans gosentry, vous pouvez également fuzzer des **types composites** construits à partir de ces scalaires : structures, tableaux, slices et pointeurs.
Cela est utile lorsque votre code prend naturellement des entrées structurées et que vous ne souhaitez pas créer un encodeur/décodeur personnalisé uniquement pour amorcer et muter le corpus.
Voir `test/gosentry/examples/multiargs` et `test/gosentry/examples/composite` pour des exemples.
#### Exemple simple```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")
}
})
}
Comment fonctionnent les seeds de type struct (f.Add) et le fuzzing de structs (le lien ainsi créé)
Le fuzzer natif de Go ne peut pas fuzzer une valeur struct directement (il ne sait muter qu'une petite liste de types scalaires). gosentry ajoute une petite couche de liaison : lorsque votre cible de fuzzing utilise des types composites (comme Input), gosentry fuzz un simple []byte en coulisses. À chaque exécution, il décode ces octets dans votre struct (champ par champ, récursivement pour les slices/tableaux/pointeurs), puis appelle votre rappel f.Fuzz avec la valeur décodée. Le même encodage est utilisé pour les seeds, de sorte qu'un f.Add(Input{...}) devient une entrée de corpus []byte encodée que le fuzzer peut réutiliser et muter comme n'importe quelle autre seed.
Les fuzzers (y compris LibAFL) mutent des octets bruts, nous voulons donc un décodeur capable de transformer n'importe quelle tranche d'octets en une valeur de struct « quelconque » et de continuer. JSON/gob rejetteraient la plupart des entrées aléatoires (mauvais pour la couverture) et ne rempliraient pas non plus les champs non exportés, alors que le fuzzing bénéficie souvent de la rupture des invariants. Ce format personnalisé est petit, rapide, déterministe et tolérant aux données malformées.
Sous le capot, cela utilise le propre format binaire simple de gosentry (pas gob, pas JSON). Le code se trouve dans src/testing/libafl.go :
- Encodage :
libaflMarshalInputs/libaflAppendValue - Décodage :
libaflUnmarshalArgs/libaflDecodeValue
Règles d'encodage (niveau haut) :
bool: 1 octet (0ou1)- Entiers : octets en little-endian (
int/uintfont 8 octets) - Flottants : bits IEEE-754 en little-endian (
float32= 4 octets,float64= 8 octets) string:uvarint(len)puis les octets bruts de la chaîne[]byte:uvarint(len)puis les octets bruts- Autres slices :
uvarint(len)puis chaque élément encodé - Structures : champs encodés dans l'ordre de déclaration
- Pointeurs : 1 octet (
0= nil,1= présent) puis la valeur pointée
Fonctionnalité 2 : Détection des problèmes de débordement et de troncature d'entiers
Aperçu
Ce travail s'inspire du go-panikint développé précédemment. Il ajoute la détection des débordements/sous-dépassements pour les opérations arithmétiques sur les entiers et (optionnellement) la détection des troncatures de type pour les conversions d'entiers. Lorsqu'un débordement ou une troncature est détecté, une panique avec un message d'erreur détaillé est déclenchée, incluant le type d'opération spécifique et les types d'entiers impliqués.
Opérations arithmétiques : Gère l'addition +, la soustraction -, la multiplication * et la division / pour les entiers signés et non signés. Pour les entiers signés, couvre int8, int16, int32. Pour les entiers non signés, couvre uint8, uint16, uint32, uint64. Le cas de la division détecte spécifiquement la condition de débordement MIN_INT / -1 pour les entiers signés. int64 et uintptr ne sont pas vérifiés pour les opérations arithmétiques.
Détection de troncature de type : Détecte les conversions de types d'entiers potentiellement avec perte. Couvre tous les types d'entiers : int8, int16, int32, int64, uint8, uint16, uint32, uint64. Exclut uintptr en raison d'une utilisation dépendante de la plateforme. Cette détection est désactivée par défaut.
La détection de débordement est activée par défaut. Pour la désactiver, ajoutez GOFLAGS='-gcflags=-overflowdetect=false' avant votre ./make.bash. Vous pouvez également activer le vérificateur de problèmes de troncature avec : -gcflags=-truncationdetect=true
Comment cela fonctionne
Cette fonctionnalité modifie la génération SSA du compilateur afin que les opérations arithmétiques sur les entiers et les conversions d'entiers reçoivent des vérifications d'exécution supplémentaires qui appellent le runtime pour déclencher une panique avec un message d'erreur détaillé lorsqu'un bug est détecté. Les vérifications sont appliquées via un filtrage basé sur l'emplacement source, de sorte que le code utilisateur est instrumenté tandis que les fichiers de la bibliothèque standard et les dépendances (cache de modules et vendor/) sont ignorés.
Vous pouvez lire l'article de blog associé ici.
Suppression des faux positifs
Ajoutez un marqueur sur la même ligne que l'opération ou sur la ligne immédiatement au-dessus pour supprimer un signalement spécifique :
- Débordement/sous-dépassement :
overflow_false_positive - Troncature :
truncation_false_positive
Exemple :
``````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
Parfois, cela peut ne pas fonctionner, car Go met la fonction en ligne. Si // overflow_false_positive ne suffit pas, ajoutez //go:noinline avant la signature de votre fonction.
Fonctionnalité 3 : Panique sur des fonctions sélectionnées
Lors du fuzzing de cibles, nous pouvons être intéressés par le déclenchement d'une panique lorsque certaines fonctions sont appelées. Par exemple, certains logiciels peuvent émettre des messages log.error au lieu de paniquer, même si ces conditions indiquent souvent des états que les chercheurs en sécurité souhaiteraient détecter pendant le fuzzing.
Cependant, ces erreurs sont généralement gérées en interne (par exemple, via des mécanismes de nouvelle tentative ou de pause, ou en imprimant des messages dans les journaux), ce qui les rend largement invisibles pour les fuzzers. L'objectif de cette fonctionnalité est de résoudre ce problème.
Comment utiliser
Compilez gosentry, puis utilisez l'option --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'exemple ci-dessus provoquerait un panic lorsque `(*Logger).Warning` ou `(*Logger).Error` est appelé (liste séparée par des virgules).
<details>
<summary><strong>Comment fonctionne la fonctionnalité de panic sur les fonctions sélectionnées</strong></summary>```text
┌───────────────────────────────────────────────────────────────────────────┐
│ 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" │
└───────────────────────────────────────────────────────────────────────────┘
En pratique, cela fait que tout site d'appel correspondant se comporte comme un crash/panic pour les fuzzers (notez que seuls les sites d'appel statiques peuvent être interceptés).
Fonctionnalité 4 : fuzzing de pointe avec LibAFL
LibAFL est bien plus performant que le fuzzer Go traditionnel. Lors du fuzzing (go test -fuzz=...), gosentry utilise LibAFL par défaut (runner dans golibafl/).
Note de stabilité : en mode LibAFL, gosentry force GODEBUG=updatemaxprocs=0 (désactive les mises à jour automatiques de GOMAXPROCS par le runtime) pour éviter un crash intermittent du CI Linux ("sync: inconsistent mutex state"). Les détails se trouvent dans misc/gosentry/USE_LIBAFL.md.
Lorsque vous utilisez LibAFL (par défaut), vous devez explicitement choisir d'activer ou non la planification git-aware : --focus-on-new-code=true|false. Plus de documentation dans ce fichier Markdown.
Vous pouvez également passer un fichier de configuration JSONC optionnel pour LibAFL (incluant les options de fuzzing par grammaire), voir ici.
Avec "stop_all_fuzzers_on_panic": false, LibAFL enregistre chaque crash et redémarre son client pour continuer le 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
Utilisez `-fuzztime=1m` pour arrêter une campagne LibAFL après une minute.
La génération de rapports de couverture à partir d'un corpus de campagne LibAFL est documentée dans [Feature 8](#feature-8-generate-go-coverage-reports-from-fuzzing-campaign).
Le fuzzing basé sur une grammaire (Nautilus) est documenté dans [Feature 7](#feature-7-grammar-based-fuzzing-nautilus).
<details>
<summary><strong>Comment Go et LibAFL sont connectés</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 │
└───────────────────────────────────────────────────────────────────────────┘
En mode --use-libafl, gosentry compile libharness.a et le runner Rust golibafl le pilote en cours de processus via les points d'entrée libFuzzer. Remarque : HARNESS_LIB peut pointer vers n'importe quel nom d'archive de harness (par exemple libharness_race.a utilisé par --catch-races).
Comment fonctionne l'instrumentation de couverture (LibAFL + cible Go)
En mode --use-libafl, gosentry compile le harness Go avec l'instrumentation de couverture activée. Cela ajoute de petits compteurs au code qui changent lorsque différentes parties de votre programme s'exécutent. Lorsque le harness démarre dans golibafl, le runtime Go expose ces compteurs à LibAFL. LibAFL les lit après chaque entrée pour voir quel code a été exécuté, et utilise cette couverture pour guider les mutations suivantes.
Limitations
Parlons de la motivation derrière l'utilisation de LibAFL. Le fuzzing avec go test -fuzz est loin derrière les techniques de fuzzing de pointe. Un bon exemple en est CMPLOG/Redqueen d'AFL++. Ces fonctionnalités permettent aux fuzzers de résoudre certaines contraintes. Prenons l'extrait suivant```go
if input == "IMARANDOMSTRINGJUSTCMPLOGMEMAN" {
panic("this string is illegal")
}
SOTA fuzzers comme AFL++ ou LibAFL détecteraient la panique instantanément dans ce cas. Cependant, le fuzzer natif de Go ne le ferait pas. C'est une énorme lacune qui restreint considérablement l'exploration de la couverture.
Le benchmark ci-dessous montre ces limites. Notez que ces benchmarks peuvent être **reproduits** et améliorés via le [dépôt gosentry-bench-libafl](https://github.com/kevin-valerio/gosentry-bench-libafl/tree/main).
##### Benchmark 1:
Le graphique ci-dessous montre l'évolution du nombre de lignes couvertes lors du fuzzing de l'[UUID](https://github.com/google/uuid) de Google avec LibAFL par rapport au fuzzer natif de Go.

##### Benchmark 2:
Le graphique ci-dessous montre l'évolution du nombre de lignes couvertes lors du fuzzing de [go-ethereum](https://github.com/ethereum/go-ethereum) avec LibAFL par rapport au fuzzer natif de Go.

#### Exemple
Vous pouvez le tester sur certains harnais de fuzzing dans `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
Arrêtez la campagne de fuzzing avec Ctrl+C.
Répertoire de sortie LibAFL (identité de la campagne / réutilisation du corpus)
gosentry stocke l'état de la campagne LibAFL (corpus, crashes, etc.) sous la racine du cache de fuzzing de Go (approximativement $(go env GOCACHE)/fuzz), dans un répertoire déterministe dérivé du même package + même cible de fuzzing (et de la même racine de projet).
Cela signifie que l'arrêt (Ctrl+C) puis le redémarrage de la même campagne de fuzzing reprendra, par défaut, à partir du corpus queue/ LibAFL précédent.
Le chemin est affiché à la fin de l'exécution :```text libafl output dir: /full/path/to/.../fuzz//libafl//
Notes :
- `<harness>` est le nom de la cible de fuzzing lorsque `-fuzz` est un identifiant simple comme `FuzzXxx` (ou `^FuzzXxx$`), sinon c’est `pattern-<hash>`.
- La génération de couverture (`--generate-coverage`) utilise la même règle pour trouver le corpus `queue/` approprié, elle doit donc être exécutée depuis le même package avec le même `-fuzz=...`.
## Fonctionnalité 5 : Fuzzing orienté git blame (expérimental)
#### Aperçu
Le fuzzing guidé par la couverture est excellent pour explorer de nouveaux chemins, mais il considère tout le code couvert comme également intéressant. Lorsque vous fuzzez de grandes bases de code, vous pouvez vouloir orienter le fuzzer vers le code récemment modifié, là où les régressions et les bogues sont plus susceptibles d’être introduits. En mode LibAFL, gosentry peut utiliser `git blame` pour privilégier les entrées qui exécutent des lignes récemment modifiées (tout en gardant le guidage par couverture comme signal principal).
Ce travail est basé sur des travaux antérieurs de [LibAFL-git-aware](https://github.com/kevin-valerio/LibAFL-git-aware). Tous les détails techniques approfondis y sont documentés.
#### Comment l’utiliser
Activez la planification git-aware avec `--focus-on-new-code=true` :```bash
./bin/go test -fuzz=FuzzHarness --use-libafl --focus-on-new-code=true --catch-races=false --catch-leaks=false
Ce mode nécessite git (pour exécuter git blame) et go tool addr2line pour mapper les compteurs de couverture aux file:line de la source.
Comment fonctionne le fuzzing orienté git-blame
```text ┌───────────────────────────────────────────────────────────────────────────┐ │ 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 │ └───────────────────────────────────────────────────────────────────────────┘ ```Comment gosentry construit git_recency_map.bin
.go.fuzzcntrs est la section de l'éditeur de liens qui contient les compteurs de couverture 8 bits de style libFuzzer de Go (activés par -gcflags=all=-d=libfuzzer) ; chaque octet indique « combien de fois ce point instrumenté a été atteint ». Lorsque --focus-on-new-code=true, golibafl génère git_recency_map.bin de la manière suivante :
- Extraire
go.odelibharness.a. - Lire la taille de la section
.go.fuzzcntrspour obtenir le nombre de compteursN. - Analyser les relocations de
.textqui référencent les symboles.go.fuzzcntrspour retrouver l'adresse de chaque index de compteur. - Résoudre chaque adresse en
file:lineà l'aide dego tool addr2line. - Exécuter
git blame --line-porcelainpour obtenircommitter-timepar ligne. - Écrire
git_recency_map.binsous la formeu64 head_time+u64 N+N * u64 timestamps(little-endian). Les entrées non mappées utilisent l'horodatage0.
Benchmark 1 (go-ethereum / geth) : baseline vs git-aware
Exécuté avec 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>
## Fonctionnalité 6 : Détecter les courses de données, les fuites de goroutines et les blocages (timeouts) au moment du fuzzing
##### Détection des blocages confirmés (timeouts LibAFL)
Lors du fuzzing avec LibAFL, une exécution de harness peut **expirer** (par exemple à cause d'un deadlock / de goroutines bloquées en attente, ou d'un chemin extrêmement lent).
Pour réduire les faux positifs, gosentry considère un timeout comme un candidat au blocage et le confirme en rejouant l'entrée ayant expiré plusieurs fois avec un timeout plus grand. En cas de blocage confirmé, gosentry écrit l'entrée dans `<libafl output dir>/hangs/` et arrête la campagne de fuzzing (il la traite comme un bug/crash).
Avant de se terminer, `golibafl` tente de minimiser l'entrée qui provoque le crash/blocage (au mieux ; les blocages sont plafonnés à ~60 s au total).
Remarque : la confirmation de blocage s'exécute également lors de l'importation/génération initiale du corpus, de sorte que les cibles qui expirent sur chaque entrée peuvent encore être détectées de manière déterministe.
Ceci est configuré via `--libafl-config` :
- `catch_hangs` (défaut : `true`)
- `hang_timeout_ms` (défaut : `10000`)
- `hang_confirm_runs` (défaut : `3`)
##### Détection des courses de données (`--catch-races`)
gosentry peut exécuter une boucle de rejeu séparée `-race` qui surveille le répertoire `queue/` de LibAFL et rejoue les seeds nouvellement découvertes avec `GORACE=halt_on_error=1`.
La boucle de rejeu construit une archive de harness `-race` séparée, réservée au rejeu (sans instrumentation de couverture de fuzzing).
Lorsqu'une course de données est détectée pendant le rejeu, gosentry affiche le rapport complet du détecteur de courses avant le résumé `catch-races:` et la commande de reproduction.
Remarque : le détecteur de courses de Go ne détecte les courses de données qu'**à l'intérieur d'une seule exécution de harness** (courses entre goroutines du même processus accédant à la même mémoire sans synchronisation appropriée). `--catch-races` manquera les courses si la seed ne déclenche pas la concurrence conflictuelle, et il ne détecte pas les courses entre processus.
<details>
<summary><strong>Comment fonctionne le mode de détection de courses de données</strong></summary>
Ce mode démarre un petit moniteur à l'intérieur de `go test` (même processus parent), et il s'exécute pendant toute la campagne de fuzzing.
- Quand : avant que le processus principal de fuzzing LibAFL ne soit démarré, gosentry construit le harness de rejeu + le runner.
- Surveillance : avant que le fuzzing ne démarre, gosentry enregistre l'état initial du contenu de `<libafl output dir>/queue/` dans un ensemble `seen`. Une goroutine interroge ensuite `<libafl output dir>/queue/` toutes les ~1 s et ne rejoue que les seeds nouvellement créées (ignore les fichiers cachés et `*.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) │
└───────────────────────────────────────────────────────────────────────────┘
Détection des fuites de goroutines (--catch-leaks)
gosentry peut également exécuter une boucle de rejeu goleak qui surveille le répertoire queue/ de LibAFL et rejoue les seeds nouvellement découvertes avec go.uber.org/goleak activé.
Lorsqu'une fuite de goroutine est détectée, gosentry affiche le chemin exact de la seed et la copie dans <libafl output dir>/leaks/.
Remarque : goleak concerne les fuites de goroutines, pas les fuites mémoire.
Comment fonctionne le mode de détection des fuites de goroutines
Ce mode démarre également un petit moniteur dans go test (même processus parent), et il s'exécute pendant toute la campagne de fuzzing.
- Surveillance : une goroutine interroge
<libafl output dir>/queue/toutes les ~1s et rejoue chaque nouvelle seed avecGOSENTRY_LIBAFL_CATCH_LEAKS=1(activego.uber.org/goleakaprès chaque exécution).```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: 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) │
└───────────────────────────────────────────────────────────────────────────┘
</details>
#### Comment utiliser
Activez la détection de fuites de goroutines avec `--catch-leaks=true` ou la détection de courses avec `--catch-races=true````bash
./bin/go test -fuzz=FuzzHarness --use-libafl --focus-on-new-code=false --catch-races=true --catch-leaks=true
Fonctionnalité 7: Fuzzing basé sur la grammaire (Nautilus)
Vue d'ensemble
Le fuzzing au niveau des octets est excellent, mais les parseurs et les formats de fichiers nécessitent souvent des entrées structurées. Avec --use-grammar, gosentry utilise le mutateur de grammaire Nautilus de LibAFL pour générer et muter des entrées conformes à une grammaire fournie par l'utilisateur (format JSON), et les transmet à votre harnais de fuzzing Go habituel (testing.F.Fuzz).
En mode grammaire, LibAFL continue d'exécuter la boucle normale guidée par la couverture (choisir une graine de corpus → muter → exécuter → conserver les entrées qui augmentent la couverture). Le runner ajoute la mutation Nautilus (graine → arbre de grammaire → muter → déparser) ainsi que (par défaut) une étape guidée par CMPLOG, de type I2S, qui réécrit les terminaux feuilles de Nautilus en fonction des comparaisons à l'exécution. Cela garantit que les entrées restent grammaticalement valides (il n'exécute pas les étapes brutes de type havoc/token au niveau des octets en mode grammaire). Vous pouvez désactiver l'étape CMPLOG/I2S dans --libafl-config via nautilus_cmplog_i2s=false (le fuzzing au niveau des octets garde toujours CMPLOG/I2S activé).
[!NOTE] Le mode grammaire est généralement plus lent que le fuzzing au niveau des octets. C'est un compromis : plus de structure vs moins d'exécutions par seconde.
Pour de meilleurs résultats, utilisez un callback de fuzz à un argument qui accepte soit une tranche d'octets ([]byte), soit une string:```go
f.Fuzz(func(t testing.T, data []byte) { / parse data */ })
// or:
f.Fuzz(func(t testing.T, s string) { / parse s */ })
Le mode grammaire fonctionne mieux avec un seul argument d’entrée (`[]byte` ou `string`). Les callbacks de fuzz multi-arguments amènent gosentry à décoder le tampon d’octets sous-jacent en valeurs séparées, de sorte que le texte généré par la grammaire d’origine ne reste pas intact.
> [!NOTE]
> Le mode grammaire génère toujours des **octets/chaînes**. Si vous avez besoin d’entrées structurées (ou si vous faites du fuzzing différentiel), c’est dans votre harness que vous convertissez `data` en valeurs du domaine (parse/unmarshal). (En dehors du mode grammaire, gosentry peut également fuzzer des types composites Go > en les décodant depuis des octets ; voir [Fonctionnalité 1](#feature-1-struct-aware-fuzzing-fuzz-structs-as-inputs).)
Vous pouvez régler Nautilus via `--libafl-config` (utilisé uniquement avec `--use-grammar`) : `nautilus_max_len` et `nautilus_cmplog_i2s` (voir `misc/gosentry/libafl.config.jsonc`).
<details>
<summary><strong>Benchmark : étape CMPLOG/I2S de la grammaire Nautilus (activée vs désactivée)</strong></summary>
Exécuté le 17 février 2026 avec l’exemple de grammaire JSON du dépôt (`test/gosentry/examples/grammar_json`, `FuzzGrammarJSON`, grammaire `testdata/JSON.json`).
Résultats (LibAFL `UserStats`) :
| mode | `nautilus_cmplog_i2s` | durée | exécutions | exéc/s | arêtes |
|---|---:|---:|---:|---:|---:|
| activé | `true` | 1m-5s | 103818 | 1.586k | 388/8008 (4%) |
| désactivé | `false` | 1m-0s | 256659 | 4.251k | 388/8008 (4%) |
Remarque : `edges` correspond aux arêtes de la carte de couverture de LibAFL, et non aux lignes de source Go.
Définissez `GOSENTRY_VERBOSE_AFL=1` pour afficher quelques entrées générées. Définissez `GOSENTRY_VERBOSE_AFL_ALL_INPUTS=1` pour afficher **chaque** exécution en mode grammaire sous la forme `GOLIBAFL_MUTATED_INPUT "..."` (très bavard).
#### Aides à la création de grammaires
Si vous devez créer une nouvelle grammaire JSON Nautilus pour votre propre format/protocole cible, gosentry fournit :
- Un prompt prêt pour LLM : [misc/gosentry/nautilus/prompt.md](https://github.com/trailofbits/gosentry/blob/HEAD/misc/gosentry/nautilus/prompt.md)
- Un petit ensemble de grammaires d’exemple : [misc/gosentry/nautilus/examples/](https://github.com/trailofbits/gosentry/blob/HEAD/misc/gosentry/nautilus/examples/)
<details>
<summary><strong>Exemple de harness de fuzz Go (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")
}
})
}
Ébauche de harnais de fuzzing différentiel (deux analyseurs) :```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>Exemple : fuzzing grammatical d'un « vrai langage d'entrée » (sans encodeur personnalisé)</strong></summary>
Cet exemple fuzze un petit évaluateur d'expressions arithmétiques en générant des **expressions valides** à partir d'une grammaire. Il n'y a pas d'encodage « struct to bytes » ad hoc : le fuzzer produit le même type d'entrée que votre code analyserait normalement.
Harness (une entrée `string` à 1 argument fonctionne le mieux en mode grammaire) :```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)
})
}
Schéma de grammaire (format 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>Exemple de grammaire JSON Nautilus (petit sous-ensemble JSON)</strong></summary>
C'est le format de fichier attendu par `--grammar=...` :
- La grammaire est un tableau JSON de règles : `["NonTerm", "RHS"]`.
- Les noms de non-terminaux doivent commencer par une lettre majuscule (`Value`, `Object`, ...).
- Utilisez `{NonTerm}` dans la RHS pour référencer une autre règle.
- `{` et `}` sont réservés aux références de non-terminaux ; pour émettre des accolades littérales, utilisez `\\{` et `\\}` dans la chaîne RHS.```json
[
["Json", "{Value}"],
["Value", "null"],
["Value", "{String}"],
["String", "\"{Chars}\""],
["Chars", ""],
["Chars", "{Char}{Chars}"],
["Char", "a"],
["Char", "b"]
]
Comment fonctionne le fuzzing de grammaire dans gosentry
```text Legend: output/... = /...┌───────────────────────────────────────────────────────────────────────────┐
│ 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/) │
└───────────────────────────────────────────────────────────────────────────┘
</details>
Limitations (glue actuelle) :
- Le mode grammaire fonctionne mieux avec un seul argument d'entrée ; les cibles de fuzzing multi-arguments décodent le tampon d'octets sous-jacent en valeurs séparées.
- Pas encore de recombinaison/croisement de grammaire entre deux graines du corpus (la mutation est à graine unique).
## Fonctionnalité 8 : Générer des rapports de couverture Go à partir de la campagne de fuzzing
Après (ou pendant) l'exécution d'une campagne de fuzzing LibAFL, gosentry peut générer un rapport de couverture Go en rejouant le **corpus de file d'attente** LibAFL actuel (sans fuzzing).```bash
# Same package + same fuzz target as your fuzz campaign:
./bin/go test -fuzz=FuzzHarness --generate-coverage .
Ceci rejoue les entrées de <libafl output dir>/queue/ et écrit cover.out et cover.html.
Trophées
Ces bugs ont été découverts en menant une campagne de fuzzing différentiel utilisant la fonctionnalité de fuzzing par grammaire de gosentry.
Optimism
- Kona et op-node peuvent être en désaccord sur les canaux brotli
- Un type de batch inconnu déclenche une panique et provoque un déni de service dans kona-protocol
- Incohérence d'analyse des trames de Kona par rapport à op-node et aux spécifications OP Stack