Los buffer overflows han estado en el CWE Top 25 durante veinte años. Tenemos stack canaries, ASLR, DEP, control-flow integrity y lenguajes memory-safe, y aún así siguen apareciendo en código crítico. La razón es simple: cada una de esas mitigaciones corre en software, y el software puede ser evadido, mal configurado o simplemente no usado.
¿Y si el hardware mismo se negara a permitirte leer más allá del final de un array?
Por qué las mitigaciones de software siguen perdiendo
Un buffer overflow ocurre cuando un programa escribe más allá de los límites de una región de memoria asignada. En C, esto es trivial porque un pointer es solo una dirección. El compiler y la CPU confían en que el programador sabe lo que hace. Si asignas 64 bytes y escribes en el offset 80, la CPU lo hará. No tiene idea de dónde termina el buffer.
Las mitigaciones de software intentan detectar esto después del hecho. Los stack canaries colocan un valor conocido antes de la return address y lo verifican antes de retornar. ASLR randomiza el layout de memoria para hacer que los exploits sean más difíciles de construir. La control-flow integrity restringe dónde pueden aterrizar los saltos indirectos. Esto ayuda, pero es probabilístico o incompleto. Un atacante determinado con suficiente tiempo y un infoleak usualmente puede sortearlos.
Los lenguajes memory-safe como Rust resuelven el problema a nivel de lenguaje, pero no ayudan con los miles de millones de líneas de C y C++ ya en producción. Reescribir el kernel de Linux, OpenSSL o tu backend legacy en Rust no va a pasar esta década. Necesitamos una defensa que proteja el código existente sin requerir una reescritura.
Lo que CHERI realmente le hace a un pointer
CHERI, que significa Capability Hardware Enhanced RISC Instructions, es una extensión de ISA desarrollada en la University of Cambridge y ahora soportada por Arm en forma de Morello. Cambia lo que es un pointer.
En un sistema convencional de 64 bits, un pointer es 64 bits: solo una dirección. En un sistema CHERI, un pointer se convierte en una capability de 128 o 256 bits. Los bits extra almacenan metadatos: la base address de la asignación, los bounds (hasta dónde se extiende) y los permissions (read, write, execute). La capability está protegida por una hardware integrity tag, de modo que manipular los metadata la invalida.
Cuando compilas código para CHERI, malloc no solo devuelve una dirección. Devuelve una capability cuyos bounds son exactamente el tamaño que solicitaste. Cuando incrementas ese pointer, el hardware verifica cada acceso contra esos bounds. Si intentas leer o escribir fuera de ellos, la CPU levanta una synchronous exception. No hay forma de falsificar una capability con bounds más amplios. El hardware simplemente no te lo permite.
Esta es la diferencia clave. El software bounds checking inserta checks alrededor de las operaciones de memoria, que los compilers pueden optimizar, los programadores olvidar y los atacantes evadir. El hardware bounds checking ocurre en cada load y store, incondicionalmente, sin agregar instructions a tu programa.
Cómo funciona la enforce de bounds a nivel de instruction
Así es como se ve un buffer overflow típico en C:
#include <string.h>
void vulnerable(char *input) {
char buf[64];
strcpy(buf, input); // No bounds check. Classic overflow.
}
En una arquitectura normal, esto corrompe el stack. En CHERI, buf no es una raw address. Es una capability cuyos bounds son exactamente 64 bytes. Cuando strcpy intenta escribir más allá del byte 64, el hardware levanta una excepción de Capability Bounds Violation. El programa crasha inmediatamente en la instruction ofensora exacta.
La misma protección aplica a las asignaciones de heap:
#include <stdlib.h>
#include <cheri.h> // CHERI intrinsics header
void heap_example(void) {
char *buf = malloc(32);
// buf is a capability with base=buf, length=32
// This works fine
buf[0] = 'A';
buf[31] = 'Z';
// This traps at the hardware level
buf[32] = 'X'; // Capability bounds violation
}
El crash es preciso. Obtienes la instruction exacta, la capability exacta y los bounds exactos que fueron violados. Depurar esto es más fácil que perseguir un stack frame corrompido tres frames después.
CHERI también previene la pointer forgery. No puedes castear un integer a un pointer y empezar a dereferenciar memoria arbitraria. El hardware solo reconoce capabilities creadas por instructions legítimas como malloc, stack allocation o derivación explícita de capabilities. Los casts de integer a pointer producen valores untagged que trapan al usarse.
El problema de compatibilidad es real
Si CHERI es tan bueno, ¿por qué no corre en cada servidor? Porque cambiar lo que es un pointer rompe suposiciones en las que décadas de código C dependen.
El primer problema es el tamaño. Las capabilities son más grandes que los raw pointers. En Morello, una capability es 128 bits más un tag de 1 bit almacenado separadamente por el hardware. Esto incrementa el uso de memoria para estructuras de datos con muchos pointers. Una linked list o un árbol lleno de pointers se vuelve notablemente más grande. Para muchas aplicaciones el overhead es de un dígito porcentual, pero para workloads de pointer-chasing puede ser peor.
El segundo problema es el casting. El código C casta pointers a uintptr_t, hace aritmética y casta de vuelta. En CHERI, uintptr_t es en realidad un tipo capability, no un integer. El código que asume que puede hacer matemática de integer arbitraria sobre valores de pointer fallará al compilar o trapará en runtime. La solución suele ser usar ptrdiff_t para offsets y aplicarlos con cheri_address_set, pero eso requiere cambiar el source.
El tercer problema es el ecosistema. El hardware CHERI es raro. Las boards Arm Morello existen pero no son commodity servers. CheriBSD y Linux habilitado para CHERI son lo suficientemente maduros para correr software real, pero la mayoría de las distribuciones Linux no envían packages CHERI. Tu container registry, tus CI runners y tu cloud provider aún no lo soportan.
Puedes probarlo hoy sin silicon personalizado
No necesitas una board Morello para experimentar con CHERI. El proyecto CHERI mantiene soporte para QEMU, así que puedes correr un userspace CHERI en cualquier host Linux o macOS.
Así es como obtener una toolchain CHERI y correr un ejemplo simple en QEMU:
# Clone the CHERI SDK setup
$ git clone https://github.com/CTSRD-CHERI/cheribuild.git
$ cd cheribuild
# Build the CheriBSD disk image and QEMU
$ ./cheribuild.py cheribsd-riscv64-purecap -d
$ ./cheribuild.py qemu -d
# Boot CheriBSD in QEMU
$ ./cheribuild.py run-cheribsd-riscv64
Una vez dentro de CheriBSD, el compiler es clang con soporte de target CHERI. Compilas con bounds checking enforceado por default:
$ clang -o test test.c
$ ./test
# Buffer overflows trap immediately with a clear message
Si quieres probar software existente, cheribuild puede compilar packages populares de FreeBSD ports con soporte CHERI. El equipo de CHERI ha portado OpenSSH, nginx, PostgreSQL y grandes partes del sistema base de FreeBSD. Muchos programas funcionan sin cambios. Los que se rompen usualmente lo hacen por casts inseguros de pointer o suposiciones sobre el tamaño de pointer.
Para un inicio más rápido sin QEMU, la University of Cambridge también publica una imagen Docker con la toolchain LLVM de CHERI preinstalada. Puedes compilar binarios CHERI e inspeccionar el assembly capability-aware generado sin bootear un OS completo.
Qué significa la hardware memory safety para codebases existentes
CHERI no es un reemplazo por escribir código seguro. Es una red de seguridad para el código que ya tienes. Un sistema CHERI corriendo C legacy todavía tendrá logic bugs, race conditions y errores de use-after-free. Pero no tendrá buffer overflows que sobrescriban return addresses, corrompan metadata adyacente de heap o filtren secrets al otro lado de los límites de asignación.
La progresión ya es visible. Arm ha incorporado features derivadas de CHERI en su Memory Tagging Extension (MTE), que se está enviando hoy en dispositivos Android de producción. MTE es más grueso que CHERI (tag gránulos de 16 bytes en lugar de asignaciones individuales), pero atrapa muchos de los mismos bugs con menor overhead. El CHERI completo probablemente seguirá a medida que el ecosistema madura.
Si mantienes código en C o C++ que procesa untrusted input, la pregunta no es si la hardware memory safety llegará. Es si estarás listo cuando lo haga. Empieza auditando tu código en busca de casts de pointer a integer, aritmética de pointer sobre objetos no relacionados y suposiciones sobre sizeof(void*). Esos son los patterns que se rompen bajo CHERI, y arreglarlos hace tu código más limpio incluso en hardware convencional.
El hardware finalmente está dispuesto a decir que no. Deberíamos dejarlo.