Débogage avancé/Travail pratique/Double libération
But de ce TP
[modifier | modifier le wikicode]Il est compliqué de suivre dans l'exécution d'un programme si une zone de mémoire allouée dynamiquement dans le tas (avec malloc) est encore utilisée. Mais il faut pourtant bien la libérer un jour pour pouvoir la recycler et ne pas exploser en utilisation de la mémoire. La libération est donc sujette à deux bugs courants: libérer trop tôt une zone encore utilisée, et la libérer deux fois dans deux endroits différents du code. Ce TP tourne autour de ce deuxième bug.
Les langages interprétés (Python, Perl, Ruby, Julia, etc.) et certains langages compilés (Java, Go, Haskell, etc.) font cette libération en exécutant un ramasse-miettes, c'est-à-dire du code dédié à la détection de ces libérations. D'autres méthodes existent comme le compteur de référence d'Objective-C ou des règles strictes d'emprunt d'un pointeur, en Rust (Elles sont vérifiées et validés à la compilation).
Le code à déboguer
[modifier | modifier le wikicode]Créer un fichier bug_doublefree.c contenant le code suivant.
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
const unsigned int SIZE = 100;
// Double libération dans et après fibon
// NB : argument de type C11 plus précis que uint *p
void fibon(const unsigned int size, unsigned int p[static size]) {
for (unsigned int i = 0; i < size; i++)
if (i < 2)
p[i] = i;
else
p[i] = p[i - 1] + p[i - 2];
free(p); // Première libération
}
int main() {
assert(SIZE > 2);
unsigned int *p = malloc(sizeof(int[SIZE]));
assert(p != NULL);
fibon(SIZE, p);
free(p); // Seconde libération
return EXIT_SUCCESS;
}
Les consignes
[modifier | modifier le wikicode]Le code bug_doublefree.c
libère deux fois le tableau
alloué, une première fois juste avant la fin de la fonction
fibon()
et une fois juste après.
- Compiler le code. L'analyseur statique de votre compilateur détecte le bug.
- Lancer le programme
bug_doublefree
. C'est votre bibliothèque C qui détecte le problème à l'exécution au moment du second free. Mais elle n'indique pas où était le premier free. - Lancer le programme avec valgrind. Il détecte également le problème. Valgrind donne les différentes lignes du programme ayant faite l'allocation et les deux libérations. Valgrind donne aussi en toute fin un résumé de ce qu'il a observé (1 allocation et deux libérations).
- Recompiler en utilisant AddressSanitizer et exécuter le programme. Cette fois la détection interne de ASan (et pas de la librairie C) est capable d'indiquer le code faisant la première libération et celui ayant fait l'allocation.
Vous devriez avoir tapé les commandes suivantes (Certains affichages de Valgrind sont omis):
$ gcc -o bug_doublefree bug_doublefree.c -g -Wall -Wextra -fanalyzer
[... l'analyseur statique détecte le bug ...]
$ ./bug_doublefree
free(): double free detected in tcache 2
Abandon
$ valgrind ./bug_doublefree
[...]
==31719== Invalid free() / delete / delete[] / realloc()
==31719== at 0x484217B: free (vg_replace_malloc.c:872)
==31719== by 0x109291: main (bug_doublefree.c:24)
==31719== Address 0x4a8f040 is 0 bytes inside a block of size 400 free'd
==31719== at 0x484217B: free (vg_replace_malloc.c:872)
==31719== by 0x1091EF: fibon (bug_doublefree.c:13)
==31719== by 0x109285: main (bug_doublefree.c:22)
==31719== Block was alloc'd at
==31719== at 0x483F7B5: malloc (vg_replace_malloc.c:381)
==31719== by 0x10923F: main (bug_doublefree.c:19)
== 31719 ==
==31719== HEAP SUMMARY:
==31719== in use at exit: 0 bytes in 0 blocks
==31719== total heap usage: 1 allocs, 2 frees, 400 bytes allocated
== 31719 ==
==31719== All heap blocks were freed -- no leaks are possible
== 31719 ==
==31719== For lists of detected and suppressed errors, rerun with: -s
==31719== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
$ gcc -o bug_doublefree bug_doublefree.c -g -Wall -Wextra -fanalyzer -fsanitize=address
[... détection complète du bug par ASan ...]
[... description de la seconde libération ...]
[... description de la première libération ...]
[... description de l'allocation ...]