Articulo de referencia

Puntero colgante

Puntero colgante En programación informática, los punteros colgantes y los punteros comodín son punteros que no apuntan a un objeto válido del tipo apropiado. Estos son casos es...

Puntero colgante

En programación informática, los punteros colgantes y los punteros comodín son punteros que no apuntan a un objeto válido del tipo apropiado. Estos son casos especiales de violaciones de seguridad de 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 al que apunta un puntero determinado se elimina o se desasigna sin modificar el valor de dicho puntero, de modo que este sigue apuntando a la ubicación de memoria desasignada. El sistema puede reasignar la memoria previamente liberada, y si el programa desreferencia el puntero (ahora) colgante, puede producirse un comportamiento impredecible , ya que la memoria puede contener datos completamente diferentes. Si el programa escribe en la memoria a la que apunta un puntero colgante, puede producirse una corrupción silenciosa de datos no relacionados, lo que da lugar a errores sutiles que pueden ser extremadamente difíciles de detectar. Si la memoria se ha reasignado a otro proceso, intentar desreferenciar el puntero colgante puede provocar fallos de segmentación (UNIX, Linux) o fallos de protección general (Windows). Si el programa tiene privilegios suficientes para sobrescribir los datos de contabilidad utilizados por el asignador de memoria del kernel, la corrupción puede provocar inestabilidades en el sistema. En los lenguajes orientados a objetos con recolección de basura , las referencias colgantes se evitan destruyendo únicamente los objetos inaccesibles, es decir, aquellos que no reciben ningún puntero entrante; 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 su inicialización a un estado conocido, lo cual es posible en algunos lenguajes de programación. Presentan el mismo comportamiento errático que los punteros colgantes, aunque es menos probable que pasen desapercibidos, ya que muchos compiladores generan una advertencia en tiempo de compilación si se accede a las variables declaradas antes de su inicialización. [ 1 ]

Causa de punteros colgantes

En muchos lenguajes de programación (por ejemplo, el lenguaje C ), eliminar un objeto de la memoria explícitamente o destruir 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 pueda utilizarse para otros fines.

A continuación se muestra un ejemplo sencillo:

