Noticias

¿Qué es un use-after-free? El error de usar memoria ya liberada, explicado con una llave de hotel

16 min de lectura

Imagina que te hospedas en un hotel, entregas el cuarto 214 al irte y, sin que nadie lo note, te quedas con una copia de la tarjeta. Al día siguiente el hotel le da ese cuarto a otra persona, y tu tarjeta vieja todavía abre la puerta. Eso, dentro de un programa, se llama use-after-free (uso después de liberar): el programa devuelve un pedazo de memoria, pero conserva un puntero que sigue señalando ahí, y más tarde lo usa cuando ese pedazo ya es de otra cosa. Es uno de los errores más explotados en navegadores y sistemas operativos, y entenderlo explica buena parte de los parches de seguridad que instalas cada mes.

Para entenderlo hace falta una idea previa. Los programas guardan sus datos en la memoria, y los que no tienen un tamaño o una vida fija (una pestaña abierta, un mensaje recibido, la sesión de un usuario) van en una zona llamada heap (montón). En lenguajes como C o C++, el programa pide un bloque del heap cuando lo necesita (con malloc o new) y lo devuelve cuando termina (con free o delete). Lo que recibe al pedirlo es un puntero: un número que dice en qué dirección de memoria está el bloque, como el número del cuarto. Nadie revisa que, después de devolverlo, el programa deje de usar ese número. Un puntero que apunta a memoria ya liberada se llama dangling pointer (puntero colgante), y usarlo es el error.

La llave que sigue abriendo Lo que hace el programa y su analogía 1. RESERVAR malloc Sesión de ana El hotel te da el cuarto 214 y su llave. El programa recibe un bloque y un puntero a él. 2. LIBERAR free libre Entregas el cuarto, pero te quedas una copia de la llave: un puntero colgante. 3. REUTILIZAR malloc Mensaje de otro El hotel da el 214 a otro huésped. El asignador entrega el mismo bloque a otra cosa. 4. USAR EL PUNTERO VIEJO activa->es_admin Mensaje de otro Tu llave vieja abre, y lo que lees o cambias ya es del otro huésped. Eso es el use-after-free. El bloque está en la misma dirección en los cuatro pasos: lo que cambió es su dueño. Basado en el ejemplo sesion.c del artículo. Gráfica: decodigo.com.
Los cuatro pasos de un use-after-free y su equivalente en el hotel. Gráfica: decodigo.com.

Lo grave no es el paso 2, sino el 3. El asignador de memoria, la parte del programa que reparte los bloques, reutiliza los que se devuelven, y lo hace pronto: un bloque recién liberado es justo el que entregará a la siguiente petición de un tamaño parecido, porque así es más rápido. Por eso el puntero viejo casi nunca apunta a «nada»: apunta a datos nuevos de otra parte del programa. Y si esos datos los eligió un atacante, el programa termina leyendo o escribiendo lo que el atacante quiso.

Claves de un use-after-free

  • Qué es: usar un bloque de memoria después de haberlo liberado, a través de un puntero o una referencia que quedó vieja.
  • Dónde ocurre: en lenguajes con manejo manual de memoria, sobre todo C y C++, que son los de los navegadores, los kernels, los sistemas de archivos, los controladores y muchos servidores.
  • Por qué es traicionero: el programa suele seguir funcionando, porque la memoria liberada conserva el dato viejo o recibe uno nuevo con forma válida. Solo se cae a veces, o en otra máquina, o con otra opción del compilador.
  • Por qué es grave: si un atacante logra que su información ocupe el bloque liberado, puede leer datos ajenos, cambiar permisos o desviar el programa hacia su propio código.
  • Cómo se detecta: con herramientas como AddressSanitizer o Valgrind al ejecutar las pruebas, y con fuzzing, que alimenta al programa con millones de entradas raras.
  • Cómo se evita: con un dueño claro para cada bloque, punteros inteligentes en C++, defensas del asignador y del hardware y, de raíz, con lenguajes que no permiten el error, como Rust, o que liberan la memoria solos, como Java, Go o Python.

