🔝 Retour au Sommaire
AddressSanitizer — ASan — est le sanitizer le plus important de votre arsenal. Il détecte les erreurs d'accès mémoire : les débordements de buffer, les accès à de la mémoire libérée, les doubles libérations, et les fuites mémoire. Ce sont les bugs qui causent des crashes inexplicables, des corruptions de données silencieuses, et des vulnérabilités de sécurité critiques.
Sans ASan, un buffer overflow de quelques octets peut passer inaperçu pendant des années. Le programme fonctionne correctement — par chance — parce que les octets écrasés ne sont pas utilisés immédiatement. Puis un jour, une modification anodine du code change l'agencement de la mémoire, et le même overflow déclenche un crash impossible à reproduire. Avec ASan, l'overflow est détecté à la première exécution, à la ligne exacte où il se produit, avec un rapport qui montre quelle allocation est concernée et de combien l'accès dépasse.
ASan a été développé par Google et intégré à Clang en 2011, puis à GCC en 2013. Il est utilisé en continu sur les bases de code de Chrome, Android, le noyau Linux, et des centaines de projets open source. Il a permis de détecter des dizaines de milliers de bugs avant qu'ils n'atteignent la production.
g++ -fsanitize=address -g -O1 -fno-omit-frame-pointer -o prog main.cppLes flags, un par un :
-fsanitize=address— active l'instrumentation ASan. Le compilateur insère des vérifications autour de chaque accès mémoire et remplace l'allocateur standard par l'allocateur ASan.-g— génère les informations de débogage pour que les rapports contiennent les noms de fichiers et numéros de lignes.-O1— niveau d'optimisation recommandé.-O0fonctionne mais produit un binaire plus lent.-O2fonctionne aussi mais peut rendre certains rapports moins lisibles.-fno-omit-frame-pointer— préserve le frame pointer pour des backtraces complètes et rapides.
Le flag doit être passé à la fois au compilateur et au linker. Si vous compilez et linkez en une seule commande (g++ -fsanitize=address ... -o prog main.cpp), c'est automatique. Si vous séparez compilation et linkage, n'oubliez pas le flag au linkage :
g++ -fsanitize=address -g -O1 -fno-omit-frame-pointer -c main.cpp -o main.o
g++ -fsanitize=address main.o -o prog # -fsanitize aussi ici ASan et UBSan sont compatibles et se complètent. La combinaison est recommandée comme configuration par défaut :
g++ -fsanitize=address,undefined -g -O1 -fno-omit-frame-pointer -o prog main.cppUn seul binaire, deux sanitizers actifs. Le surcoût additionnel d'UBSan au-dessus d'ASan est négligeable.
Comprendre le mécanisme interne d'ASan n'est pas nécessaire pour l'utiliser, mais ça aide à comprendre ses rapports et ses limites.
ASan maintient une shadow memory — une copie compacte de l'espace d'adressage qui enregistre, pour chaque groupe de 8 octets de mémoire applicative, si ces octets sont accessibles ou non. Chaque octet de shadow memory encode l'état d'un groupe de 8 octets :
- 0 : les 8 octets sont accessibles
- 1-7 : seuls les N premiers octets sont accessibles (fin d'allocation non alignée)
- Valeur négative : les 8 octets sont inaccessibles (zone interdite)
Avant chaque accès mémoire, le code instrumenté consulte la shadow memory. Si l'octet correspondant indique que la zone est inaccessible, ASan signale l'erreur.
ASan entoure chaque allocation de redzones — des zones de mémoire marquées comme inaccessibles dans la shadow memory :
┌──────────┬─────────────────────┬──────────┐
│ Redzone │ Allocation réelle │ Redzone │
│ (poison) │ (accessible) │ (poison) │
└──────────┴─────────────────────┴──────────┘
Si un accès touche une redzone, c'est un overflow (ou underflow). La taille par défaut des redzones est de 16 octets, mais elle peut être augmentée pour détecter des overflows plus grands :
ASAN_OPTIONS="redzone=128" ./prog # Redzones de 128 octetsQuand un bloc de mémoire est libéré (delete, free), ASan ne le rend pas immédiatement au système. Il le place en quarantaine — la mémoire est marquée comme inaccessible dans la shadow memory, mais elle n'est pas réutilisée. Si le programme accède à cette mémoire après la libération, ASan détecte le use-after-free.
La quarantaine a une taille limitée (256 Mo par défaut). Quand elle est pleine, les blocs les plus anciens sont recyclés. Cela signifie qu'un use-after-free qui accède à de la mémoire libérée très longtemps auparavant peut échapper à la détection.
L'accès au-delà des limites d'une allocation sur le heap — le bug mémoire le plus courant.
// heap_overflow.cpp
#include <cstdio>
int main() {
int* array = new int[10];
for (int i = 0; i <= 10; ++i) { // Bug : <= au lieu de <
array[i] = i * 100;
}
std::printf("Dernier : %d\n", array[9]);
delete[] array;
return 0;
}La boucle itère de 0 à 10 inclus, mais le tableau n'a que 10 éléments (indices 0 à 9). L'écriture à array[10] dépasse de 4 octets. Sans ASan, ce programme peut fonctionner parfaitement — les 4 octets écrasés font partie de la redzone du gestionnaire de mémoire ou d'une allocation adjacente.
g++ -fsanitize=address -g -O1 -fno-omit-frame-pointer -o heap_overflow heap_overflow.cpp
./heap_overflow=================================================================
==54321==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x604000000038
at pc 0x0000004f4b27 bp 0x7ffc8a9b1230 sp 0x7ffc8a9b1228
WRITE of size 4 at 0x604000000038 thread T0
#0 0x4f4b26 in main heap_overflow.cpp:7
#1 0x7f8a12345890 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58
#2 0x7f8a12345949 in __libc_start_main csu/../csu/libc-start.c:360
#3 0x4f4a04 in _start (heap_overflow+0x4f4a04)
0x604000000038 is located 0 bytes after 40-byte region [0x604000000010,0x604000000038)
allocated by thread T0 here:
#0 0x4e8c30 in operator new[](unsigned long) (heap_overflow+0x4e8c30)
#1 0x4f4a90 in main heap_overflow.cpp:5
#2 0x7f8a12345890 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58
SUMMARY: AddressSanitizer: heap-buffer-overflow heap_overflow.cpp:7 in main
Shadow bytes around the buggy address:
[...]
==54321==ABORTING
Ligne d'erreur : heap-buffer-overflow — le type d'erreur est clair.
WRITE of size 4 at 0x604000000038 thread T0 — une écriture de 4 octets (un int) dans le thread principal.
Pile d'appels de l'accès fautif : heap_overflow.cpp:7 — la ligne de la boucle array[i] = i * 100 quand i == 10.
0 bytes after 40-byte region [0x604000000010,0x604000000038) — l'accès se situe immédiatement après un bloc de 40 octets (10 entiers × 4 octets). Le dépassement est de 0 octets après la fin du bloc — c'est-à-dire que l'écriture commence exactement au premier octet hors limites.
Pile d'appels de l'allocation : heap_overflow.cpp:5 — le new int[10]. Vous voyez à la fois où le bug se manifeste et quelle allocation est insuffisante.
L'accès au-delà d'un tableau alloué sur la pile. Particulièrement dangereux car il peut écraser l'adresse de retour ou d'autres variables locales.
// stack_overflow.cpp
#include <cstring>
#include <cstdio>
void process(const char* input) {
char buffer[16];
std::strcpy(buffer, input); // Bug : pas de vérification de taille
std::printf("Traité : %s\n", buffer);
}
int main() {
process("cette-chaine-est-beaucoup-trop-longue-pour-le-buffer");
return 0;
}g++ -fsanitize=address -g -O1 -fno-omit-frame-pointer -o stack_overflow stack_overflow.cpp
./stack_overflow💡 Note Ubuntu : sur les distributions récentes, la protection
_FORTIFY_SOURCE(activée par défaut) peut intercepter lestrcpyavant ASan. Si vous obtenez*** buffer overflow detected ***au lieu d'un rapport ASan, ajoutez-D_FORTIFY_SOURCE=0à la compilation pour désactiver cette protection et laisser ASan faire son travail.
==54322==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc8a9b1240
at pc 0x0000004f4c10 bp 0x7ffc8a9b1210 sp 0x7ffc8a9b0dd0
WRITE of size 53 at 0x7ffc8a9b1240 thread T0
#0 0x4f4c0f in __interceptor_strcpy (stack_overflow+0x4f4c0f)
#1 0x4f5120 in process(char const*) stack_overflow.cpp:6
#2 0x4f5190 in main stack_overflow.cpp:11
Address 0x7ffc8a9b1240 is located in stack of thread T0 at offset 48 in frame
#0 0x4f50a0 in process(char const*) stack_overflow.cpp:4
This frame has 1 object(s):
[32, 48) 'buffer' (line 5) <== Memory access at offset 48 overflows this variable
Le rapport est plus détaillé pour les overflows de stack : ASan identifie la variable locale exacte (buffer, ligne 5) et indique que l'accès à l'offset 48 dépasse la fin de cette variable (qui occupe les offsets 32 à 48 dans le frame).
C'est le type de bug que Valgrind ne détecte pas — Valgrind ne surveille pas les accès à la pile. ASan est le seul outil courant capable de détecter les stack buffer overflows.
L'accès au-delà d'un tableau global ou statique.
// global_overflow.cpp
#include <cstdio>
int lookup_table[8] = {10, 20, 30, 40, 50, 60, 70, 80};
int get_value(int index) {
return lookup_table[index]; // Pas de vérification de bornes
}
int main() {
std::printf("Valeur : %d\n", get_value(12)); // index hors limites
return 0;
}==54323==ERROR: AddressSanitizer: global-buffer-overflow on address 0x000000601090
at pc 0x0000004f4a60 bp 0x7ffc8a9b1250 sp 0x7ffc8a9b1248
READ of size 4 at 0x000000601090 thread T0
#0 0x4f4a5f in get_value(int) global_overflow.cpp:6
#1 0x4f4ab0 in main global_overflow.cpp:10
0x000000601090 is located 16 bytes after global variable 'lookup_table'
defined in 'global_overflow.cpp:3:5' (0x601060) of size 32
ASan identifie la variable globale (lookup_table, définie à la ligne 3, taille 32 octets) et le dépassement (16 octets après la fin, soit 4 éléments au-delà).
L'accès à de la mémoire qui a été libérée. L'un des bugs les plus insidieux en C++ : la mémoire libérée peut être réallouée à un autre objet, et l'accès via l'ancien pointeur lit ou écrit des données qui appartiennent maintenant à quelqu'un d'autre.
// use_after_free.cpp
#include <cstdio>
#include <string>
int main() {
std::string* msg = new std::string("hello world");
std::string& ref = *msg;
delete msg;
// Bug : ref pointe vers de la mémoire libérée
std::printf("Message : %s\n", ref.c_str());
return 0;
}==54324==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010
at pc 0x0000004f5a40 bp 0x7ffc8a9b1230 sp 0x7ffc8a9b1228
READ of size 8 at 0x602000000010 thread T0
#0 0x4f5a3f in main use_after_free.cpp:11
#1 0x7f8a12345890 in __libc_start_call_main (...)
0x602000000010 is located 0 bytes inside of 32-byte region [0x602000000010,0x602000000030)
freed by thread T0 here:
#0 0x4e9a20 in operator delete(void*) (use_after_free+0x4e9a20)
#1 0x4f5980 in main use_after_free.cpp:8
previously allocated by thread T0 here:
#0 0x4e8c30 in operator new(unsigned long) (use_after_free+0x4e8c30)
#1 0x4f5910 in main use_after_free.cpp:5
SUMMARY: AddressSanitizer: heap-use-after-free use_after_free.cpp:11 in main
Le rapport contient trois piles d'appels :
- L'accès fautif :
use_after_free.cpp:11— la ligne qui lit via la référence dangereuse. - La libération :
use_after_free.cpp:8— ledelete msg. - L'allocation originale :
use_after_free.cpp:5— lenew std::string(...).
Vous avez les trois pièces du puzzle : où la mémoire a été créée, où elle a été détruite, et où on a essayé de l'utiliser après sa destruction. Ce diagnostic complet est la raison pour laquelle ASan est si efficace — avec un seul rapport, vous comprenez le cycle de vie complet du bug.
Libérer deux fois la même allocation.
// double_free.cpp
#include <cstdio>
void cleanup(int* data) {
delete[] data;
}
int main() {
int* values = new int[5]{1, 2, 3, 4, 5};
cleanup(values);
// ... du code intermédiaire ...
delete[] values; // Bug : déjà libéré par cleanup()
return 0;
}==54325==ERROR: AddressSanitizer: attempting double-free on 0x603000000010 in thread T0:
#0 0x4e9b40 in operator delete[](void*) (double_free+0x4e9b40)
#1 0x4f4b80 in main double_free.cpp:14
0x603000000010 is located 0 bytes inside of 20-byte region [0x603000000010,0x603000000024)
freed by thread T0 here:
#0 0x4e9b40 in operator delete[](void*) (double_free+0x4e9b40)
#1 0x4f4a30 in cleanup(int*) double_free.cpp:4
#2 0x4f4b50 in main double_free.cpp:10
previously allocated by thread T0 here:
#0 0x4e8c30 in operator new[](unsigned long) (double_free+0x4e8c30)
#1 0x4f4af0 in main double_free.cpp:8
Même structure à trois piles : le double-free (ligne 14), la première libération (ligne 4 via cleanup), et l'allocation (ligne 8). La solution est évidente : soit cleanup est responsable de la libération, soit main — pas les deux. En C++ moderne, ce bug est éliminé par les smart pointers (chapitre 9).
ASan inclut un détecteur de fuites mémoire — LeakSanitizer (LSan) — qui s'exécute à la fin du programme et signale toute allocation non libérée.
// memory_leak.cpp
#include <cstdio>
#include <vector>
void process_data() {
auto* buffer = new std::vector<int>(1000);
buffer->push_back(42);
// Oubli du delete — le pointeur sort de la portée
}
int main() {
for (int i = 0; i < 100; ++i) {
process_data(); // 100 allocations jamais libérées
}
std::printf("Terminé\n");
return 0;
}Terminé
=================================================================
==54326==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 2400 byte(s) in 100 object(s) allocated from:
#0 0x4e8c30 in operator new(unsigned long) (memory_leak+0x4e8c30)
#1 0x4f4b10 in process_data() memory_leak.cpp:5
#2 0x4f4c20 in main memory_leak.cpp:11
Indirect leak of 401600 byte(s) in 100 object(s) allocated from:
#0 0x4e8c30 in operator new(unsigned long) (memory_leak+0x4e8c30)
#1 0x7f8a1237ab50 in std::vector<int>::_M_default_append(unsigned long) (...)
#2 0x4f4b10 in process_data() memory_leak.cpp:5
SUMMARY: AddressSanitizer: 404000 byte(s) leaked in 200 allocation(s).
LSan distingue deux catégories de fuites :
- Direct leak : l'allocation dont le pointeur a été perdu (le
new std::vector<int>(1000)— 24 octets par vector × 100 = 2400 octets). - Indirect leak : les allocations internes faites par l'objet fuité (le buffer interne du vector — ~4000 octets × 100 = 401600 octets).
Cette distinction est précieuse : les fuites directes sont les causes racine à corriger. Les fuites indirectes disparaissent automatiquement quand les fuites directes sont corrigées.
# Désactiver la détection de fuites (garder seulement ASan)
ASAN_OPTIONS="detect_leaks=0" ./prog
# Activer uniquement la détection de fuites, sans ASan complet
g++ -fsanitize=leak -g -o prog main.cppLe flag -fsanitize=leak active LSan seul, sans l'instrumentation mémoire complète d'ASan. Le binaire est beaucoup plus rapide, mais ne détecte que les fuites — pas les overflows ou les use-after-free.
L'accès à une variable locale après le retour de sa fonction. Le cadre de pile a été détruit, mais un pointeur vers la variable survit.
// use_after_return.cpp
#include <cstdio>
int* get_value() {
int local = 42;
return &local; // Retourne l'adresse d'une variable locale
}
int main() {
int* ptr = get_value();
std::printf("Valeur : %d\n", *ptr); // Bug : ptr pointe vers un frame détruit
return 0;
}La détection du use-after-return nécessite une option d'exécution spécifique — elle n'est pas activée par défaut car elle a un coût mémoire supplémentaire :
ASAN_OPTIONS="detect_stack_use_after_return=1" ./use_after_returnAvec GCC 15+ et Clang 19+, cette option est activée par défaut. Vérifiez le comportement de votre version.
Une variante plus subtile : accéder à une variable après la fin de sa portée (son bloc {}), mais avant le retour de la fonction.
// use_after_scope.cpp
#include <cstdio>
int main() {
int* ptr;
{
int scoped_var = 42;
ptr = &scoped_var;
} // scoped_var est détruit ici
std::printf("Valeur : %d\n", *ptr); // Bug : scoped_var n'existe plus
return 0;
}g++ -fsanitize=address -g -O1 -fno-omit-frame-pointer \
-fsanitize-address-use-after-scope -o use_after_scope use_after_scope.cpp
./use_after_scopeLe flag -fsanitize-address-use-after-scope est nécessaire pour cette détection. Il instrumente les sorties de portée pour empoisonner la mémoire des variables locales dès qu'elles sont hors scope.
Les options ASan sont passées via la variable d'environnement ASAN_OPTIONS. Voici les plus utiles :
Par défaut, ASan arrête le programme à la première erreur. Pour détecter plusieurs erreurs en une seule exécution :
ASAN_OPTIONS="halt_on_error=0" ./progUtile quand vous exécutez une suite de tests et voulez voir tous les problèmes d'un coup. Attention cependant : après une erreur mémoire, l'état du programme est potentiellement corrompu. Les erreurs suivantes peuvent être des conséquences de la première, pas des bugs indépendants.
ASAN_OPTIONS="log_path=/tmp/asan_report" ./prog
# Génère /tmp/asan_report.54321 (PID ajouté automatiquement)Indispensable en CI où vous voulez archiver les rapports comme artefacts.
ASAN_OPTIONS="malloc_context_size=20" ./progPar défaut, ASan capture environ 30 frames dans les piles d'appels. Si votre programme a des piles d'appels profondes (frameworks, callbacks imbriqués), augmentez cette valeur.
ASAN_OPTIONS="quarantine_size_mb=512" ./progAugmente la taille de la quarantaine pour détecter des use-after-free sur des allocations libérées plus tôt. Utile pour les programmes à longue durée de vie avec beaucoup d'allocations.
# Configuration recommandée pour le CI
ASAN_OPTIONS="halt_on_error=0:detect_leaks=1:log_path=/tmp/asan:print_stats=1"
# Configuration recommandée pour le développement interactif
ASAN_OPTIONS="halt_on_error=1:detect_leaks=1:detect_stack_use_after_return=1"ASan a un coût. Il est important de le connaître pour savoir où et quand l'activer :
| Métrique | Surcoût typique |
|---|---|
| Temps d'exécution | ~2x plus lent |
| Mémoire | ~3x plus de mémoire (shadow memory + quarantaine + redzones) |
| Taille du binaire | ~2x plus gros |
| Temps de compilation | ~1.5x plus long |
Un facteur 2x de ralentissement est tout à fait acceptable pour les tests automatisés. C'est incomparablement mieux que Valgrind (20-50x) et ça permet d'exécuter les sanitizers dans le pipeline CI sans exploser les temps de build.
Pour les tests de performance (benchmarks), désactivez les sanitizers — le surcoût fausse les mesures.
ASan et GDB fonctionnent très bien ensemble. Quand ASan détecte une erreur, vous pouvez configurer le programme pour qu'il s'arrête dans GDB au lieu de terminer :
# Option 1 : ASan appelle abort(), GDB intercepte le signal
ASAN_OPTIONS="abort_on_error=1" gdb ./prog
(gdb) run
# ASan détecte l'erreur et appelle abort()
# GDB intercepte SIGABRT et vous rend la main
(gdb) backtrace
(gdb) info locals# Option 2 : poser un breakpoint sur le handler d'erreur ASan
gdb ./prog
(gdb) break __asan::ReportGenericError
(gdb) run
# GDB s'arrête quand ASan détecte une erreur, AVANT l'affichage du rapport
(gdb) backtraceLa deuxième option est particulièrement puissante : le programme est en pause à l'instant exact de l'accès fautif, avec toutes les variables accessibles. Vous pouvez inspecter le pointeur fautif, le contenu de la mémoire autour, et l'état complet du programme — comme si vous aviez posé un breakpoint sur la ligne problématique.
ASan produit très peu de faux positifs — c'est l'un de ses points forts. Mais il peut signaler des erreurs dans des bibliothèques tierces que vous ne pouvez pas corriger. Dans ce cas, utilisez un fichier de suppressions :
# asan_suppressions.txt
# Format : type_erreur:pattern_fonction_ou_fichier
leak:third_party/legacy_lib.cpp
leak:libcrypto.so
LSAN_OPTIONS="suppressions=asan_suppressions.txt" ./progLes suppressions fonctionnent principalement pour les fuites mémoire (via LSan). Pour les erreurs d'accès mémoire (overflow, use-after-free), il n'y a pas de mécanisme de suppression — ces erreurs sont trop graves pour être ignorées.
Si ASan signale un faux positif dans votre propre code (ce qui est extrêmement rare), vous pouvez annoter la fonction :
// Désactiver ASan pour cette fonction spécifique
__attribute__((no_sanitize("address")))
void legacy_function() {
// Code qui fait des choses que ASan ne comprend pas
// (allocateur personnalisé, mmap direct, etc.)
}Utilisez cette annotation avec parcimonie et documentez toujours la raison.
- Lectures de mémoire non initialisée. C'est le domaine de MemorySanitizer (section 29.4.4), pas d'ASan.
- Overflows à l'intérieur d'une structure. Si un accès dépasse un champ de structure mais reste dans les limites de la structure elle-même, ASan ne le détecte pas — il n'y a pas de redzone entre les champs.
- Overflows exacts sur des allocations alignées. Si un overflow tombe exactement dans le padding d'alignement entre deux allocations, il peut échapper à la détection. En pratique, c'est rare grâce à la taille des redzones.
- Data races. ASan ne détecte pas les accès concurrents non synchronisés. C'est le domaine de ThreadSanitizer (section 29.4.3).
ASan remplace l'allocateur standard (malloc/free, new/delete) par son propre allocateur. Cela signifie que les bugs qui dépendent du comportement spécifique de l'allocateur système (fragmentation, ordre de réutilisation) peuvent se manifester différemment sous ASan. Le bug existe toujours, mais son symptôme peut changer.
ASan instrumente le code compilé avec -fsanitize=address. Les bibliothèques précompilées (installées via apt, par exemple) ne sont pas instrumentées. Un buffer overflow dans une bibliothèque non instrumentée peut échapper à la détection.
Pour une couverture maximale, compilez toutes les dépendances avec ASan. En pratique, c'est souvent trop coûteux — concentrez-vous sur votre code et les bibliothèques critiques.
À intégrer dans votre workflow de développement :
- ☐ Compilation quotidienne avec ASan + UBSan — en développement local, compilez par défaut avec
-fsanitize=address,undefined - ☐ Tests unitaires sous ASan — chaque exécution de
ctestoumake testdoit utiliser un build ASan - ☐ CI avec ASan — un job CI dédié qui compile et exécute les tests avec ASan
- ☐ Zéro erreur ASan tolérée — toute erreur ASan est un bug à corriger, jamais un "faux positif à ignorer"
- ☐ Log des rapports en CI —
ASAN_OPTIONS="log_path=..."pour archiver les rapports - ☐ Combinaison avec GDB quand le rapport ASan ne suffit pas —
abort_on_error=1+ GDB pour inspection interactive
À retenir : ASan détecte les buffer overflows (heap, stack, global), les use-after-free, les double-free, et les fuites mémoire, avec un rapport qui inclut la pile d'appels de l'accès fautif, de l'allocation, et de la libération. Le surcoût de ~2x est négligeable comparé au temps que vous perdriez à diagnostiquer ces bugs manuellement. Activez-le par défaut. Toujours.