Skip to content

Latest commit

 

History

History
679 lines (522 loc) · 26.3 KB

File metadata and controls

679 lines (522 loc) · 26.3 KB

🔝 Retour au Sommaire

45.5.2 — LibFuzzer et intégration

Section 45.5 — Fuzzing avec AFL++, LibFuzzer ⭐

Chapitre 45 — Sécurité en C++ ⭐


Introduction

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.


Prérequis

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.


Écrire un fuzz target

La fonction LLVMFuzzerTestOneInput

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 exacteextern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size). Le extern "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 propre main(). 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() ou abort() — LibFuzzer interprète un abort comme un crash. Si le code testé appelle exit() légitimement, il faut l'intercepter ou le supprimer dans le harness.

Catcher les bonnes exceptions

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;
}

Fuzz target avec initialisation

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;
}

Fuzz target structuré avec FuzzedDataProvider

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.


Compilation et exécution

Compilation de base

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_parser

Le flag -fsanitize=fuzzer accomplit deux choses :

  1. Instrumentation — insère des compteurs de couverture dans chaque branche du code.
  2. 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_parser

Le 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).

Exécution

# 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é

Options essentielles

./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 finales

Tableau 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

Gestion du corpus

Corpus initial

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  

Corpus évolutif

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/

Fusion et déduplication (merge)

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"  

Gestion des crashs

Fichiers générés

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  

Reproduire un crash

# Exécuter le fuzz target avec l'entrée qui a causé le crash
./fuzz_json_parser crash-adc83b19e793491b1c6ea0fd8b46cd9f32e592fc

Avec ASan activé, la sortie affiche le diagnostic complet (type de bug, stack trace, site d'allocation et de libération pour les UAF).

Minimiser un crash

# 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-adc83b19e793491b1c6ea0fd8b46cd9f32e592fc

L'option -exact_artifact_path spécifie le nom du fichier minimisé. Sans elle, LibFuzzer écrit dans le répertoire courant avec un nom automatique.

Convertir en test de régression

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).


Fuzzing parallèle

Mode -fork

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=3600

Le 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.

Mode -jobs/-workers

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=7200

Chaque 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.

Comparaison des approches parallèles

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.


Intégration CMake

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

Intégration CI/CD

GitHub Actions

# .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=0 exé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 corpusactions/cache persiste 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=4 utilise les cœurs disponibles du runner.

OSS-Fuzz

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.


Résumé

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-link pour les bibliothèques.
  • Toujours combiner avec ASan et UBSan-fsanitize=fuzzer,address,undefined est la configuration standard.
  • Utiliser FuzzedDataProvider pour 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=N pour 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=1 pour réduire, puis convertir en test de régression (fichier corpus + test unitaire GTest).

Pour aller plus loin

  • 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