{ char * dp = NULL ; // ... { char c ; dp = & c ; } // c queda fuera del ámbito // dp ahora es un puntero colgante }

Si el sistema operativo puede detectar referencias en tiempo de ejecución a punteros nulos , una solución consiste en 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 usar sin una inicialización adicional.

Otra causa frecuente de punteros colgantes es una combinación desordenada de llamadas malloc()a free()bibliotecas: 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.

#include <stdlib.h>void func () { char * dp = ( char * ) malloc ( sizeof ( char ) * 10 ); // ... free ( dp ); // dp ahora se convierte en un puntero colgante dp = NULL ; // dp ya no es colgante // ... }

Un error demasiado común es devolver direcciones de variables locales asignadas en la pila: una vez que una función llamada regresa, el espacio para estas variables se libera y, técnicamente, tienen "valores basura".

int * func ( void ) { int num = 1234 ; // ... return & num ; }

Los intentos de leer desde el puntero aún pueden devolver el valor correcto (1234) durante un tiempo después de llamar a func, pero cualquier función llamada posteriormente puede sobrescribir el almacenamiento de pila asignado para numcon otros valores y el puntero dejaría de funcionar correctamente. Si numse debe devolver un puntero a , numdebe tener un ámbito más allá de la función; podría declararse como static.

Desasignación manual sin referencia colgante

Antoni Kreczmar (1945–1996) creó un sistema completo de gestión de objetos que está libre del fenómeno de referencias colgantes. [ 2 ] Un enfoque similar fue propuesto por Fisher y LeBlanc [ 3 ] bajo el nombre de Locks-and-keys .

Causa de punteros salvajes

Los punteros salvajes se crean al omitir la inicialización necesaria antes de su primer uso. Por lo tanto, estrictamente hablando, todo puntero en los lenguajes de programación que no exigen inicialización comienza como un puntero salvaje.

Esto suele ocurrir al saltarse la inicialización, no al omitirla. 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 conservan sus valores de  * la última llamada posterior.  * El uso de esta característica puede considerarse un mal  * estilo si no se comenta */ }

Agujeros de seguridad relacionados con punteros colgantes

Al igual que los errores de desbordamiento de búfer , los errores de puntero colgante/salvaje frecuentemente se convierten en agujeros de seguridad. Por ejemplo, si el puntero se usa para hacer una llamada a una función virtual , se puede llamar a una dirección diferente (posiblemente apuntando a código de explotación) debido a que el puntero de la tabla virtual se sobrescribe. Alternativamente, si el puntero se usa para escribir en la memoria, alguna otra estructura de datos puede corromperse. 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 escalada de privilegios (si la memoria ahora inválida se usa en comprobaciones de seguridad). Cuando se usa un puntero colgante después de que se ha liberado sin asignarle un nuevo bloque 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 fue utilizada por ataques de día cero por una amenaza persistente avanzada . [ 6 ]

Cómo evitar errores de puntero colgante

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 reinicio 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 segura de free() static void safeFree ( void ** pp ) { // en modo depuración, abortar si pp es NULL assert ( pp ); // free(NULL) funciona correctamente, por lo que no se requiere ninguna comprobación aparte de la assert en modo depuración free ( * pp ); // liberar el bloque, tenga en cuenta que free(NULL) es válido * pp = NULL ; // restablecer el puntero original }int f ( int i ) { char * p = NULL ; char * p2 ; p = ( char * ) malloc ( 1000 ); // obtener un fragmento p2 = p ; // copiar el puntero // usar el fragmento aquí safeFree (( void ** ) & p ); // liberación segura; 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 puede utilizarse incluso para garantizar la validez de un puntero vacío antes de llamar a malloc():

safeFree ( & p ); // No estoy seguro de si el fragmento ha sido liberado */ p = ( char * ) malloc ( 1000 ); // asignar ahora

Estos usos pueden ocultarse mediante #definedirectivas para construir macros útiles (una común es #define XFREE(ptr) safeFree((void**)&(ptr))), creando algo parecido a un metalenguaje o pueden integrarse en una biblioteca de herramientas aparte. En todos los casos, los programadores que utilicen esta técnica deben usar las versiones seguras en cada instancia donde free()se usaría ; no hacerlo conduce nuevamente al problema. Además, esta solución se limita al ámbito de un solo programa o proyecto, y debe estar debidamente documentada.

Entre las soluciones más estructuradas, una técnica popular para evitar punteros colgantes en C++ es el uso de punteros inteligentes . Un puntero inteligente normalmente utiliza el conteo de referencias para recuperar objetos. Otras técnicas incluyen el método de marcadores de posición y el método de llaves y candados . [ 3 ]

Otro enfoque consiste en utilizar el recolector de basura Boehm , un recolector de basura conservador que reemplaza las funciones estándar de asignación de memoria en C y C++ con un recolector de basura. Este enfoque elimina por completo los errores de punteros colgantes al deshabilitar las liberaciones y recuperar los objetos mediante la recolección de basura.

Otro enfoque consiste en utilizar un sistema como CHERI , que almacena punteros con metadatos adicionales que pueden prevenir accesos no válidos al incluir información sobre el tiempo de vida de los punteros. CHERI generalmente requiere soporte de la CPU para realizar estas comprobaciones adicionales.

En lenguajes como Java, no se producen punteros colgantes porque no existe un mecanismo para liberar explícitamente la memoria. En cambio, el recolector de basura puede liberar la 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 la duración de las variables y la adquisición de recursos durante la inicialización . A menos que se desactiven estas características del lenguaje, los punteros colgantes se detectarán en tiempo de compilación y se reportarán como errores de programación.

Detección de puntero colgante

Para detectar errores de punteros colgantes, una técnica de programación común consiste en asignar punteros al puntero nulo o a una dirección no válida una vez que se haya liberado la memoria a la que apuntan. Al desreferenciar el puntero nulo (en la mayoría de los lenguajes), el programa finaliza inmediatamente, sin riesgo de corrupción de datos ni comportamiento impredecible. Esto facilita la detección y resolución del error de programación subyacente. Sin embargo, esta técnica no es útil cuando existen varias copias del puntero.

Algunos depuradores sobrescriben y destruyen automáticamente los datos liberados, generalmente con un patrón específico, como 0xDEADBEEF(el depurador Visual C/C++ de Microsoft, por ejemplo, usa 0xCC, 0xCDo 0xDDdependiendo de lo que se haya liberado [ 7 ] ). Esto suele impedir que los datos se reutilicen al hacerlos inútiles y muy visibles (el patrón sirve para mostrar al programador que la memoria ya se ha liberado).

También se pueden utilizar herramientas como Polyspace , TotalView , Valgrind , Mudflap, [ 8 ] AddressSanitizer o herramientas basadas en LLVM [ 9 ] para detectar el uso de punteros colgantes.

Otras herramientas ( SoftBound , Insure++ y CheckPointer ) instrumentan el código fuente para recopilar y rastrear valores legítimos para los punteros ("metadatos") y comprueban la validez de cada acceso a un puntero comparándolo con los metadatos.

Otra estrategia, cuando se sospecha de un pequeño conjunto de clases, es hacer que todas sus funciones miembro sean virtuales temporalmente : después de que la instancia de la clase se haya destruido/liberado, su puntero a la Tabla de Métodos Virtuales se establece en NULL, y cualquier llamada a una función miembro hará que el programa falle y mostrará el código culpable en el depurador.

La extensión de etiquetado de memoria ARM64 (MTE), deshabilitada por defecto en sistemas Linux, pero que puede habilitarse en Android 16 , provoca un fallo de segmentación cuando detecta uso de memoria liberada y desbordamiento de búfer . [ 10 ] [ 11 ]

Véase también

Referencias

  1. "Opciones de advertencia: uso de la colección de compiladores GNU (GCC)" .
  2. Gianna Cioni, Antoni Kreczmar, Desasignación programada sin referencia colgante , Information Processing Letters , vol. 18, 1984 , págs. 179–185
  3. 1 2 C. N. Fisher, RJ Leblanc, La implementación de diagnósticos en tiempo de ejecución en Pascal , IEEE Transactions on Software Engineering , 6(4):313–319, 1980.
  4. Dalci, Eric; autor anónimo; Equipo de contenido de CWE (11 de mayo de 2012). "CWE-416: Uso después de liberación" . Enumeración de debilidades comunes . Mitre Corporation . Recuperado el 28 de abril de 2014 .{{cite web}}: |author2=tiene nombre genérico ( ayuda )
  5. "CVE-2014-1776" . Vulnerabilidades y exposiciones comunes (CVE) . 29/01/2014. Archivado del original el 30/04/2017 . Consultado el 16/05/2017 .
  6. Chen, Xiaobo; Caselden, Dan; Scott, Mike (26 de abril de 2014). "Se identifica una nueva vulnerabilidad de día cero dirigida a las versiones 9 a 11 de Internet Explorer en ataques dirigidos" . Blog de FireEye . FireEye . Consultado el 28 de abril de 2014 .
  7. Patrones de llenado de memoria de Visual C++ 6.0
  8. Depuración de punteros de Mudflap
  9. Dhurjati, D. y Adve, V. Detección eficiente de todos los usos de punteros colgantes en servidores de producción
  10. "Extensión de etiquetado de memoria ARM" . Proyecto de código abierto de Android . Consultado el 11 de junio de 2025 .
  11. Goodin, Dan (13 de mayo de 2025). "Google introduce el modo de protección avanzada para sus usuarios de Android más vulnerables" . Ars Technica . Consultado el 11 de junio de 2025 .