El error en treinta líneas de C

Este programa reproduce la historia del hotel. Crea la sesión de «ana», sin permisos de administrador, y guarda una copia del puntero en activa, como haría otra parte de un programa real. Luego cierra la sesión, que libera el bloque, pero nadie le avisa a activa. Enseguida llega un mensaje de la red de 28 bytes, el mismo tamaño que una sesión, y el programa lo guarda en un bloque nuevo. Por último, consulta el permiso con el puntero viejo:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char usuario[24];
    int es_admin;
} Sesion;

Sesion *activa;                  /* otra parte del programa guarda una copia del puntero */

void cerrar_sesion(Sesion *s) {
    free(s);                     /* se libera el bloque... pero «activa» sigue apuntando ahí */
}

int main(void) {
    Sesion *ana = malloc(sizeof *ana);
    if (ana == NULL) return 1;
    strcpy(ana->usuario, "ana");
    ana->es_admin = 0;
    activa = ana;

    cerrar_sesion(ana);

    /* Llega un mensaje de 28 bytes desde la red: malloc entrega el bloque recién liberado */
    char *mensaje = malloc(sizeof(Sesion));
    if (mensaje == NULL) return 1;
    memcpy(mensaje, "AAAAAAAAAAAAAAAAAAAAAAAA\x01\x00\x00\x00", sizeof(Sesion));

    if (activa->es_admin)
        printf("Acceso de administrador concedido (es_admin = %d)\n", activa->es_admin);
    else
        printf("Acceso normal\n");

    free(mensaje);
    return 0;
}

Lo compilamos con gcc 15.3 en un contenedor desechable y lo ejecutamos tres veces. Ni un aviso al compilar, ni un error al ejecutar, y las tres veces el mismo resultado:

salida, sin herramientas$ gcc -Wall -Wextra -g -O0 sesion.c -o sesion
$ ./sesion
Acceso de administrador concedido (es_admin = 1)

El asignador de glibc le entregó al mensaje exactamente el bloque que acababa de liberar la sesión, y los bytes 25 a 28 del mensaje (\x01\x00\x00\x00) cayeron justo donde antes estaba es_admin. El puntero viejo leyó un 1 y el programa le dio permisos de administrador a una sesión cerrada. Nadie escribió en es_admin: bastó con que alguien eligiera bien el contenido de un mensaje.

Lo más inquietante vino al cambiar una sola opción. Con -O2, gcc sí reconoce el error, porque al optimizar mete cerrar_sesion dentro de main y ve el free y el uso juntos:

gcc -O2: avisosesion.c: In function 'main':
sesion.c:30:15: warning: pointer 'ana' used after 'free' [-Wuse-after-free]
   30 |     if (activa->es_admin)
      |         ~~~~~~^~~~~~~~~~
In function 'cerrar_sesion',
    inlined from 'main' at sesion.c:23:5:
sesion.c:13:5: note: call to 'free' here

Y ese binario imprime «Acceso normal». El mismo código, dos resultados opuestos según cómo se compile: el estándar de C dice que usar memoria liberada es comportamiento indefinido, así que el compilador no tiene que respetar ningún resultado en particular. Un programa con este error puede pasar todas las pruebas en la computadora del programador y fallar, o abrirse a un ataque, en la del usuario. Tampoco hay que confiarse del aviso: aparece solo cuando el compilador ve las dos cosas en la misma función, y en un programa real el free y el uso suelen estar en archivos distintos.

La herramienta que sí lo atrapa es AddressSanitizer (ASan), que viene con gcc y clang. Se activa con una opción al compilar y vigila cada acceso a memoria mientras el programa corre:

gcc -fsanitize=address: salida recortada==30==ERROR: AddressSanitizer: heap-use-after-free on address 0x75c0113e0058 at pc 0x0000004012ef bp 0x7ffc13b0d720 sp 0x7ffc13b0d718
READ of size 4 at 0x75c0113e0058 thread T0
    #0 0x0000004012ee in main /lab/sesion.c:30
