free(): double free detected in tcache 2 en C
double free detected in tcache 2, free(): invalid pointer, munmap_chunk(): invalid pointer : d'où viennent ces messages glibc et comment corriger le free fautif.
Le programme tourne, affiche son résultat, puis meurt sur une ligne cryptique qui mentionne tcache, munmap_chunk ou invalid pointer, suivie de Aborted. Ces messages ne viennent pas de ton code : c'est l'allocateur de la bibliothèque C qui s'est rendu compte qu'on lui demandait de libérer quelque chose d'impossible.
En bref
Ces messages sont émis par la glibc (la bibliothèque C de Linux) quand un free est invalide : libérer deux fois le même bloc, libérer un pointeur décalé, ou libérer une adresse qui ne vient pas du tas. Le programme s'arrête par abort, et le shell affiche 134 pour echo $?. La glibc détecte l'erreur après coup, pas la ligne exacte : pour la trouver, lance ton programme sous Valgrind, qui pointe le free fautif par Invalid free() / delete / delete[] / realloc(). La correction est presque toujours de n'appeler free qu'une fois, sur l'adresse exacte renvoyée par malloc, calloc ou realloc ; quand le message parle de corruption, cherche une écriture hors bornes avant le free.
D'où viennent ces messages
Ils sont imprimés par le gestionnaire de mémoire de la glibc, dans malloc.c. À chaque free, cette bibliothèque fait quelques contrôles de cohérence sur ses propres structures. Quand un contrôle échoue, elle appelle une fonction interne qui imprime le message et arrête le processus : il n'y a pas de retour possible, le programme reçoit SIGABRT.
Le mot tcache revient souvent. C'est le « per-thread cache » de la glibc, un cache par thread des petits blocs récemment libérés, apparu avec la glibc 2.26 en 2017 pour accélérer les allocations courantes. Le « 2 » de tcache 2 fait partie du texte fixe du message dans le code de la glibc : il ne dépend ni de la taille du bloc ni du nombre de free, ce n'est ni un indice ni un code d'erreur.
Le shell affiche la même valeur pour tous avec echo $? : 134, soit 128 + 6, et 6 est le numéro de SIGABRT. C'est la signature d'un abort, à distinguer du 139 d'un segmentation fault.
free(): double free detected in tcache 2
Le cas le plus fréquent : le même bloc libéré deux fois.
#include <stdio.h>#include <stdlib.h>#include <string.h>
int main(void){ char *nom = malloc(16); if (nom == NULL) return 1; strcpy(nom, "grademe"); printf("%s\n", nom); free(nom); free(nom); return 0;}$ gcc -Wall -Wextra -g double.c -o doubledouble.c: In function 'main':double.c:13:5: warning: pointer 'nom' used after 'free' [-Wuse-after-free] 13 | free(nom); | ^~~~~~~~~double.c:12:5: note: call to 'free' here 12 | free(nom); | ^~~~~~~~~$ ./doublegrademefree(): double free detected in tcache 2Aborted$ echo $?134Ici GCC a vu le problème à la compilation (-Wuse-after-free), parce que les deux free sont dans la même fonction. Dès que les libérations sont éparpillées, le compilateur ne voit plus rien et le message n'apparaît qu'à l'exécution. Le réflexe qui l'évite : free(nom); nom = NULL;. Un second free(NULL) est sans effet.
free(): invalid pointer
Ce message apparaît quand tu passes à free une adresse qui n'est pas le début d'un bloc. Le classique : un pointeur qu'on a fait avancer, par exemple pour sauter des espaces, puis qu'on libère tel quel.
#include <stdio.h>#include <stdlib.h>#include <string.h>
int main(void){ char *ligne = malloc(32); if (ligne == NULL) return 1; strcpy(ligne, " bonjour"); while (*ligne == ' ') ligne++; printf("%s\n", ligne); free(ligne); return 0;}$ gcc -Wall -Wextra -g offset.c -o offset$ ./offsetbonjourfree(): invalid pointerAbortedligne ne pointe plus sur le début du bloc, il a avancé de deux cases. La glibc ne reconnaît pas cette adresse comme un début de bloc et refuse. La correction est de garder le pointeur d'origine pour le free, et d'utiliser une copie pour parcourir :
char *ligne = malloc(32);/* ... */while (*ligne == ' ') ligne++;free(ligne); /* adresse décalée */char *ligne = malloc(32);char *p = ligne;/* ... */while (*p == ' ') p++;free(ligne); /* le début du bloc */munmap_chunk(): invalid pointer
Le même bug (un pointeur décalé), mais sur un gros bloc, donne un autre message. La glibc sert les grosses allocations directement avec mmap, et les libère avec munmap. Quand l'adresse décalée tombe dans un de ces blocs, c'est munmap_chunk qui se plaint :
$ ./bigun long fichiermunmap_chunk(): invalid pointerAbortedLe programme est celui d'offset.c, avec un malloc(200000) au lieu de malloc(32) et une autre chaîne. Le diagnostic et la correction sont identiques : ne libère que l'adresse d'origine.
double free or corruption
Un double free sur un bloc trop grand pour le tcache (plus de 1 032 octets demandés sur 64 bits) mais sous le seuil de mmap, ou un bloc dont les métadonnées ont été écrasées, donne cette famille de messages ((!prev), (top), (out), (fasttop)) :
$ ./double-bigdouble free or corruption (!prev)AbortedLe suffixe entre parenthèses dit quel contrôle interne a échoué. Pour toi, le diagnostic reste le même : un bloc libéré deux fois, ou un débordement qui a corrompu les octets de service que la glibc range autour de chaque bloc. Dans ce second cas, le vrai bug est l'écriture hors bornes qui précède, pas le free.
Libérer ce qui ne vient pas du tas
free n'accepte que des adresses renvoyées par malloc, calloc ou realloc. Lui passer une variable de pile ou une chaîne littérale est un comportement indéfini. GCC le voit souvent à la compilation. À l'exécution, tout dépend des octets qui précèdent l'adresse : sur une variable de pile, la glibc répond par free(): invalid size ou munmap_chunk(): invalid pointer, ou le programme fait simplement un segfault, parfois différemment d'une exécution à l'autre. Sur une chaîne littérale, rangée en mémoire en lecture seule, ça finit en segfault :
$ gcc -Wall -Wextra -g literal.c -o literalliteral.c: In function 'main':literal.c:7:5: warning: 'free' called on a pointer to an unallocated object '"grademe"' [-Wfree-nonheap-object] 7 | free(nom); | ^~~~~~~~~$ ./literalSegmentation faultLa règle derrière tous ces cas est dans la fiche de free(3) : une adresse du tas, telle que malloc l'a renvoyée, libérée une seule fois.
Trouver la ligne avec Valgrind
Le défaut commun de ces messages, c'est qu'ils arrivent après coup : la glibc détecte l'incohérence, pas la ligne qui l'a causée. Valgrind, lui, intercepte chaque free et nomme le fichier et la ligne. Sur le double free :
$ valgrind -q ./doublegrademe==16== Invalid free() / delete / delete[] / realloc()==16== at 0x48884F8: free (vg_replace_malloc.c:989)==16== by 0x40071B: main (double.c:13)==16== Address 0x4a7f040 is 0 bytes inside a block of size 16 free'd==16== at 0x48884F8: free (vg_replace_malloc.c:989)==16== by 0x400713: main (double.c:12)==16== Block was alloc'd at==16== at 0x488545C: malloc (vg_replace_malloc.c:446)==16== by 0x4006D3: main (double.c:7)Tout est là : le free fautif (ligne 13), le free précédent du même bloc (ligne 12), et l'endroit où le bloc avait été alloué (ligne 7). Sur le pointeur décalé, Valgrind précise même de combien : Address 0x4a7f042 is 2 bytes inside a block of size 32 alloc'd. La lecture détaillée de ce genre de rapport est dans lire un rapport Valgrind.
dangling_repair
build_label renvoie une chaîne name#id, mais lue dans une mémoire qu'elle a déjà rendue. Le texte affiché semble correct alors que le code est faux : la vérification mémoire est le vrai juge. Répare la fonction pour qu'elle rende un bloc qu'elle possède encore.
free_str_array
Libère un char ** terminé par NULL sans rien oublier : chaque chaîne, puis le tableau de pointeurs, dans le bon ordre. Libérer le tableau avant les chaînes rend celles-ci introuvables, c'est précisément le genre d'erreur qui mène à une fuite ou à un free invalide.
Questions fréquentes
Que veut dire « tcache » dans le message ?
C'est le cache par thread des petits blocs libérés que la glibc tient depuis la version 2.26. Le « 2 » qui suit est écrit en dur dans le message : tu obtiens le même tcache 2 quelle que soit la taille du bloc.
Pourquoi le message arrive après l'affichage de mon résultat ?
Parce que la glibc ne vérifie la cohérence qu'au moment du free, souvent après que ton printf a déjà produit sa sortie. L'ordre des lignes à l'écran ne dit pas où est le bug ; Valgrind, lui, pointe la ligne.
134 ou 139 : quelle différence ?
134 (128 + 6) est un abort, le signal SIGABRT que la glibc déclenche quand elle refuse un free. 139 (128 + 11) est un SIGSEGV, un accès mémoire interdit. Un double free donne 134 sous glibc, mais 139 sous musl.
Comment éviter les doubles free ?
Mets le pointeur à NULL juste après l'avoir libéré : free(p); p = NULL;. Un second free(NULL) ne fait rien. Et ne libère qu'à un seul endroit du code un bloc dont la propriété est claire.
Valgrind dit « Invalid free » mais mon programme ne plante pas, est-ce grave ?
Oui. Un free invalide est un comportement indéfini : il peut passer inaperçu une fois et corrompre le tas la suivante. Le fait que le programme ait l'air de marcher ne prouve rien, corrige le free.
Sources
