Dangling pointer

Dangling pointer (traduction littérale : « pointeur pendouillant » ou « pointeur sautillant ») est un terme anglais de programmation informatique désignant un pointeur qui ne pointe pas vers un type d'objet approprié.
Les dangling pointers sont créés lors de la destruction de l'objet.
Cause de pointeurs pendouillants
[modifier | modifier le code]Dans de nombreux langages (par exemple, le langage C ), la suppression explicite d'un objet de la mémoire ou la destruction de son bloc d'activation lors du retour d'une instruction ne modifie pas les pointeurs associés. Dans ce cas, le pointeur continue de pointer vers le même emplacement mémoire, alors que cet emplacement peut désormais être utilisé à d'autres fins.
Un exemple simple est présenté ci-dessous :
{
char* dp = NULL;
// ...
{
char c;
dp = &c;
}
// c sort de la portée
// dp est désormais un pointeur pendouillant
}
Si le système d'exploitation est capable de détecter les références à des pointeurs nuls lors de l'exécution, une solution consiste à affecter la valeur 0 (NULL) à dp juste avant la sortie du bloc interne. Une autre solution serait de garantir d'une façon ou d'une autre que dp ne soit pas réutilisé sans une nouvelle initialisation.
Une autre cause fréquente de pointeurs pendouillant est une combinaison erronée d'appels aux fonctions malloc() et free() : un pointeur devient pendouillant si le bloc de mémoire qu'il pointe est libéré. Comme dans l'exemple précédent, une solution consiste à réinitialiser le pointeur à NULL après avoir libéré sa référence, comme illustré ci-dessous.
#include <stdlib.h>
void func() {
char* dp = (char*)malloc(sizeof(char) * 10);
// ...
free(dp); // dp devient pendouillant à cet instant
dp = NULL; // dp cesse d'être pendouillant
// ...
}
Une erreur trop fréquente consiste à renvoyer l'adresse d'une variable locale allouée sur la pile : une fois qu'une fonction appelée a renvoyé une valeur, l'espace alloué à ces variables sur la pile est libéré et, techniquement, elles contiennent des valeurs indéfinies.
int* func(void) {
int num = 1234;
// ...
return #
}
Les tentatives de lecture à partir du pointeur peuvent encore renvoyer la valeur correcte (1234) pendant un certain temps après l'appel func, mais toute fonction appelée plus tard risque d'écraser avec d'autres valeurs l'espace mémoire qui était alloué à num sur la pile , et le pointeur ne fonctionnera alors plus correctement. Si un pointeur vers num doit être renvoyé, num doit avoir une portée au-delà de la fonction ; il peut, par exemple, être déclaré comme static .
Antoni Kreczmar (pl) (1945–1996) has created a complete object management system which is free of dangling reference phenomenon. A similar approach was proposed by Fisher and LeBlanc under the name Locks-and-keys.
Causes de pointeurs non initialisés
[modifier | modifier le code]Les pointeurs non initialisés sont créés quand l'initialisation nécessaire avant leur première utilisation est omise. Ainsi, à proprement parler, tout pointeur dans les langages de programmation qui n'imposent pas d'initialisation est au départ un pointeur non initialisé.
Cela se produit le plus souvent lorsqu'on saute l'initialisation et non pas lorsqu'on l'omet. La plupart des compilateurs sont capables d'avertir au sujet de ce problème.
int f(int i) {
char* dp; // dp est un pointeur non initialisé
static char* scp; /* scp n'est pas un pointeur non initialisé:
* Les variables static sont initialisées à 0
* au début, et conservent leurs valeurs du
* dernier appel ensuite.
* Cependant, utiliser cette fonctionnalité peut
* être considéré comme une mauvaise pratique
* s'il n'y a pas de commentaire*/
}
Failles de sécurité impliquant des pointeurs pendouillants
[modifier | modifier le code]Tout comme les failles de dépassement de tampon, les failles liées aux pointeurs pendouillants/invalides ou non initialisés constituent fréquemment des failles de sécurité. Par exemple, si le pointeur est utilisé pour appeler une fonction virtuelle, une autre adresse (qui peut pointer vers du code d'exploitation) peut être appelée, suite à l'écrasement du pointeur de la table virtuelle . De plus, si le pointeur est utilisé pour écrire en mémoire, une autre structure de données peut être corrompue. Même si la mémoire n'est lue qu'une fois que le pointeur est devenu invalide, cela peut entraîner des fuites d'informations (si des données sensibles sont placées dans la structure suivante allouée à cet emplacement) ou à une élévation de privilèges (si la mémoire désormais invalide est utilisée lors de contrôles de sécurité). Lorsqu'un pointeur invalide est utilisé après avoir été libéré sans qu'un nouveau bloc de mémoire lui ait été alloué, on parle de vulnérabilité « use after free »[1]. Par exemple, CVE 2014-1776 est une vulnérabilité de type « use after free » sur Microsoft Internet Explorer 6 à 11 qui était utilisée dans des attaques zero-day par une Advanced Persistent Threat (menace persistente avancée).
Eviter les erreurs de pointeur pendouillant
[modifier | modifier le code]
En C, la technique la plus simple est d'implémenter une version alternative de la fonction free() (ou équivalente) qui garantit la réinitialisation du pointeur. Cependant, cette technique ne libère pas les autres variables de type pointeur qui peuvent contenir une copie de ce pointeur.
#include <assert.h>
#include <stdlib.h>
// Version sécurisée de free()
static void safeFree(void** pp) {
// pour débuguer, abandonner si pp est NULL
assert(pp);
// free(NULL) fonctionne comme il faut, donc aucune vérification n'est nécessaire en plus du assert
free(*pp); // désallouer
*pp = NULL; // réinitialiser
}
int f(int i) {
char* p = NULL;
char* p2;
p = (char*)malloc(1000); // obtenir un emplacement mémoire
p2 = p; // copier le pointeur
// ici, utiliser l'emplacement mémoire puis ensuite :
safeFree((void**)&p); // free sécurisé ; n'affecte pas p2
safeFree((void**)&p); // le réappeler ne pose pas de problème comme p est à NULL
char c = *p2; // p2 est en revanche toujours pendouillant, donc ce n'est pas valide.
return i + c;
}
La version alternative peut même être utilisée pour garantir la validité d'un pointeur vide avant d'appeler malloc() :
safeFree(&p); // Si je ne suis pas sûr.e que l'emplacement ait été libéré, maintenant c'est bon
p = (char*)malloc(1000); // maintenant on peut allouer
Ces usages peuvent être masqués par des directives #define pour construire des macros utiles (par exemple : #define XFREE(ptr) safeFree((void**)&(ptr)) ), créant ainsi une sorte de métalangage, ou encore intégrés à une bibliothèque d'outils distincte. Dans tous les cas, les programmeurs utilisant cette technique doivent impérativement utiliser les versions sécurisées de free() ; sinon, le problème est toujours là. De plus, cette solution est limitée au cadre d'un seul programme ou projet, et doit être correctement documentée.
Parmi les solutions plus structurées, une technique courante pour éviter les pointeurs pendouillant en C++ consiste à utiliser des pointeurs intelligents . Un pointeur intelligent utilise généralement le comptage de références pour récupérer les objets. D'autres techniques existent, comme la méthode des pierres tombales et la méthode des clés et des verrous .
Une autre approche consiste à utiliser le ramasse-miettes Boehm, un ramasse-miettes conservateur qui remplace les fonctions d'allocation mémoire standard en C et C++ par un ramasse-miettes. Cette approche élimine toutes les erreurs de pointeurs non initialisés en désactivant les libérations de mémoire et en récupérant les objets via le ramasse-miettes.
Une autre approche consiste à utiliser un système tel que CHERI, qui stocke les pointeurs avec des métadonnées supplémentaires susceptibles d'empêcher les accès invalides en incluant des informations sur leur durée de vie. CHERI nécessite généralement une prise en charge par le processeur afin d'effectuer ces vérifications supplémentaires.
Dans les langages comme Java, les pointeurs non initialisés ne peuvent pas car il n'existe aucun mécanisme pour libérer explicitement la mémoire. C'est le ramasse-miettes qui peut libérer la mémoire, mais seulement lorsque l'objet n'est plus accessible par aucune référence.
En Rust, le système de types a été étendu dans le but d'inclure la durée de vie des variables et l'initialisation des ressources . À moins de désactiver ces fonctionnalités, les pointeurs non initialisés sont détectés à la compilation et signalés comme des erreurs de programmation.
Libération manuelle de mémoire sans référence pendouillante
[modifier | modifier le code]Antoni Kreczmar (pl) (1945–1996) a créé un système de gestion d'objets complet, libre de tout phénomène de référence pendouillante[2]. Fisher et Leblanc[3] ont proposé une approche similaire, appelée Locks-and-keys (Verrous et clés en français).
Détection de pointeurs pendouillants
[modifier | modifier le code]Pour détecter les erreurs de pointeurs pendouillants, une technique courante consiste à affecter une valeur nulle au pointeur, ou bien une adresse invalide une fois que ce pointeur est libéré. Dans la plupart des langages, quand le pointeur nul est déréférencé, le programme s'arrête immédiatement, éliminant ainsi tout risque de corruption des données, ou de comportement imprévisible. Cela facilite la détection et la résolution des potentielles erreurs de programmation. Cette technique est inefficace lorsqu'il y a plusieurs copies du pointeur.
Certains débogueurs suppriment automatiquement les données libérées, généralement à l'aide d'un motif spécifique, tel que 0xDEADBEEF (le débogueur Visual C/C++ de Microsoft, par exemple, utilise 0xCC, 0xCD ou 0xDD selon les données libérées ). Ceci empêche généralement la réutilisation des données en les rendant inutilisables et en les signalant clairement (le motif indique au programmeur que la mémoire a déjà été libérée).
Des outils tels que Polyspace, TotalView, Valgrind, Mudflap, AddressSanitizer, ou des outils basés sur LLVM[4] peuvent également être utilisés pour détecter les utilisations de pointeurs pendouillants.
D'autres outils (SoftBound, Insure++ et CheckPointer) instrumentent le code source pour collecter et suivre les valeurs légitimes des pointeurs (les « métadonnées »), et vérifier la validité de chaque accès du pointeur par rapport aux métadonnées.
Une autre stratégie, lorsqu'on soupçonne un petit ensemble de classes, consiste à rendre temporairement toutes leurs fonctions virtuelles : une fois l'instance de classe détruite ou libérée, son pointeur vers la table des méthodes virtuelles est défini sur NULL, et tout appel à une fonction membre de la classe fera planter le programme, et affichera le code concerné dans le débogueur.
L'extension de balisage de la mémoire ARM64 (MTE, pour memory tagging extension en anglais), désactivée par défaut sur les systèmes Linux, mais pouvant être activée sur Android 16, déclenche une erreur de segmentation lorsqu'elle détecte une utilisation après libération et un dépassement de tampon[5],[6].
Notes et références
[modifier | modifier le code]- ↑ Dalci, anonymous author et CWE Content Team, « CWE-416: Use After Free », Common Weakness Enumeration, Mitre Corporation, (consulté le )
- ↑ (en) Gianna Cioni et Antoni Kreczmar, Programmed deallocation without dangling reference, Information Processing Letters, vol. 18, , p. 179–185
- ↑ (en) C. N. Fisher et R. J. Leblanc, The implementation of run-time diagnostics in Pascal, IEEE Transactions on Software Engineering, , p. 313–319
- ↑ (en) Dinakar Dhurjati et Vikram Adve, Efficiently Detecting All Dangling Pointer Uses in Production Servers (lire en ligne)
- ↑ (en) « Arm memory tagging extension », Android Open Source Project (consulté le )
- ↑ (en) Goodin, « Google introduces Advanced Protection mode for its most at-risk Android users », Ars Technica, (consulté le )
Liens externes
[modifier | modifier le code]