…
0x75c0113e0058 is located 24 bytes inside of 28-byte region [0x75c0113e0040,0x75c0113e005c)
freed by thread T0 here:
    #0 0x7990127dbd4b  (/usr/local/lib64/libasan.so.8+0x118d4b)
    #1 0x0000004011dd in cerrar_sesion /lab/sesion.c:13
    #2 0x00000040127a in main /lab/sesion.c:23
…
previously allocated by thread T0 here:
    #0 0x7990127dd08b in malloc (/usr/local/lib64/libasan.so.8+0x11a08b)
    #1 0x0000004011f2 in main /lab/sesion.c:17
…
SUMMARY: AddressSanitizer: heap-use-after-free /lab/sesion.c:30 in main
Shadow bytes around the buggy address:
=>0x75c0113e0000: fa fa 00 00 00 fa fa fa fd fd fd[fd]fa fa 00 00
…
  Freed heap region:       fd

Se lee de arriba abajo. heap-use-after-free es el diagnóstico. READ of size 4 dice que se leyeron 4 bytes (un int) en la línea 30. «24 bytes inside of 28-byte region» ubica la lectura dentro del bloque: el byte 24, que es donde vive es_admin después de los 24 de usuario. Luego vienen las otras dos piezas del crimen: dónde se liberó (línea 13, llamada desde la 23) y dónde se había reservado (línea 17). La última línea es el mapa de la memoria vigilada: cada par de cifras representa 8 bytes y fd marca memoria liberada; el que está entre corchetes es el que se tocó.

ASan lo logra con un truco que también explica por qué el error es difícil de ver sin él: cuando el programa libera un bloque, ASan no lo devuelve de inmediato al asignador, sino que lo deja en cuarentena y lo marca como prohibido. Así el paso 3 no ocurre y cualquier acceso al bloque se detecta. Valgrind hace algo parecido sin recompilar, y también lo encontró («Invalid read of size 4… 24 bytes inside a block of size 28 free’d»); bajo Valgrind, por cierto, el programa imprimió «Acceso normal», porque su asignador tampoco reutiliza el bloque enseguida. Las dos herramientas solo ven el error si ese camino del programa se ejecuta durante la prueba.

Por qué no es solo un programa que se cae

En el ejemplo, el «atacante» tuvo suerte fácil: el mensaje llegó justo después de liberar la sesión y era del tamaño exacto. En un navegador o en un kernel no es tan simple, pero las técnicas para conseguirlo están muy estudiadas. La primera se llama heap grooming (acomodar el heap): el atacante hace que el programa reserve y libere bloques en un orden calculado, para que el heap quede en un estado predecible. La segunda, heap spraying (rociar el heap): pide miles de bloques del mismo tamaño que el objeto liberado, todos con su contenido, para que alguno caiga justo en el hueco. En un navegador basta con una página web con JavaScript que cree muchos objetos o cadenas; en un kernel, con un programa sin privilegios que abra archivos, sockets o mensajes.

Cómo el atacante ocupa el hueco Bloques del heap del mismo tamaño A. El programa libera un objeto pero un puntero sigue apuntando a él. puntero viejo era el obj B. El atacante pide miles de bloques de ese tamaño con su contenido (spraying). puntero viejo X X X Antes ordena el heap para que los huecos estén donde le conviene (grooming). C. El programa usa el puntero viejo y lee el dato X del atacante. puntero viejo X X X Si X es un permiso el programa cree que eres admin Si X es una dirección de función salta al código del atacante Si X es basura el programa se cae Esquema simplificado: los ataques reales combinan varios fallos. Gráfica: decodigo.com.
Cómo se ocupa el bloque liberado con datos del atacante. Gráfica: decodigo.com.

Lo que pase después depende de qué había en el objeto original. Si guardaba un permiso, el atacante lo cambia, como en el ejemplo. Si guardaba la dirección de una función, que es muy común en C++ porque así funcionan los métodos virtuales, el programa salta a donde el atacante diga. Si era la longitud de un búfer, ahora puede leer o escribir más allá. Por eso un use-after-free rara vez se usa solo: en los ataques reales suele ser la primera pieza de una cadena que termina en ejecutar código o en escapar del sandbox del navegador.

