Tienes una codebase en C que es demasiado grande como para rewrite en Rust y demasiado crítica como para dejarla expuesta a desbordamientos de búfer. El hardware con capacidades CHERI promete detectar violaciones de seguridad en la memoria a nivel de CPU, pero internet no para de decirte que los punteros CHERI tienen 128 bits y tu código asume que tienen 64.

La buena noticia es que CHERI no impone una migración de todo o nada. El ABI híbrido te permite compilar código en C existente con cambios mínimos, ejecutarlo en hardware CHERI real y reforzar la seguridad de forma incremental donde más importa. No necesitas portar un millón de líneas de código antes de obtener tu primer puntero protegido.

Por qué el porting incremental es posible ahora

Las primeras arquitecturas con capacidades eran intransigentes. El iAPX 432 exigía que cada puntero fuera una capacidad, sin excepciones. Eso rompió todos los compiladores y sistemas operativos existentes, y el proyecto fracasó.

CHERI aprendió de ese fracaso. Admite tres modos de compilación: baseline (solo punteros enteros heredados), híbrido (punteros enteros y de capacidad coexisten) y purecap (cada puntero es una capacidad). El ABI híbrido es el puente. Te permite mantener tu código existente prácticamente intacto mientras introduces capacidades de forma selectiva donde aportan más valor.

En modo híbrido, void* sigue teniendo 64 bits. Un puntero de capacidad es un tipo distinto, void* __capability, en el que optas de forma explícita. Tus structs, tus listas enlazadas y tus tablas hash no cambian de tamaño a menos que tú lo solicites. Esto no es una capa de compatibilidad. Es un ABI de primera clase soportado por el hardware y el compilador.

Qué se rompe realmente al compilar para CHERI híbrido

El primer paso es intentar compilar tu código con una cadena de herramientas CHERI y ver qué explota. La mayoría del código en C bien comportado se compila sin cambios. Los problemas se concentran en un puñado de patrones que CHERI hace ilegales, que es precisamente el objetivo.

Conversiones de puntero a entero. El código que convierte un puntero a uintptr_t, enmascara algunos bits y lo vuelve a convertir fallará. En CHERI, uintptr_t es un tipo de capacidad, no un entero simple. Las operaciones bit a bit sobre capacidades eliminan el bit de etiqueta, y el valor resultante genera una excepción al desreferenciarse.

Suposiciones sobre el tamaño de los punteros. El código que codifica de forma rígida sizeof(void*) == 8 o serializa punteros como valores de 8 bytes se romperá en modo purecap. En modo híbrido esto funciona en su mayoría, pero se convierte en un problema en el momento en que mezclas punteros de capacidad y enteros en la misma struct.

Aritmética de punteros entre objetos no relacionados. Los programadores en C a veces calculan la distancia entre dos punteros arbitrarios o comparan punteros de asignaciones diferentes. Las capacidades CHERI transportan metadatos de límites, y restar capacidades de asignaciones diferentes es un comportamiento indefinido. El compilador lo rechazará o el hardware generará una excepción.

Ensamblado en línea. Cualquier ensamblado escrito a mano que mueva punteros en registros enteros necesitará actualizarse. CHERI tiene registros de capacidad dedicados, y el compilador necesita saber cuáles estás tocando.

La cadena de herramientas LLVM de CHERI te ofrece diagnósticos excelentes. No obtendrás errores crípticos del enlazador. Obtendrás mensajes claros como “cast from capability to integer is not allowed” o “arithmetic on capabilities from different allocations”. Corrige el primer error, recompila y persigue el siguiente.

Un ejemplo concreto: envolver un analizador con capacidades

Imagina que tienes un analizador de paquetes de red que lee entrada no confiable en un búfer y luego analiza las cabeceras. Este es exactamente el tipo de código que se beneficia de la comprobación de límites por hardware. Así es como envolverlo sin hacer rewrite del propio analizador.

Primero, compila el analizador en modo híbrido. La cadena de herramientas CHERI es LLVM estándar con un objetivo CHERI:

# Compilar un archivo individual con el ABI híbrido
$ clang --target=riscv64-unknown-freebsd \
    -march=rv64imafdcxcheri \
    -mabi=lp64d \
    -mno-relax \
    -c parser.c -o parser.o

El indicador -mabi=lp64d mantiene los punteros enteros de 64 bits. Tus tipos void* y char* existentes permanecen sin cambios. El analizador se compila tal cual.

Ahora añade un envoltorio consciente de capacidades en un archivo separado:

#include <cheriintrin.h>
#include <stddef.h>
#include <stdint.h>

// Analizador existente de parser.c
extern int parse_packet(const char *data, size_t len);

// Punto de entrada consciente de capacidades
int parse_packet_safe(const char * __capability data, size_t len) {
    // Restringe la capacidad exactamente a la longitud del búfer.
    // Incluso si la persona que llama pasó una asignación mayor,
    // el analizador no puede leer más allá de 'len' bytes.
    const char * __capability narrowed =
        cheri_bounds_set(data, len);

    // En modo híbrido, pasamos un puntero simple al analizador heredado.
    // El compilador inserta aquí una conversión de capacidad a entero.
    // Si la capacidad no cabe en 64 bits (no cabrá para
    // direcciones de capacidad grandes), esto emitirá una advertencia o error.
    //
    // Para la migración a purecap, modificarías parse_packet
    // para aceptar un puntero de capacidad.
    return parse_packet((const char *)narrowed, len);
}

