
En programmation, les pointeurs orphelins et les pointeurs non valides sont des pointeurs qui ne pointent pas vers un objet valide du type approprié. Il s'agit de cas particuliers de violations de la sécurité mémoire . Plus généralement, les références orphelines et les références non valides sont des références qui ne mènent pas à une destination valide.
Les pointeurs non valides apparaissent lors de la destruction d'un objet , lorsqu'un objet pointé par un pointeur donné est supprimé ou désalloué sans que la valeur dudit pointeur soit modifiée. Le pointeur pointe alors toujours vers l'emplacement mémoire désalloué. Le système peut réallouer la mémoire précédemment libérée, et si le programme déréférence ensuite le pointeur (désormais) non valide, un comportement imprévisible peut se produire , car la mémoire peut maintenant contenir des données complètement différentes. Si le programme écrit dans la mémoire référencée par un pointeur non valide, une corruption silencieuse de données sans rapport peut survenir, entraînant des bogues subtils extrêmement difficiles à détecter. Si la mémoire a été réallouée à un autre processus, toute tentative de déréférencement du pointeur non valide peut provoquer des erreurs de segmentation (UNIX, Linux) ou des erreurs de protection générale (Windows). Si le programme dispose des privilèges suffisants pour écraser les données de gestion de la mémoire utilisées par l'allocateur du noyau, la corruption peut engendrer des instabilités du système. Dans les langages orientés objet avec ramasse-miettes , les références orphelines sont évitées en ne détruisant que les objets inaccessibles, c'est-à-dire ceux qui ne possèdent aucun pointeur entrant ; ceci est garanti par le traçage ou le comptage des références . Cependant, un finaliseur peut créer de nouvelles références à un objet, ce qui nécessite la résurrection de l'objet pour éviter une référence orpheline.
Les pointeurs sauvages, également appelés pointeurs non initialisés, apparaissent lorsqu'un pointeur est utilisé avant son initialisation à un état connu, ce qui est possible dans certains langages de programmation. Ils présentent le même comportement erratique que les pointeurs orphelins, mais ils ont moins de chances de passer inaperçus car de nombreux compilateurs émettent un avertissement à la compilation si des variables déclarées sont accédées avant leur initialisation.
Cause des aiguilles qui pendent
Dans de nombreux langages (par exemple, le langage C ), la suppression explicite d'un objet de la mémoire ou la destruction de son cadre de pile lors du retour d'une instruction ne modifie pas les pointeurs associés. Le pointeur continue de pointer vers le même emplacement mémoire, même si cet emplacement peut désormais être utilisé à d'autres fins.
Un exemple simple est présenté ci-dessous :
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 immédiatement avant la sortie du bloc interne. Une autre solution serait de garantir que dp ne soit pas réutilisé sans une nouvelle initialisation.
Une autre cause fréquente de pointeurs non initialisés est une combinaison confuse d' appels de fonctions malloc()et free()de bibliothèques : un pointeur devient non initialisé lorsque le bloc de mémoire qu'il pointe est libéré. Comme dans l'exemple précédent, une façon d'éviter ce problème est de s'assurer de réinitialiser le pointeur à null après avoir libéré sa référence, comme illustré ci-dessous.
Une erreur bien 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 est libéré et, techniquement, elles contiennent des « valeurs indéfinies ».
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 ultérieurement risque d'écraser l'espace mémoire alloué à ce numpointeur avec d'autres valeurs, et celui-ci ne fonctionnera plus correctement. Si un pointeur vers ce pointeur numdoit être renvoyé, numsa portée doit dépasser celle de la fonction ; il peut par exemple être déclaré comme tel static.
Désaffectation manuelle sans référence suspendue
Cause des pointeurs sauvages
Les pointeurs non initialisés sont créés en omettant l'initialisation nécessaire avant leur première utilisation. Ainsi, à proprement parler, tout pointeur dans les langages de programmation qui n'imposent pas d'initialisation est initialement un pointeur non initialisé.
Cela se produit le plus souvent lorsqu'on saute l'initialisation, et non lorsqu'on l'omet. La plupart des compilateurs sont capables de signaler ce problème.
Failles de sécurité liées aux pointeurs non connectés
2014-1776 est une vulnérabilité d'utilisation après libération présente dans Microsoft Internet Explorer 6 à 11 et exploitée par des attaques zero-day d'une menace persistante avancée .
Éviter les erreurs de pointeur orphelin
En C, la technique la plus simple consiste à implémenter une version alternative de la free()fonction (ou d'une fonction similaire) qui garantit la réinitialisation du pointeur. Cependant, cette technique ne supprime pas les autres variables de pointeur qui pourraient contenir une copie du pointeur.
La version alternative peut même être utilisée pour garantir la validité d'un pointeur vide avant l'appelmalloc() :
Ces usages peuvent être masqués par #definedes directives permettant de construire des macros utiles (une macro courante étant `__init__` #define XFREE(ptr) safeFree((void**)&(ptr))), créant ainsi une sorte de métalangage, ou 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 `__init__` à chaque free()utilisation de `__init__` ; tout manquement à cette règle engendre à nouveau le problème. 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 orphelins 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 incluent 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 complètement les erreurs de pointeurs non initialisés en désactivant les libérations de mémoire et en récupérant les objets par le biais du 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 pour effectuer ces vérifications supplémentaires.
Dans les langages comme Java, les pointeurs non initialisés sont impossibles 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 pour inclure la durée de vie des variables et l'initialisation des ressources . Sauf si l'on désactive ces fonctionnalités, les pointeurs non initialisés sont détectés à la compilation et signalés comme des erreurs de programmation.
Détection de pointeur orphelin
Pour détecter les erreurs de pointeurs invalides, une technique courante consiste à affecter aux pointeurs une valeur nulle ou une adresse invalide une fois la mémoire pointée libérée. Lorsque le pointeur nul est déréférencé (dans la plupart des langages), le programme s'arrête immédiatement, éliminant ainsi tout risque de corruption de données ou de comportement imprévisible. L'erreur de programmation sous-jacente est alors plus facile à identifier et à corriger. Cette technique est cependant inefficace en présence de plusieurs copies du pointeur.
Certains débogueurs écrasent et détruisent automatiquement les données libérées, généralement selon un motif spécifique 0xDEADBEEF(par exemple, le débogueur Visual C/C++ de Microsoft utilise `libre` ou `libre` 0xCC, 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).0xCD0xDD
Des outils tels que Polyspace , TotalView , Valgrind , Mudflap, AddressSanitizer ou des outils basés sur LLVM peuvent également être utilisés pour détecter les utilisations de pointeurs orphelins.
D'autres outils ( SoftBound , Insure++ et CheckPointer ) instrumentent le code source pour collecter et suivre les valeurs légitimes des pointeurs (« métadonnées ») et vérifier la validité de chaque accès au 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 membres virtuelles : une fois l'instance de classe détruite/libérée, son pointeur vers la table des méthodes virtuelles est défini sur ` NULLtrue`, et tout appel à une fonction membre provoquera le plantage du programme et affichera le code coupable dans le débogueur.
L' extension de marquage de mémoire ARM64 (MTE) - désactivée par défaut sur les systèmes Linux, mais activable 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 .