Conviene distinguirlo de otros dos errores de memoria con los que se le confunde:

  • Double free (liberar dos veces): el programa llama a free dos veces sobre el mismo bloque. El asignador queda con su lista interna corrupta y puede entregar el mismo bloque a dos peticiones a la vez, que termina siendo un use-after-free. Es como devolver dos veces el mismo cuarto: el hotel lo apunta como libre dos veces y se lo da a dos huéspedes.
  • Buffer overflow (desbordamiento): el programa escribe más allá del final de un bloque que sí le pertenece y pisa al vecino. Es un error de espacio (te sales de tu cuarto); el use-after-free es de tiempo (entras a tu cuarto cuando ya no es tuyo).

En C++ también: el botón que recuerda una ventana cerrada

C++ tiene punteros inteligentes que liberan solos, y aun así el use-after-free es su fallo grave más frecuente. La razón es que el problema no es quién libera, sino quién se queda con un acceso después. Este ejemplo imita lo que pasa en una interfaz o en un navegador: una ventana se suscribe al clic de un botón con una función que guarda this, un puntero a la propia ventana. El usuario cierra la ventana y después pulsa el botón:

#include <functional>
#include <memory>
#include <print>
#include <string>
#include <vector>

struct Boton {
    std::vector<std::function<void()>> al_pulsar;
    void pulsar() { for (auto& f : al_pulsar) f(); }
};

struct Ventana {
    std::string titulo = "Informe de ventas de noviembre";
    explicit Ventana(Boton& b) {
        b.al_pulsar.push_back([this] { std::println("Guardando {}", titulo); });
    }
};

int main() {
    Boton guardar;
    auto v = std::make_unique<Ventana>(guardar);
    v.reset();          // el usuario cerró la ventana
    guardar.pulsar();   // ...pero el botón todavía la recuerda
}

Aquí la ventana vive en un unique_ptr, el puntero inteligente más estricto, y se libera correctamente. Pero el botón guardó su propia copia de this, y el unique_ptr no tiene forma de saberlo. Sin herramientas, las tres ejecuciones terminaron igual:

salida, sin herramientas$ g++ -std=c++23 -Wall -Wextra -g -O0 boton.cpp -o boton
$ ./boton
Segmentation fault (core dumped)

Esta vez se cayó en lugar de seguir: al liberar, el asignador escribió sus propios datos internos al principio del bloque, justo donde la cadena titulo guardaba la dirección de sus letras, y al imprimirla el programa saltó a una dirección inválida. Que se caiga es lo mejor que puede pasar, porque así se nota. ASan dice qué ocurrió (recortamos los marcos internos de la biblioteca estándar):

g++ -fsanitize=address: salida recortada==45==ERROR: AddressSanitizer: heap-use-after-free on address 0x74286b3e0040 at pc 0x00000040e66f bp 0x7ffcb3f5aa00 sp 0x7ffcb3f5a9f8
READ of size 8 at 0x74286b3e0040 thread T0
…
    #9 0x0000004076c3 in Ventana::Ventana(Boton&)::{lambda()#1}::operator()() const /lab/boton.cpp:15
…
    #14 0x000000407499 in Boton::pulsar() /lab/boton.cpp:9
    #15 0x000000404772 in main /lab/boton.cpp:23
…
0x74286b3e0040 is located 0 bytes inside of 32-byte region [0x74286b3e0040,0x74286b3e0060)
freed by thread T0 here:
    #0 0x77f86c807afb in operator delete(void*, unsigned long) (/usr/local/lib64/libasan.so.8+0x11bafb)
…
    #4 0x000000404766 in main /lab/boton.cpp:22
…
previously allocated by thread T0 here:
    #0 0x77f86c806b9b in operator new(unsigned long) (/usr/local/lib64/libasan.so.8+0x11ab9b)
…
    #2 0x000000404755 in main /lab/boton.cpp:21
