🔝 Retour au Sommaire
LibFuzzer est le fuzzer in-process du projet LLVM. Contrairement à AFL++ qui lance le programme cible comme un processus séparé, LibFuzzer s'exécute dans le même processus que le code à tester — il appelle directement une fonction du programme, sans fork, sans I/O inter-processus, sans overhead de démarrage. Ce modèle lui confère une vitesse d'exécution exceptionnelle : des dizaines de milliers, parfois des centaines de milliers d'itérations par seconde sur une seule fonction de parsing.
LibFuzzer est l'outil qui propulse OSS-Fuzz, la plateforme de fuzzing continu de Google qui couvre plus de 1 200 projets open source critiques (Chrome, OpenSSL, cURL, FFmpeg, etc.). Son intégration native avec Clang — un seul flag suffit à instrumenter et linker — en fait le choix le plus naturel pour le fuzzing de bibliothèques et de fonctions C++ isolées.
Cette section couvre l'écriture de fuzz targets pour LibFuzzer, les options d'exécution, la gestion du corpus, les techniques avancées, et l'intégration dans un pipeline CI/CD via OSS-Fuzz ou en autonome.
LibFuzzer est intégré à Clang. Aucune installation séparée n'est nécessaire — il suffit d'avoir Clang installé :
sudo apt install clang llvm
# Vérifier que le flag -fsanitize=fuzzer est reconnu
echo 'extern "C" int LLVMFuzzerTestOneInput(const uint8_t*, size_t) { return 0; }' \
| clang++ -x c++ -fsanitize=fuzzer - -o /dev/null && echo "LibFuzzer OK"LibFuzzer est exclusivement disponible avec Clang. GCC ne fournit pas d'équivalent intégré. Si votre projet doit être compilé avec GCC en production, vous pouvez maintenir un build de fuzzing séparé compilé avec Clang — le code source est le même, seul le compilateur change.
Le cœur d'un fuzz target LibFuzzer est une unique fonction avec une signature imposée :
// fuzz/fuzz_json_parser.cpp
#include <cstdint>
#include <cstddef>
#include "json_parser.h"
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
// Appeler le code à tester avec les données fournies par le fuzzer
try {
json::parse(data, size);
} catch (const json::ParseError&) {
// Les erreurs de parsing sont attendues, pas des bugs
}
return 0;
}Règles de la fonction :
- Signature exacte —
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size). Leextern "C"est obligatoire car LibFuzzer la recherche par nom C dans les symboles du binaire. - Retour 0 — la valeur de retour doit toujours être 0 (les valeurs non nulles sont réservées pour un usage futur).
- Pas de
main()— LibFuzzer fournit son propremain(). Si vous en définissez un, l'édition de liens échoue avec un symbole dupliqué. - Pas d'état persistant — chaque appel doit être indépendant. Pas de variables globales modifiées, pas de fichiers temporaires non nettoyés.
- Pas d'appel à
exit()ouabort()— LibFuzzer interprète un abort comme un crash. Si le code testé appelleexit()légitimement, il faut l'intercepter ou le supprimer dans le harness.
Un piège courant est de catcher trop large, masquant de vrais bugs :
// ❌ Trop large : masque les vrais bugs
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
try {
process(data, size);
} catch (...) { // Cache tout, y compris les bugs !
return 0;
}
return 0;
}
// ✅ Correct : ne catcher que les erreurs attendues
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
try {
process(data, size);
} catch (const ParseError&) {
// Entrée invalide — comportement normal
} catch (const std::bad_alloc&) {
// Allocation échouée sur une entrée trop volumineuse — tolérable
}
// std::out_of_range, SIGSEGV, SIGABRT, etc. → crash visible par LibFuzzer
return 0;
}Certains composants nécessitent une phase de setup (chargement de configuration, initialisation d'un contexte). LibFuzzer propose un hook d'initialisation appelé une seule fois avant le début du fuzzing :
#include <cstdint>
#include <cstddef>
#include "codec.h"
static CodecContext* g_ctx = nullptr;
extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) {
// Appelé une seule fois au démarrage
g_ctx = codec_create_context();
// On peut aussi parser les arguments personnalisés ici
return 0;
}
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
codec_decode(g_ctx, data, size);
codec_reset(g_ctx); // Remettre le contexte dans un état propre
return 0;
}Les données brutes (uint8_t*) ne sont pas toujours pratiques lorsque le code à tester attend plusieurs paramètres typés. FuzzedDataProvider (fourni par Clang) permet de découper le buffer de données en valeurs structurées :
#include <cstdint>
#include <cstddef>
#include <string>
#include <fuzzer/FuzzedDataProvider.h>
#include "database.h"
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
FuzzedDataProvider fdp(data, size);
// Extraire des valeurs typées depuis le buffer du fuzzer
std::string table_name = fdp.ConsumeRandomLengthString(64);
uint32_t limit = fdp.ConsumeIntegralInRange<uint32_t>(0, 10000);
bool ascending = fdp.ConsumeBool();
// Utiliser les données restantes comme payload
std::string query_suffix = fdp.ConsumeRemainingBytesAsString();
try {
db::query(table_name, limit, ascending, query_suffix);
} catch (const db::QueryError&) {
// Attendu
}
return 0;
}Méthodes principales de FuzzedDataProvider :
| Méthode | Retourne |
|---|---|
ConsumeBool() |
bool |
ConsumeIntegral<T>() |
Entier de type T (toute la plage) |
ConsumeIntegralInRange<T>(min, max) |
Entier de type T dans [min, max] |
ConsumeFloatingPoint<T>() |
Flottant de type T |
ConsumeRandomLengthString(max_len) |
std::string de longueur aléatoire ≤ max_len |
ConsumeBytesWithTerminator<T>(n) |
std::vector<T> de n éléments |
ConsumeRemainingBytes<T>() |
std::vector<T> — tout le buffer restant |
ConsumeRemainingBytesAsString() |
std::string — tout le buffer restant |
ConsumeEnum<EnumType>() |
Valeur d'un enum (par indice) |
remaining_bytes() |
Nombre d'octets non encore consommés |
FuzzedDataProvider consomme les données de manière déterministe : pour le même buffer d'entrée, les mêmes valeurs sont extraites. Cela garantit la reproductibilité des crashs.
clang++ -std=c++23 -O1 -g \
-fsanitize=fuzzer,address,undefined \
-fno-omit-frame-pointer \
fuzz/fuzz_json_parser.cpp src/json_parser.cpp \
-o fuzz_json_parserLe flag -fsanitize=fuzzer accomplit deux choses :
- Instrumentation — insère des compteurs de couverture dans chaque branche du code.
- Linkage — lie le programme avec la bibliothèque LibFuzzer qui fournit le
main(), le moteur de mutation et la boucle de fuzzing.
Pour compiler le code de production séparément du harness (utile dans les projets structurés avec CMake) :
# Compiler la bibliothèque avec l'instrumentation de couverture uniquement
clang++ -std=c++23 -O1 -g \
-fsanitize=fuzzer-no-link,address,undefined \
-fno-omit-frame-pointer \
-c src/json_parser.cpp -o json_parser.o
# Compiler et linker le harness avec le runtime LibFuzzer complet
clang++ -std=c++23 -O1 -g \
-fsanitize=fuzzer,address,undefined \
-fno-omit-frame-pointer \
fuzz/fuzz_json_parser.cpp json_parser.o \
-o fuzz_json_parserLe flag -fsanitize=fuzzer-no-link instrumente le code sans lier le runtime LibFuzzer. Il est utilisé pour les fichiers de bibliothèque qui seront liés ensuite avec un harness (qui, lui, utilise -fsanitize=fuzzer complet).
# Syntaxe : ./fuzz_target [corpus_dirs...] [options...]
./fuzz_json_parser fuzz/corpus/json/LibFuzzer commence par charger les entrées existantes dans le corpus, puis lance le fuzzing. Les résultats s'affichent sur stderr :
INFO: Seed: 3251205890
INFO: Loaded 7 modules (12543 inline 8-bit counters)
INFO: Loaded 1 PC tables (12543 PCs)
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: 7 files found in fuzz/corpus/json/
INFO: seed corpus: files: 7 min: 2b max: 156b total: 423b
#8 INITED cov: 142 ft: 183 corp: 7/423b exec/s: 0 rss: 35Mb
#1024 pulse cov: 198 ft: 267 corp: 23/1854b exec/s: 512 rss: 36Mb
#8192 pulse cov: 234 ft: 312 corp: 41/3291b exec/s: 4096 rss: 38Mb
#65536 pulse cov: 289 ft: 398 corp: 67/8812b exec/s: 16384 rss: 42Mb
Lecture des colonnes de log :
| Colonne | Signification |
|---|---|
#N |
Nombre d'exécutions effectuées |
INITED / pulse / NEW / REDUCE |
Type d'événement (voir ci-dessous) |
cov |
Nombre de blocs de base couverts |
ft |
Nombre de features (couverture fine, incluant les hit counts) |
corp |
Nombre d'entrées dans le corpus / taille totale |
exec/s |
Exécutions par seconde |
rss |
Mémoire résidente |
Types d'événements :
| Événement | Signification |
|---|---|
INITED |
Corpus initial chargé |
NEW |
Nouvelle entrée ajoutée au corpus (couverture inédite) |
REDUCE |
Entrée existante remplacée par une version plus courte (même couverture) |
pulse |
Heartbeat périodique (pas de nouveauté) |
BINGO / crash |
Bug trouvé — le fichier est sauvegardé |
./fuzz_json_parser corpus/ \
-max_len=4096 \ # Taille maximale des entrées générées
-timeout=10 \ # Timeout par exécution (secondes)
-dict=dictionaries/json.dict \ # Dictionnaire de tokens
-jobs=4 \ # Nombre de jobs parallèles
-workers=4 \ # Nombre de workers simultanés
-max_total_time=3600 \ # Durée totale maximale (secondes)
-print_final_stats=1 # Afficher les statistiques finalesTableau des options les plus utiles :
| Option | Défaut | Description |
|---|---|---|
-max_len=N |
4096 | Taille max des entrées. Réduire accélère le fuzzing, augmenter explore des cas plus complexes |
-timeout=N |
1200 | Secondes avant qu'une exécution soit considérée comme un hang |
-dict=file |
aucun | Dictionnaire de tokens pour le format ciblé |
-jobs=N |
1 | Nombre de jobs à exécuter au total |
-workers=N |
min(jobs, CPUs/2) | Nombre de jobs simultanés |
-max_total_time=N |
0 (infini) | Durée totale de la campagne en secondes |
-runs=N |
-1 (infini) | Nombre d'exécutions avant arrêt |
-seed=N |
aléatoire | Graine pour le générateur aléatoire (reproductibilité) |
-only_ascii=1 |
0 | Restreindre les mutations aux caractères ASCII imprimables |
-artifact_prefix=path/ |
. |
Répertoire pour les crashs et les timeouts |
-print_final_stats=1 |
0 | Afficher un résumé en fin de campagne |
-fork=N |
0 | Mode fork : lance N processus (isolation des crashs) |
-minimize_crash=1 |
0 | Minimiser automatiquement un crash donné |
-merge=1 |
0 | Mode merge : fusionner et dédupliquer des corpus |
Comme pour AFL++, un seed corpus de qualité accélère considérablement la convergence. Les mêmes principes s'appliquent (voir section 45.5) : couvrir les cas structurels, rester minimal, inclure des fichiers réels.
# Créer un corpus initial minimal
mkdir -p fuzz/corpus/json
echo '{}' > fuzz/corpus/json/empty_obj.json
echo '[]' > fuzz/corpus/json/empty_arr.json
echo '{"a":1}' > fuzz/corpus/json/simple.json
echo '{"a":{"b":{"c":true}}}' > fuzz/corpus/json/nested.json
echo '[1,2.0,"s",null,false]' > fuzz/corpus/json/mixed_array.json Pendant le fuzzing, LibFuzzer enrichit le corpus en place : les nouvelles entrées découvertes sont écrites directement dans le répertoire du corpus passé en argument. À l'arrêt, le répertoire contient le corpus initial enrichi de toutes les découvertes.
Si plusieurs répertoires sont passés, le premier est utilisé pour l'écriture et les suivants sont lus uniquement :
# Premier répertoire = écriture + lecture
# Deuxième répertoire = lecture seule (corpus supplémentaire)
./fuzz_json_parser fuzz/corpus/json/ fuzz/corpus/json_extra/Au fil du temps et des campagnes successives, le corpus accumule des entrées redondantes. Le mode merge élimine les entrées qui ne contribuent pas à la couverture :
# Fusionner old_corpus/ dans new_corpus/ en ne gardant que le minimum
mkdir new_corpus
./fuzz_json_parser -merge=1 new_corpus/ old_corpus/Le mode merge est déterministe : il sélectionne l'ensemble minimal d'entrées qui couvre le même nombre de features que le corpus complet. C'est l'opération à exécuter avant de committer le corpus dans le dépôt Git.
# Workflow de maintenance du corpus
# 1. Fuzzer pendant un certain temps
./fuzz_json_parser fuzz/corpus/json/ -max_total_time=3600
# 2. Merger pour éliminer les redondances
mkdir /tmp/merged_corpus
./fuzz_json_parser -merge=1 /tmp/merged_corpus/ fuzz/corpus/json/
# 3. Remplacer l'ancien corpus
rm -rf fuzz/corpus/json/
mv /tmp/merged_corpus fuzz/corpus/json/
# 4. Committer le corpus minimal dans le dépôt
git add fuzz/corpus/json/
git commit -m "fuzz: update seed corpus after merge" Quand LibFuzzer détecte un crash, il sauvegarde l'entrée dans le répertoire courant (ou dans -artifact_prefix) avec un nom descriptif :
crash-adc83b19e793491b1c6ea0fd8b46cd9f32e592fc # Crash (signal)
timeout-7c211433f02024a5b5903e06f8b2e2e01a4a5e6a # Timeout dépassé
oom-e4d909c290d0fb1ca068ffaddf22cbd0 # Out-of-memory
slow-unit-... # Exécution anormalement lente
# Exécuter le fuzz target avec l'entrée qui a causé le crash
./fuzz_json_parser crash-adc83b19e793491b1c6ea0fd8b46cd9f32e592fcAvec ASan activé, la sortie affiche le diagnostic complet (type de bug, stack trace, site d'allocation et de libération pour les UAF).
# LibFuzzer réduit l'entrée au minimum nécessaire pour reproduire le crash
./fuzz_json_parser -minimize_crash=1 -exact_artifact_path=minimized_crash.bin \
crash-adc83b19e793491b1c6ea0fd8b46cd9f32e592fcL'option -exact_artifact_path spécifie le nom du fichier minimisé. Sans elle, LibFuzzer écrit dans le répertoire courant avec un nom automatique.
Les crashs minimisés doivent être pérennisés en tests de régression pour empêcher les régressions futures. Deux approches complémentaires :
Approche 1 — Fichier de corpus permanent :
# Ajouter l'entrée minimisée au seed corpus
cp minimized_crash.bin fuzz/corpus/json/regression_issue_42.bin
git add fuzz/corpus/json/regression_issue_42.bin
git commit -m "fuzz: add regression corpus for issue #42" LibFuzzer charge ce fichier au démarrage de chaque campagne. Si une régression réintroduit le bug, le fuzzer le détecte immédiatement.
Approche 2 — Test unitaire Google Test :
// tests/test_json_regressions.cpp
#include <gtest/gtest.h>
#include <fstream>
#include <vector>
#include "json_parser.h"
// Helper : charger un fichier binaire
static std::vector<uint8_t> load_file(const std::string& path) {
std::ifstream f(path, std::ios::binary);
return {std::istreambuf_iterator<char>(f),
std::istreambuf_iterator<char>()};
}
TEST(JsonFuzzRegressions, Issue42_HeapOverflow) {
auto data = load_file("fuzz/corpus/json/regression_issue_42.bin");
// Doit terminer sans crash ni undefined behavior
EXPECT_NO_FATAL_FAILURE(
try { json::parse(data.data(), data.size()); }
catch (const json::ParseError&) { /* OK */ }
);
}Les deux approches se complètent : le fichier de corpus est le filet de sécurité pour le fuzzer, le test unitaire est le filet de sécurité pour la CI classique (build sans fuzzer).
LibFuzzer propose un mode fork qui lance N processus fils, chacun effectuant du fuzzing indépendant avec synchronisation du corpus :
# Lancer 8 processus de fuzzing en parallèle
./fuzz_json_parser fuzz/corpus/json/ -fork=8 -max_total_time=3600Le mode fork offre un avantage supplémentaire par rapport à l'exécution in-process classique : l'isolation des crashs. Dans le mode normal (sans fork), un crash termine le processus entier et arrête le fuzzing. En mode fork, seul le processus fils crashe — le processus parent enregistre le crash et relance un nouveau fils.
Alternative au mode fork, les options -jobs et -workers lancent plusieurs campagnes successives ou simultanées :
# 8 jobs, 4 exécutés simultanément
./fuzz_json_parser fuzz/corpus/json/ -jobs=8 -workers=4 -max_total_time=7200Chaque job est un processus séparé avec son propre seed aléatoire. Les jobs partagent le même répertoire de corpus et bénéficient mutuellement de leurs découvertes.
| Approche | Isolation crashs | Synchronisation corpus | Simplicité |
|---|---|---|---|
-fork=N |
Oui | Automatique | Simple — un seul processus parent |
-jobs=N -workers=M |
Oui | Via le répertoire partagé | Simple — gestion intégrée |
| Lancement manuel (N terminaux) | Oui | Manuelle (merge périodique) | Flexible mais plus lourd |
Pour la majorité des cas, -fork=N est le choix le plus simple et le plus efficace.
option(ENABLE_LIBFUZZER "Build LibFuzzer fuzz targets" OFF)
if(ENABLE_LIBFUZZER)
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
message(FATAL_ERROR "LibFuzzer nécessite Clang")
endif()
# Instrumentation de couverture pour les bibliothèques du projet
# fuzzer-no-link : instrumente sans lier le runtime LibFuzzer
set(FUZZ_INSTRUMENT_FLAGS
-fsanitize=fuzzer-no-link,address,undefined
-fno-omit-frame-pointer -O1 -g
)
# Flags pour les harnesses (lient le runtime LibFuzzer)
set(FUZZ_LINK_FLAGS
-fsanitize=fuzzer,address,undefined
-fno-omit-frame-pointer -O1 -g
)
# Bibliothèque instrumentée (partagée entre tous les fuzz targets)
add_library(json_parser_fuzz STATIC src/json_parser.cpp)
target_compile_options(json_parser_fuzz PRIVATE ${FUZZ_INSTRUMENT_FLAGS})
# Fuzz target : JSON parser
add_executable(fuzz_json_parser fuzz/fuzz_json_parser.cpp)
target_compile_options(fuzz_json_parser PRIVATE ${FUZZ_LINK_FLAGS})
target_link_options(fuzz_json_parser PRIVATE ${FUZZ_LINK_FLAGS})
target_link_libraries(fuzz_json_parser PRIVATE json_parser_fuzz)
# Fuzz target : Protocol parser
add_executable(fuzz_protocol fuzz/fuzz_protocol.cpp)
target_compile_options(fuzz_protocol PRIVATE ${FUZZ_LINK_FLAGS})
target_link_options(fuzz_protocol PRIVATE ${FUZZ_LINK_FLAGS})
target_link_libraries(fuzz_protocol PRIVATE protocol_parser_fuzz)
# Cible custom pour lancer le fuzzing
add_custom_target(run_fuzz_json
COMMAND $<TARGET_FILE:fuzz_json_parser>
${CMAKE_SOURCE_DIR}/fuzz/corpus/json/
-dict=${CMAKE_SOURCE_DIR}/fuzz/dictionaries/json.dict
-max_total_time=300
DEPENDS fuzz_json_parser
COMMENT "Fuzzing JSON parser (5 minutes)"
)
endif()# Build
cmake -B build_fuzz -DENABLE_LIBFUZZER=ON -DCMAKE_CXX_COMPILER=clang++
cmake --build build_fuzz
# Fuzzing rapide (5 minutes)
cmake --build build_fuzz --target run_fuzz_json
# Fuzzing long (manuel)
./build_fuzz/fuzz_json_parser fuzz/corpus/json/ -dict=fuzz/dictionaries/json.dict# .github/workflows/libfuzzer.yml
name: LibFuzzer CI
on:
push:
branches: [main]
pull_request:
schedule:
- cron: '0 3 * * *' # Fuzzing nocturne
jobs:
# Job rapide sur chaque PR : vérifier que le corpus ne régresse pas
fuzz-regression:
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: Build fuzz targets
run: |
cmake -B build -DENABLE_LIBFUZZER=ON -DCMAKE_CXX_COMPILER=clang++
cmake --build build
- name: Run corpus (regression check)
run: |
# Exécuter le corpus existant sans fuzzing
# -runs=0 : charger le corpus et vérifier, sans muter
./build/fuzz_json_parser fuzz/corpus/json/ -runs=0
# Job long en schedule : fuzzing exploratoire
fuzz-explore:
runs-on: ubuntu-latest
if: github.event_name == 'schedule'
timeout-minutes: 360
steps:
- uses: actions/checkout@v4
# Restaurer le corpus de la campagne précédente
- uses: actions/cache@v4
with:
path: fuzz/corpus/
key: libfuzzer-corpus-${{ github.sha }}
restore-keys: libfuzzer-corpus-
- name: Build fuzz targets
run: |
cmake -B build -DENABLE_LIBFUZZER=ON -DCMAKE_CXX_COMPILER=clang++
cmake --build build
- name: Fuzz JSON parser
run: |
./build/fuzz_json_parser fuzz/corpus/json/ \
-dict=fuzz/dictionaries/json.dict \
-fork=4 \
-max_total_time=18000 \
-artifact_prefix=fuzz/crashes/ \
-print_final_stats=1
- name: Check for crashes
run: |
CRASHES=$(find fuzz/crashes/ -name 'crash-*' -o -name 'timeout-*' | wc -l)
echo "Crashes/timeouts: $CRASHES"
if [ "$CRASHES" -gt 0 ]; then
echo "::error::LibFuzzer found $CRASHES issue(s)"
for f in fuzz/crashes/crash-* fuzz/crashes/timeout-*; do
[ -f "$f" ] || continue
echo "=== $f ==="
./build/fuzz_json_parser "$f" 2>&1 || true
done
exit 1
fi
- name: Merge corpus
if: always()
run: |
mkdir -p /tmp/merged
./build/fuzz_json_parser -merge=1 /tmp/merged/ fuzz/corpus/json/ || true
rm -rf fuzz/corpus/json/
mv /tmp/merged fuzz/corpus/json/
- name: Upload crashes
if: failure()
uses: actions/upload-artifact@v4
with:
name: libfuzzer-crashes-${{ github.run_id }}
path: fuzz/crashes/Points clés de cette configuration :
- Deux jobs distincts — un job rapide de régression sur chaque push/PR (
-runs=0exécute le corpus existant sans mutation, vérifie qu'aucune régression n'a été introduite), et un job long de fuzzing exploratoire en schedule nocturne. - Cache du corpus —
actions/cachepersiste le corpus entre les exécutions. Chaque nuit, le fuzzing reprend là où il s'était arrêté et accumule de la couverture. - Merge automatique — après chaque campagne, le corpus est mergé pour ne conserver que les entrées utiles avant d'être mis en cache.
- Mode fork —
-fork=4utilise les cœurs disponibles du runner.
Pour les projets open source, l'intégration avec OSS-Fuzz est le chemin le plus efficace vers le fuzzing continu professionnel. OSS-Fuzz fournit une infrastructure dédiée (machines, storage, alertes) et exécute le fuzzing en permanence.
L'intégration nécessite un répertoire de configuration dans le dépôt du projet :
project-repo/
└── .clusterfuzzlite/ # ou soumission au repo oss-fuzz
├── Dockerfile # Build du fuzz target
├── build.sh # Script de compilation
└── project.yaml # Métadonnées du projet
# .clusterfuzzlite/Dockerfile
FROM gcr.io/oss-fuzz-base/base-builder
RUN apt-get update && apt-get install -y cmake
COPY . /src/myproject
WORKDIR /src/myproject # .clusterfuzzlite/build.sh
mkdir build && cd build
cmake .. -DCMAKE_CXX_COMPILER=clang++ -DENABLE_LIBFUZZER=ON
cmake --build .
cp fuzz_json_parser fuzz_protocol $OUT/
cp ../fuzz/dictionaries/*.dict $OUT/
cp -r ../fuzz/corpus/* $OUT/ ClusterFuzzLite (la version légère d'OSS-Fuzz) peut s'exécuter directement dans GitHub Actions, GitLab CI ou tout autre environnement CI, sans soumission au programme OSS-Fuzz complet.
LibFuzzer est le fuzzer le plus rapide et le plus simple à intégrer pour le code C++ compilé avec Clang :
- Un seul flag (
-fsanitize=fuzzer) pour instrumenter, lier et fuzzer. Utiliser-fsanitize=fuzzer-no-linkpour les bibliothèques. - Toujours combiner avec ASan et UBSan —
-fsanitize=fuzzer,address,undefinedest la configuration standard. - Utiliser
FuzzedDataProviderpour les fuzz targets qui nécessitent des entrées structurées (multiples paramètres typés). - Gérer le corpus — seed initial minimal, merge régulier, stockage dans le dépôt Git.
- Utiliser
-fork=Npour le fuzzing parallèle avec isolation des crashs. - Deux stratégies CI — régression rapide sur chaque PR (
-runs=0), fuzzing exploratoire nocturne avec cache du corpus. - Minimiser et pérenniser les crashs —
-minimize_crash=1pour réduire, puis convertir en test de régression (fichier corpus + test unitaire GTest).
- Section 45.5.1 — AFL++ : le fuzzer externe, complémentaire de LibFuzzer pour les cibles qui ne sont pas des fonctions isolées.
- Section 29.4 — Sanitizers : configuration détaillée d'ASan, UBSan et MSan.
- Chapitre 33 — Google Test : écriture des tests de régression à partir des crashs de fuzzing.
- Section 38.2 — GitHub Actions : workflows CI avancés.
- Section 45.6 — Sécurité mémoire en 2026 : le fuzzing comme pilier de la stratégie de sécurité, aux côtés de l'analyse statique, des sanitizers et des Safety Profiles.
⏭️ Sécurité mémoire : Réponses concrètes du comité C++ en 2026