GitHub Security Lab livre un agent qui fuzze un dépôt C/C++ de bout en bout
Le 24 septembre 2026, GitHub Security Lab a publié un pipeline de fuzzing autonome qui écrit ses harnais, lit sa couverture et rédige ses rapports de vulnérabilité. Le goulot du fuzzing — l’attention humaine — est délégué à un LLM, mais le code s’exécute sans conteneur sur l’hôte.
24 septembre 2026. Antonio Morales, chercheur au GitHub Security Lab, publie le Fuzzing Taskflow, un pipeline de fuzzing autonome pour les projets C/C++. 2014. Michal Zalewski publiait AFL, le fuzzer à couverture de code qui a popularisé la discipline. 2016. Google lançait OSS-Fuzz, qui a industrialisé le fuzzing continu de l’open source. Pourquoi c’est important : le goulot du fuzzing n’a jamais été le calcul, mais l’attention humaine — et c’est elle que ce pipeline délègue à un LLM, du premier harnais jusqu’au rapport de vulnérabilité.
Un pipeline qui va du dépôt au rapport
Le Fuzzing Taskflow est construit sur le framework Taskflow Agent du GitHub Security Lab, dédié à l’automatisation de la sécurité pilotée par LLM. La promesse tient en une phrase : pointez-le vers un dépôt GitHub, il fait le reste.
Concrètement, l’agent identifie les points d’entrée pertinents, analyse le système de build, écrit les harnais, lance AFL++, lit les rapports de couverture, améliore les harnais, trie chaque crash et rédige un rapport de vulnérabilité pour chaque bug distinct — sans supervision humaine entre les étapes. L’exemple donné dans l’annonce est explicite : lancer la campagne sur xz, le projet dont la porte dérobée a ébranlé la supply chain en 2024, ou sur cJSON pour un simple test de fumée.
git clone https://github.com/GitHubSecurityLab/seclab-taskflows-fuzzing
cd seclab-taskflows-fuzzing
./scripts/fuzzing/run_fuzzing.sh tukaani-project/xz L’architecture tient en trois couches. Un pilote shell, run_fuzzing.sh, enchaîne les étapes du pipeline. Une série de YAML de taskflows — un par étape — porte les prompts qui disent à l’agent quoi faire. Un ensemble d’outils MCP exécute le travail réel : lancer AFL, compiler un harnais, stocker un crash, lire un rapport de couverture. La règle de conception affichée est une séparation stricte des responsabilités : le LLM décide, les outils exécutent. L’agent ne touche jamais AFL ou clang directement ; il compose le pipeline à partir de ces briques. Tout l’état transite par une base SQLite, fuzz_context.db, jamais par la mémoire partagée entre étapes.
La boucle de couverture, cœur du système
La partie qui automatise le plus fidèlement le travail manuel est la boucle de couverture. Améliorer la couverture d’un fuzzer à la main est un cycle bien connu : on mesure la couverture, on cherche les branches non atteintes, puis on écrit un harnais ou un seed pour les toucher. Le Fuzzing Taskflow confie ces deux étapes à l’agent.
À chaque itération, pour chaque harnais, l’agent lance AFL sur un budget de temps, rejoue la file d’attente contre le binaire de couverture pour obtenir un vrai rapport, puis lit la liste des branches non couvertes. Il choisit alors une action parmi un petit ensemble : ajouter un seed conçu pour atteindre une branche, modifier le harnais pour appeler une API supplémentaire, enrichir le dictionnaire AFL avec les constantes magiques qu’une comparaison vérifie, ou simplement ignorer l’écart s’il s’agit d’un chemin d’erreur froid ou de code fournisseur sans intérêt.
Les budgets de temps doublent à chaque itération — 30 s, 60 s, 120 s, 240 s, 480 s, 960 s, soit environ 32 minutes par cible. L’idée est de dépenser des rounds courts et bon marché au début, quand la couverture facile est abondante, puis des rounds longs quand le fuzzer doit franchir une garde difficile. L’arrêt est gouverné par une détection de plateau : dès que deux itérations consécutives gagnent chacune moins d’un seuil configurable — 1 % de couverture de ligne absolue par défaut — la boucle conclut à des rendements décroissants et passe à la suite.
Un fuzzing qui comprend la structure des formats
Les mutations octet-à-octet d’AFL excellent sur les formats binaires, mais peinent sur les entrées structurées et textuelles. Le pipeline embarque donc quatre mécanismes complémentaires pour produire des entrées structure-aware.
Pour les formats reconnus — JSON, XML, expressions régulières, PNG, binaires TLV à préfixe de longueur — il livre des dictionnaires AFL préconstruits et des fichiers LLVMFuzzerCustomMutator en C. Le mutateur JSON pratique le splicing de jetons et la duplication de crochets équilibrés ; celui d’XML connaît les balises, les entités et les jetons billion-laughs ; celui des regex porte de vrais motifs ReDoS. Chaque mutateur délègue la moitié de ses mutations au mutateur octet par défaut, pour conserver l’aléatoire du moteur au lieu de le combattre.
Pour les formats inconnus, il génère un mutateur à la volée en scannant les fichiers .c/h de la cible, extrait les littéraux de chaînes et les constantes numériques 32 bits, puis les utilise comme jetons de splice. L’intuition est simple : les valeurs magiques les plus intéressantes qu’un parseur vérifie sont presque toujours écrites quelque part dans son propre code source. Un opérateur de splice de corpus complète le dispositif, en recombinant des sous-régions de fichiers existants d’une façon que le havoc d’AFL ne fait pas bien.
Le tri des crashs, désormais automatisé
Trouver un crash n’est que la moitié du travail. Le triage — souvent la partie la plus fastidieuse — est l’autre endroit où l’agent brille. Trois étapes s’enchaînent après la boucle de fuzzing.
Chaque crash est minimisé avec afl-tmin, rejoué sous ASan pour capturer une trace de pile, puis dédupliqué par un hachage du sommet de pile — cadres normalisés, avec gabarits, espaces de noms inline et suffixes LTO supprimés pour que des crashs sémantiquement identiques fusionnent. Les crashs déjà connus sont rejoués contre le binaire courant pour vérifier si un correctif amont les a résolus. Enfin, l’agent lit le harnais et la fonction fautive, remonte la chaîne d’appel depuis l’API publique, et rédige un rapport Markdown par crash, avec un verdict parmi vulnerability, library_hardening, harness_bug, OOM, timeout, assertion_failure et duplicate.
La distinction entre une vraie vulnérabilité — atteignable et exploitable via une API publique — et un simple harness_bug est précisément le jugement qui exigeait autrefois de tracer le code à la main. Chaque rapport inclut une analyse de cause racine avec références fichier:ligne, un argument de portée, une évaluation d’exploitabilité, un correctif proposé en diff unifié et une ébauche de test de régression. L’auteur est transparent sur une limite : les correctifs proposés sont marqués « revue requise », car l’analyse de l’agent est bornée par la compréhension que le modèle a du code cible — et il se trompe parfois.
La limite que l’auteur assume : pas de conteneur
Un avertissement, placé avant même le mode d’emploi, mérite toute l’attention d’un RSSI ou d’un SRE. Ce taskflow exécute afl-fuzz, clang et des commandes de build arbitraires choisies par le LLM directement sur l’hôte, sans conteneur intermédiaire. Un agent victime d’une prompt injection pourrait, en principe, faire tout ce que votre utilisateur peut faire.
La recommandation de l’auteur est sans ambiguïté : ne le lancer que dans un environnement jetable — un Codespace ou une VM jetable — et sans privilèges élevés. Le modèle par défaut est Claude Sonnet 5, retenu parce qu’il a passé tous les tests internes sans accroc. L’auteur rappelle enfin qu’un tableau de bord HTML en direct, publié sur le port 8765, permet de suivre la campagne en temps réel — impulsion par harnais, tendance de couverture avec sparklines, carte de chaleur des crashs et chronologie d’itération.
Verdict
Si vous maintenez un projet C/C++, essayez-le sur un dépôt jamais fuzzé : c’est le chemin le plus court pour un premier harnais et un premier tri de crashs, et le code est libre. Si votre projet est déjà dans OSS-Fuzz, l’intérêt est ailleurs — augmenter la couverture en laissant l’agent chasser les branches que vos harnais n’atteignent pas. Quel que soit votre cas, exécutez-le dans une VM jetable sans élévation de privilèges et traitez chaque verdict de l’agent comme un point de départ pour un humain, jamais comme une conclusion. La leçon générale dépasse ce projet : quand vous confiez un pipeline de sécurité à un LLM, gardez la séparation des responsabilités — à l’agent la décision, à vos outils l’exécution, et à vous la revue.