…
SUMMARY: AddressSanitizer: heap-use-after-free /lab/boton.cpp:15 in Ventana::Ventana(Boton&)::{lambda()#1}::operator()() const

La lectura ocurre dentro de la lambda de la línea 15, llamada por pulsar() desde la línea 23; la ventana de 32 bytes se liberó en la línea 22 (v.reset()) y se había creado en la 21. Una forma de corregirlo es que el botón no guarde un puntero que mantenga a la ventana como si fuera suya, sino un weak_ptr: una referencia que puede preguntar si el objeto sigue vivo antes de usarlo.

#include <functional>
#include <memory>
#include <print>
#include <string>
#include <vector>

struct Boton {
    std::vector<std::function<void()>> al_pulsar;
    void pulsar() { for (auto& f : al_pulsar) f(); }
};

struct Ventana {
    std::string titulo = "Informe de ventas de noviembre";
};

int main() {
    Boton guardar;
    auto v = std::make_shared<Ventana>();
    guardar.al_pulsar.push_back([w = std::weak_ptr<Ventana>(v)] {
        if (auto ventana = w.lock())
            std::println("Guardando {}", ventana->titulo);
        else
            std::println("La ventana ya no existe: no hago nada");
    });
    guardar.pulsar();
    v.reset();
    guardar.pulsar();
}
salida, con -fsanitize=addressGuardando Informe de ventas de noviembre
La ventana ya no existe: no hago nada

Otra solución, igual de válida, es que la ventana se dé de baja del botón en su destructor. Lo importante es que alguien sea responsable de que el acceso no sobreviva al objeto. En nuestras páginas de C++ hay más casos del mismo tipo, los más comunes en el día a día: un string_view que apunta a una cadena temporal, o una referencia a un elemento de un vector que crece y se muda de bloque.

Por qué abunda en navegadores y kernels

Un navegador moderno tiene millones de líneas de C++ y maneja objetos con vidas enredadas: los nodos de una página, que un script puede borrar en cualquier momento; los callbacks que se disparan cuando termina una descarga o cambia el tamaño de la ventana; los hilos que comparten datos; el motor de JavaScript, que optimiza el código al vuelo, y la GPU, que procesa órdenes en paralelo. Basta con que un callback se ejecute después de que su objeto murió, como en el ejemplo del botón, o con que dos hilos no se pongan de acuerdo sobre quién libera, para que aparezca un use-after-free. Además, el atacante tiene una ventaja enorme: una página web le deja ejecutar su propio JavaScript dentro del navegador, que es justo lo que necesita para acomodar el heap.

Un kernel tiene el mismo problema con otras piezas: controladores, sistemas de archivos, la red y miles de llamadas al sistema que pueden ocurrir a la vez desde varios procesos. Ahí un use-after-free suele servir para pasar de usuario normal a root. Algunos casos recientes:

  • Chrome, febrero de 2026 (CVE-2026-2441). «Use after free in CSS», es decir, en el código que aplica los estilos de las páginas. Google lo corrigió el 13 de febrero en Chrome 145.0.7632.75 y avisó de que ya existía un exploit en circulación; la agencia de ciberseguridad de Estados Unidos (CISA) lo añadió el 17 de febrero a su catálogo de vulnerabilidades explotadas.
  • Chrome, marzo de 2026 (CVE-2026-5281). «Use after free in Dawn», la biblioteca con la que Chrome implementa WebGPU. Corregido el 31 de marzo en Chrome 146.0.7680.177, también con un exploit en circulación y en el catálogo de CISA desde el 1 de abril. La descripción de CISA muestra cómo se encadenan estos fallos: permite ejecutar código a un atacante «que ya había comprometido el proceso de renderizado» con otro error.
  • Linux, noviembre de 2026. El 6 de noviembre, Greg Kroah-Hartman publicó el kernel estable 7.3.3 y, el mismo día, las versiones de soporte largo 6.18.62, 6.12.116, 6.6.162, 6.1.192, 5.15.225 y 5.10.274. Todas incluyen dos correcciones de use-after-free en los controladores de Xen que dan disco y red a las máquinas virtuales: «xen-blkback: Avoid UAF when reading I/O statistics» y «xen-netback: Fix UAF when reading statistics data». En ambos casos, leer las estadísticas mientras el dispositivo se desmontaba podía tocar memoria ya liberada. Una tercera corrección de xen-netback, del aviso XSA-527, ordena cuándo se libera una lista para que nadie la consulte después.
  • Linux, nf_tables (CVE-2024-1086). Un use-after-free en netfilter, el filtro de paquetes del kernel, que según CISA «permite a un atacante lograr una escalada local de privilegios». Está en su catálogo de fallos explotados desde el 30 de mayo de 2024 y es el ejemplo de manual de un usuario normal que termina como root.

De los zero-days de Chrome que Google reconoció como explotados en 2025, ninguno fue un use-after-free: fueron de otros tipos, como type confusion o accesos fuera de límites. Que en 2026 vuelvan a aparecer dos muestra que el error sigue vivo a pesar de las defensas que Chrome añadió en los últimos años.

Cómo se evita

Capas de defensa De evitar el error a hacerlo inútil LENGUAJE EVITA Rust: el borrow checker no compila el uso tras liberar. Java, Go, Python: el recolector no libera lo que se usa. DISEÑO DEL CÓDIGO EVITA Un solo dueño por bloque. En C++, RAII, unique_ptr, shared_ptr y weak_ptr. Nada de vistas que duren más. PRUEBAS DETECTA AddressSanitizer, Valgrind y fuzzing lo encuentran antes de publicar, si el camino con el error se ejecuta. PROGRAMA EN USO MITIGA MiraclePtr en Chrome: el bloque no se reutiliza mientras queden punteros que lo señalen. ASIGNADOR Y SISTEMA MITIGA Asignadores endurecidos: más difícil predecir qué bloque recibirá el atacante. HARDWARE DETECTA MTE de ARM: memoria y puntero llevan una etiqueta; si ya no coinciden, el procesador lo nota. Ninguna capa basta sola: las de arriba quitan errores, las de abajo los encarecen. Datos: Chromium, Arm y la documentación de cada herramienta. Gráfica: decodigo.com.
Las defensas, desde el lenguaje hasta el procesador. Gráfica: decodigo.com.

No hay una sola solución; hay capas, y las mejores evitan el error en lugar de perseguirlo.

  • En C, un dueño claro para cada bloque. Decide qué función o estructura es responsable de liberar cada bloque y que nadie más guarde copias del puntero sin saberlo. Poner el puntero en NULL justo después de free ayuda, porque un uso posterior falla de forma clara en vez de leer basura, pero solo protege a esa variable: en el ejemplo, poner ana = NULL no habría salvado a activa.
  • En C++, RAII y punteros inteligentes. unique_ptr para un solo dueño, shared_ptr cuando de verdad hay varios y weak_ptr para quien solo observa. Y la regla que el ejemplo del botón muestra: referencias, vistas (string_view, span), iteradores y lambdas que capturan this o por referencia no deben vivir más que su objeto.
  • Herramientas en las pruebas. Compilar las pruebas con -fsanitize=address, revisar con Valgrind lo que no se puede recompilar y hacer fuzzing: generar millones de entradas raras con herramientas como libFuzzer o AFL++ mientras ASan vigila. Es así como se encuentra la mayoría de estos errores antes de que lleguen a los usuarios. Y no ignorar los avisos -Wuse-after-free y -Wdangling-reference de gcc.
  • Defensas dentro del programa. Chrome usa desde 2022 MiraclePtr (técnicamente, BackupRefPtr): su asignador, PartitionAlloc, lleva una cuenta oculta de cuántos punteros protegidos señalan cada bloque, y si el programa lo libera mientras la cuenta no es cero, lo pone en cuarentena y lo llena de basura en vez de reutilizarlo. El hueco ya no se puede ocupar. Google lo activó en Chrome 102 para el proceso principal en Windows y Android, calculó que protegería contra la explotación a cerca de la mitad de los use-after-free y pagó un 3.5 a 6.5 % más de memoria.
  • Asignadores endurecidos. glibc protege desde la versión 2.32 sus listas internas de bloques libres con «Safe-Linking», que mezcla las direcciones con un valor aleatorio. El kernel Linux tiene opciones como SLAB_FREELIST_HARDENED y, desde la versión 6.6, cachés de kmalloc aleatorias, pensadas para «hacer más difícil el heap spraying». No evitan el error, pero hacen menos predecible qué bloque recibirá el atacante.
  • Hardware. La Memory Tagging Extension (MTE) de ARM le pone una pequeña etiqueta a cada trozo de memoria y otra al puntero; al liberar y reutilizar, la etiqueta cambia, y si se usa un puntero viejo, el procesador nota que no coinciden. Android la ofrece en los Pixel desde el Pixel 8, y Apple presentó en septiembre de 2025 Memory Integrity Enforcement, basada en una versión mejorada (EMTE), activa en todos los iPhone 17 y en el iPhone Air.
  • Lenguajes con seguridad de memoria. En Rust, el borrow checker comprueba al compilar que ninguna referencia viva más que el dato al que apunta: el programa de la sesión no compila. En Java, Go, Python, C# o JavaScript, un recolector de basura solo libera lo que ya nadie puede alcanzar, así que no existe un puntero a memoria liberada. Rust permite saltarse esas reglas dentro de bloques unsafe, y los lenguajes con recolector tienen sus propios fallos, pero esta clase de error desaparece casi por completo.

Lo que está en juego cada vez que actualizas el navegador

El proyecto Chromium analizó 912 errores de seguridad graves o críticos de su código desde 2015 y concluyó que «alrededor del 70 %» son problemas de seguridad de memoria y que «la mitad de esos son use-after-free». Es decir, cerca de uno de cada tres fallos graves de Chrome es de este tipo. Microsoft había dado en 2019 una cifra parecida para todo su software: cerca del 70 % de las vulnerabilidades a las que asigna un CVE cada año eran de seguridad de memoria. Y la experiencia de Android muestra que el remedio funciona: Google informó en noviembre de 2025 que, al escribir el código nuevo en Rust, los fallos de memoria bajaron por primera vez a menos del 20 % del total de sus vulnerabilidades.

Para quien no programa, la conclusión es concreta. Un use-after-free en el navegador se puede activar con solo abrir una página, sin descargar nada, y los dos de Chrome de este año ya se usaban en ataques reales cuando se publicó el parche. Por eso las actualizaciones del navegador y del sistema no son un trámite: muchas de ellas cierran exactamente este tipo de puerta.

El use-after-free no es un error exótico: es lo que pasa cuando un programa olvida que ya devolvió algo, y en C y C++ nada lo impide por defecto. El ejemplo de la sesión lo resume: un mensaje bien elegido convirtió una sesión cerrada en una de administrador sin un solo aviso, y el resultado cambió con la forma de compilar. Para el programador, la receta es la de siempre: dueños claros, punteros inteligentes, sanitizadores en las pruebas y, en código nuevo, un lenguaje que no permita el error. Para todos los demás, lo práctico es lo más simple: actualizar el navegador y el sistema operativo en cuanto hay versión nueva.

Para profundizar: cómo funcionan los punteros en C y la memoria dinámica con malloc y free; los fallos de seguridad típicos de C; referencias, punteros y punteros inteligentes en C++ y los fallos de seguridad típicos de C++. Y del lado de Rust, por qué crece en Linux y el fallo que el borrow checker arrastró durante 11 años.

Fuente: Chrome Releases (CVE-2026-2441); Chrome Releases (CVE-2026-5281); catálogo KEV de CISA; ChangeLog de Linux 7.3.3; Chromium, seguridad de memoria; MSRC (2019); Google Security Blog, Rust en Android; Google Security Blog, MiraclePtr; Android, MTE; Apple, Memory Integrity Enforcement; KernelNewbies, Linux 6.6. Pruebas propias con gcc 15.3 y AddressSanitizer.