Los punteros colgantes y los punteros comodín en programación informática son punteros que no apuntan a un objeto válido del tipo apropiado. Son casos especiales de violaciones de seguridad de la memoria . En términos más generales, las referencias colgantes y las referencias comodín son referencias que no se resuelven en un destino válido.
Los punteros colgantes surgen durante la destrucción de objetos , cuando un objeto que tiene una referencia entrante se elimina o se desasigna, sin modificar el valor del puntero, de modo que el puntero sigue apuntando a la ubicación de memoria de la memoria desasignada. El sistema puede reasignar la memoria previamente liberada y, si el programa luego desreferencia el puntero (ahora) colgante, puede resultar un comportamiento impredecible , ya que la memoria ahora puede contener datos completamente diferentes. Si el programa escribe en la memoria referenciada por un puntero colgante, puede resultar una corrupción silenciosa de datos no relacionados, lo que lleva a errores sutiles que pueden ser extremadamente difíciles de encontrar. Si la memoria ha sido reasignada a otro proceso, entonces intentar desreferenciar el puntero colgante puede causar fallas de segmentación (UNIX, Linux) o fallas de protección general (Windows). Si el programa tiene suficientes privilegios para permitirle sobrescribir los datos de contabilidad utilizados por el asignador de memoria del núcleo, la corrupción puede causar inestabilidades del sistema. En lenguajes orientados a objetos con recolección de basura , las referencias colgantes se evitan destruyendo únicamente los objetos que son inalcanzables, es decir, que no tienen punteros entrantes; esto se garantiza mediante el rastreo o el conteo de referencias . Sin embargo, un finalizador puede crear nuevas referencias a un objeto, lo que requiere la resurrección del objeto para evitar una referencia colgante.
Los punteros salvajes, también llamados punteros no inicializados, surgen cuando se utiliza un puntero antes de la inicialización a un estado conocido, lo que es posible en algunos lenguajes de programación. Muestran el mismo comportamiento errático que los punteros colgantes, aunque es menos probable que pasen desapercibidos porque muchos compiladores lanzarán una advertencia en tiempo de compilación si se accede a las variables declaradas antes de que se inicialicen. [1]
En muchos lenguajes (por ejemplo, el lenguaje de programación C ), eliminar un objeto de la memoria de manera explícita o destruyendo el marco de pila al regresar no altera los punteros asociados. El puntero sigue apuntando a la misma ubicación en la memoria, aunque esa ubicación ahora se pueda usar para otros fines.
A continuación se muestra un ejemplo sencillo:
{ char * dp = NULL ; /* ... */ { char c ; dp = & c ; } /* c queda fuera del alcance */ /* dp ahora es un puntero colgante */ }
Si el sistema operativo puede detectar referencias en tiempo de ejecución a punteros nulos , una solución a lo anterior es asignar 0 (nulo) a dp inmediatamente antes de salir del bloque interno. Otra solución sería garantizar de alguna manera que dp no se vuelva a utilizar sin una inicialización adicional.
Otra fuente frecuente de punteros colgantes es una combinación desordenada de llamadas a bibliotecas malloc()
y free()
: un puntero se vuelve colgante cuando se libera el bloque de memoria al que apunta. Al igual que en el ejemplo anterior, una forma de evitar esto es asegurarse de restablecer el puntero a nulo después de liberar su referencia, como se muestra a continuación.
#incluir <stdlib.h> void func () { char * dp = malloc ( A_CONST ); /* ... */ free ( dp ); /* dp ahora se convierte en un puntero colgante */ dp = NULL ; /* dp ya no está colgante */ /* ... */ }
Un error muy común es devolver direcciones de una variable local asignada a la pila: una vez que una función llamada retorna, el espacio para estas variables se desasigna y técnicamente tienen "valores basura".
int * func ( void ) { int num = 1234 ; /* ... */ return & num ; }
Los intentos de leer desde el puntero pueden devolver el valor correcto (1234) durante un tiempo después de llamar a func
, pero cualquier función llamada a partir de entonces puede sobrescribir el almacenamiento de pila asignado para con otros valores y el puntero ya no funcionará correctamente. Si se debe devolver num
un puntero a , debe tener un alcance más allá de la función; podría declararse como .num
num
static
Antoni Kreczmar Locks-and-keys .
(1945–1996) creó un sistema completo de gestión de objetos que está libre del fenómeno de referencias colgantes. [2] Fisher y LeBlanc [3] propusieron un enfoque similar bajo el nombre deLos punteros comodín se crean omitiendo la inicialización necesaria antes del primer uso. Por lo tanto, estrictamente hablando, cada puntero en lenguajes de programación que no imponen la inicialización comienza como un puntero comodín.
Esto ocurre con mayor frecuencia debido a que se omite la inicialización, no por omisión. La mayoría de los compiladores pueden advertir sobre esto.
int f ( int i ) { char * dp ; /* dp es un puntero comodín */ static char * scp ; /* scp no es un puntero comodín: * las variables estáticas se inicializan a 0 * al inicio y retienen sus valores desde * la última llamada posterior. * El uso de esta función puede considerarse de mal * estilo si no se comenta */ }
Al igual que los errores de desbordamiento de búfer , los errores de punteros colgantes o salvajes se convierten con frecuencia en agujeros de seguridad. Por ejemplo, si el puntero se utiliza para realizar una llamada a una función virtual , se puede llamar a una dirección diferente (que posiblemente apunte a un código de explotación) debido a que se sobrescribe el puntero de la tabla virtual . Alternativamente, si el puntero se utiliza para escribir en la memoria, se puede dañar alguna otra estructura de datos. Incluso si la memoria solo se lee una vez que el puntero se vuelve colgante, puede provocar fugas de información (si se colocan datos interesantes en la siguiente estructura asignada allí) o una escalada de privilegios (si la memoria ahora inválida se utiliza en controles de seguridad). Cuando se utiliza un puntero colgante después de que se haya liberado sin asignarle un nuevo trozo de memoria, esto se conoce como una vulnerabilidad de "uso después de la liberación". [4] Por ejemplo, CVE - 2014-1776 es una vulnerabilidad de uso después de la liberación en Microsoft Internet Explorer 6 a 11 [5] que está siendo utilizada por ataques de día cero por una amenaza persistente avanzada . [6]
En C, la técnica más sencilla consiste en implementar una versión alternativa de la free()
función (o similar) que garantice el restablecimiento del puntero. Sin embargo, esta técnica no borrará otras variables de puntero que puedan contener una copia del puntero.
#include <assert.h> #include <stdlib.h> /* Versión alternativa para 'free()' */ static void safefree ( void ** pp ) { /* en modo de depuración, abortar si pp es NULL */ assert ( pp ); /* free(NULL) funciona correctamente, por lo que no se requiere ninguna verificación además de la afirmación en modo de depuración */ free ( * pp ); /* desasignar fragmento, tenga en cuenta que free(NULL) es válido */ * pp = NULL ; /* restablecer puntero original */ } int f ( int i ) { char * p = NULL , * p2 ; p = malloc ( 1000 ); /* obtener un fragmento */ p2 = p ; /* copiar el puntero */ /* usar el fragmento aquí */ safefree (( void ** ) & p ); /* liberación de seguridad; no afecta a la variable p2 */ safefree (( void ** ) & p ); /* esta segunda llamada no fallará ya que p se restablece a NULL */ char c = * p2 ; /* p2 sigue siendo un puntero colgante, por lo que este es un comportamiento indefinido. */ return i + c ; }
La versión alternativa se puede utilizar incluso para garantizar la validez de un puntero vacío antes de llamar malloc()
:
safefree ( & p ); /* No estoy seguro si el fragmento ha sido liberado */ p = malloc ( 1000 ); /* asignar ahora */
Estos usos se pueden enmascarar mediante #define
directivas para construir macros útiles (una de las más comunes es #define XFREE(ptr) safefree((void **)&(ptr))
), creando algo así como un metalenguaje, o se pueden incrustar en una biblioteca de herramientas aparte. En todos los casos, los programadores que utilicen esta técnica deberían utilizar las versiones seguras en cada caso en el que free()
se utilicen; si no lo hacen, se vuelve a producir el problema. Además, esta solución está limitada al alcance de un único programa o proyecto y debería estar debidamente documentada.
Entre las soluciones más estructuradas, una técnica popular para evitar punteros colgantes en C++ es usar punteros inteligentes . Un puntero inteligente generalmente usa el conteo de referencias para recuperar objetos. Otras técnicas incluyen el método de las lápidas y el método de las cerraduras y las llaves . [3]
Otro enfoque es utilizar el recolector de basura Boehm , un recolector de basura conservador que reemplaza las funciones de asignación de memoria estándar en C y C++ con un recolector de basura. Este enfoque elimina por completo los errores de punteros colgados al deshabilitar las liberaciones y recuperar objetos mediante la recolección de basura.
En lenguajes como Java, no pueden aparecer punteros colgantes porque no existe ningún mecanismo para desasignar memoria explícitamente. En cambio, el recolector de elementos no utilizados puede desasignar memoria, pero solo cuando el objeto ya no es accesible desde ninguna referencia.
En el lenguaje Rust , el sistema de tipos se ha ampliado para incluir también las variables lifetimes y resource acquisition and initialization . A menos que se deshabiliten las funciones del lenguaje, los punteros colgantes se detectarán en tiempo de compilación y se informarán como errores de programación.
Para exponer los errores de punteros colgantes, una técnica de programación común es establecer punteros al puntero nulo o a una dirección no válida una vez que se haya liberado el almacenamiento al que apuntan. Cuando se desreferencia el puntero nulo (en la mayoría de los lenguajes), el programa terminará inmediatamente; no hay posibilidad de corrupción de datos o comportamiento impredecible. Esto hace que el error de programación subyacente sea más fácil de encontrar y resolver. Esta técnica no ayuda cuando hay múltiples copias del puntero.
Algunos depuradores sobrescribirán y destruirán automáticamente los datos que se han liberado, normalmente con un patrón específico, como 0xDEADBEEF
(el depurador Visual C/C++ de Microsoft, por ejemplo, utiliza 0xCC
o dependiendo de lo que se haya liberado [7] ). Esto normalmente evita que los datos se vuelvan a utilizar al hacerlos inútiles y también muy prominentes (el patrón sirve para mostrar al programador que la memoria ya se ha liberado).0xCD
0xDD
También se pueden utilizar herramientas como Polyspace , TotalView , Valgrind , Mudflap, [8] AddressSanitizer o herramientas basadas en LLVM [9] para detectar usos de punteros colgantes.
Otras herramientas (SoftBound, Insure++ y CheckPointer) instrumentan el código fuente para recopilar y rastrear valores legítimos de punteros ("metadatos") y verifican cada acceso de puntero contra los metadatos para verificar su validez.
Otra estrategia, cuando se sospecha de un conjunto pequeño de clases, es hacer que todas sus funciones miembro sean virtuales temporalmente : después de que la instancia de la clase haya sido destruida/liberada, su puntero a la Tabla de métodos virtuales se establece en NULL
, y cualquier llamada a una función miembro bloqueará el programa y mostrará el código culpable en el depurador.
{{cite web}}
: |author2=
tiene nombre genérico ( ayuda )