En este ejemplo, parse_packet sigue usando punteros enteros heredados. El envoltorio restringe la capacidad de la persona que llama a la longitud exacta de la entrada, y luego la convierte de nuevo. El paso de restricción significa que, incluso si la persona que llama pasa accidentalmente un búfer de 4KB cuando quería pasar 64 bytes, el hardware hará respetar el límite de 64 bytes sobre la capacidad restringida.

Este no es el estado final. Es un peldaño. Obtienes el bounds enforcement en el límite de la API hoy, y puedes migrar parse_packet a purecap en una refactor posterior.

La ruta de migración de híbrido a purecap

El modo híbrido es un punto de partida, no un destino. Te ofrece compatibilidad pero no el beneficio de seguridad completo de las capacidades. El objetivo a largo plazo es purecap, donde cada puntero transporta límites.

La migración práctica se ve así:

  1. Compila en modo híbrido y corrige los errores de compilación. Esto suele significar corregir conversiones de puntero a entero y suposiciones sobre sizeof(void*). No cambies tus estructuras de datos todavía. Solo consigue una compilación limpia.

  2. Identifica objetivos de alto valor. Los analizadores de red, los decodificadores de formatos de archivo y las rutinas de deserialización son los mejores candidatos para el capability enforcement. Procesan entrada no confiable y es donde viven la mayoría de los errores de seguridad en la memoria.

  3. Envuelve esos objetivos con capacidades restringidas. Usa cheri_bounds_set para crear capacidades restringidas en los límites de confianza. Pásalas a tu código existente.

  4. Migra las funciones hoja a purecap. Empieza con funciones de utilidad que asignan y devuelven punteros. Cambia sus firmas para usar __capability y compílalas con -mabi=purecap. Avanza por la pila de llamadas hacia arriba.

  5. Eventualmente cambia todo el module a purecap. Cuando una unidad de compilación ya no tenga punteros enteros, compílala con -mabi=purecap y enlázala con el resto de tu código híbrido. La cadena de herramientas CHERI admite el enlazado de ABI mixto.

Este no es un proyecto de fin de semana para una codebase grande. Pero tampoco es un rewrite. Puedes proteger tus rutas de código más vulnerables en días, y migrar el resto de forma incremental a medida que lo tocas.

Cómo son realmente las compensaciones

El ABI híbrido tiene costes reales, y deberías conocerlos antes de comprometerte.

Primero, los ABI mixtos complican tu compilación. Ahora tienes archivos objeto compilados con diferentes tamaños de puntero en el mismo binario. El enlazador debe manejar reubicaciones de capacidad y de entero. La cadena de herramientas CHERI lo soporta, pero tu sistema de compilación probablemente aún no lo sabe. Tendrás que enseñárselo.

Segundo, la conversión de capacidad a entero en los límites híbridos es con pérdida. Si restringes una capacidad y luego la conviertes a un puntero simple, pierdes los límites. La memoria subyacente sigue protegida por la capacidad con la que empezaste, pero la función heredada no recibe hardware enforcement. Por eso purecap es el objetivo: cada puntero en la cadena de llamadas transporta sus propios límites.

Tercero, la depuración cambia. GDB en CHERI entiende los registros de capacidad y puede imprimir los metadatos de límites. El soporte de LLDB está mejorando. Si tu flujo de trabajo de depuración actual depende de inspeccionar valores de puntero sin procesar, necesitarás aprender los nombres de los registros CHERI.

La sobrecarga de rendimiento del modo híbrido suele ser insignificante para el código que usa principalmente punteros enteros. La sobrecarga de purecap suele ser de un dígito porcentual para la mayoría de cargas de trabajo, subiendo hasta un 10-15% para estructuras de datos con muchos punteros. Eso es competitivo con mitigaciones software como ASAN, y a diferencia de ASAN, CHERI se ejecuta a velocidad completa en producción.

Puedes empezar hoy con QEMU

No necesitas una placa Morello para experimentar. El proyecto CHERI mantiene soporte para QEMU e imágenes Docker con la cadena de herramientas LLVM preinstalada.

# Descargar la imagen Docker de la cadena de herramientas CHERI
$ docker run --rm -it ctsrd/cheri-sdk:latest

# Dentro del contenedor, compila tu código para RISC-V CHERI
$ clang --target=riscv64-unknown-freebsd \
    -march=rv64imafdcxcheri \
    -mabi=lp64d \
    -o myapp myapp.c

Empieza compilando tu proyecto en modo híbrido y contando los errores. El número te dirá cuánto trabajo te espera. Una compilación limpia a la primera es rara pero no imposible para C moderno y conforme a estándares. Unos pocos cientos de errores es lo típico para bases de código antiguas con muchas conversiones de punteros.

Corrige los errores en este orden: primero las conversiones de entero a puntero, luego la aritmética de punteros entre asignaciones, y luego el ensamblado en línea. Cada categoría tiene una solución mecánica. El proyecto CHERI publica una guía de porting con ejemplos de antes y después para los patrones más comunes.

El hardware con capacidades no es un futuro teórico. Es una cadena de herramientas que funciona, un ABI soportado y una ruta de migración que no requiere quemar tu codebase hasta los cimientos. Empieza con un archivo, una función, una capacidad restringida. El hardware hará el resto.