Buffer overflows estão no CWE Top 25 há vinte anos. Temos stack canaries, ASLR, DEP, control-flow integrity e linguagens memory-safe, e ainda assim eles continuam aparecendo em código crítico. A razão é simples: cada uma dessas mitigações roda em software, e software pode ser contornado, mal configurado ou simplesmente não usado.
E se o próprio hardware se recusasse a deixar você ler além do final de um array?
Por que as mitigações de software continuam perdendo
Um buffer overflow acontece quando um programa escreve além dos limites de uma região de memória alocada. Em C, isso é trivial porque um pointer é apenas um endereço. O compiler e a CPU confiam que o programador sabe o que está fazendo. Se você alocar 64 bytes e escrever no offset 80, a CPU fará isso. Ela não tem ideia de onde o buffer termina.
As mitigações de software tentam detectar isso depois do fato. Stack canaries colocam um valor conhecido antes do return address e o verificam antes de retornar. ASLR randomiza o layout da memória para tornar exploits mais difíceis de construir. Control-flow integrity restringe onde jumps indiretos podem aterrissar. Isso ajuda, mas é probabilístico ou incompleto. Um atacante determinado com tempo suficiente e um infoleak geralmente consegue contorná-los.
Linguagens memory-safe como Rust resolvem o problema no nível da linguagem, mas não ajudam os bilhões de linhas de C e C++ já em produção. Reescrever o kernel Linux, o OpenSSL ou seu backend legado em Rust não vai acontecer nesta década. Precisamos de uma defesa que proteja o código existente sem exigir uma reescrita.
O que o CHERI realmente faz com um pointer
CHERI, que significa Capability Hardware Enhanced RISC Instructions, é uma extensão de ISA desenvolvida na University of Cambridge e agora suportada pela Arm na forma de Morello. Ela muda o que é um pointer.
Em um sistema convencional de 64 bits, um pointer é 64 bits: apenas um endereço. Em um sistema CHERI, um pointer se torna uma capability de 128 ou 256 bits. Os bits extras armazenam metadados: o base address da alocação, os bounds (até onde se estende) e as permissions (read, write, execute). A capability é protegida por uma hardware integrity tag, de forma que a manipulação dos metadata a invalida.
Quando você compila código para CHERI, malloc não retorna apenas um endereço. Ele retorna uma capability cujos bounds são exatamente o tamanho solicitado. Quando você incrementa esse pointer, o hardware verifica cada acesso contra esses bounds. Se você tentar ler ou escrever fora deles, a CPU levanta uma synchronous exception. Não há como forjar uma capability com bounds mais amplos. O hardware simplesmente não permite.
Essa é a diferença-chave. O software bounds checking insere checks ao redor de operações de memória, que compilers podem otimizar, programadores podem esquecer e atacantes podem contornar. O hardware bounds checking acontece em cada load e store, incondicionalmente, sem adicionar instructions ao seu programa.
Como a enforce de bounds funciona no nível de instruction
Aqui está como um buffer overflow típico se parece em C:
#include <string.h>
void vulnerable(char *input) {
char buf[64];
strcpy(buf, input); // No bounds check. Classic overflow.
}
Em uma arquitetura normal, isso corrompe o stack. Em CHERI, buf não é um raw address. É uma capability cujos bounds são exatamente 64 bytes. Quando strcpy tenta escrever além do byte 64, o hardware levanta uma exceção de Capability Bounds Violation. O programa crasha imediatamente na instruction ofensora exata.
A mesma proteção se aplica a alocações 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
}
O crash é preciso. Você obtém a instruction exata, a capability exata e os bounds exatos que foram violados. Depurar isso é mais fácil do que perseguir um stack frame corrompido três frames depois.
O CHERI também previne a pointer forgery. Você não pode castear um integer para pointer e começar a dereferenciar memória arbitrária. O hardware só reconhece capabilities criadas por instructions legítimas como malloc, stack allocation ou derivação explícita de capability. Casts de integer para pointer produzem valores untagged que trapam ao serem usados.
O problema de compatibilidade é real
Se o CHERI é tão bom, por que cada servidor não está rodando nele? Porque mudar o que é um pointer quebra suposições nas quais décadas de código C dependem.
O primeiro problema é o tamanho. Capabilities são maiores que raw pointers. Em Morello, uma capability é 128 bits mais uma tag de 1 bit armazenada separadamente pelo hardware. Isso aumenta o uso de memória para estruturas de dados cheias de pointers. Uma linked list ou uma árvore cheia de pointers fica visivelmente maior. Para muitas aplicações o overhead é de um dígito percentual, mas para workloads de pointer-chasing pode ser pior.
O segundo problema é o casting. Código C casta pointers para uintptr_t, faz aritmética e casta de volta. Em CHERI, uintptr_t é na verdade um tipo capability, não um integer. Código que assume que pode fazer matemática de integer arbitrária sobre valores de pointer falhará em compilar ou trapará em runtime. A solução geralmente é usar ptrdiff_t para offsets e aplicá-los com cheri_address_set, mas isso requer mudanças no source.
O terceiro problema é o ecossistema. Hardware CHERI é raro. Boards Arm Morello existem, mas não são commodity servers. CheriBSD e Linux habilitado para CHERI são maduros o suficiente para rodar software real, mas a maioria das distribuições Linux não envia packages CHERI. Seu container registry, seus CI runners e seu cloud provider ainda não suportam isso.
Você pode experimentar hoje sem silicon customizado
Você não precisa de uma board Morello para experimentar com CHERI. O projeto CHERI mantém suporte a QEMU, então você pode rodar um userspace CHERI em qualquer host Linux ou macOS.
Aqui está como obter uma toolchain CHERI e rodar um exemplo simples no 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
Uma vez dentro do CheriBSD, o compiler é clang com suporte a target CHERI. Você compila com bounds checking enforceado por padrão:
$ clang -o test test.c
$ ./test
# Buffer overflows trap immediately with a clear message
Se você quiser testar software existente, o cheribuild pode compilar packages populares do FreeBSD ports com suporte a CHERI. O time CHERI portou OpenSSH, nginx, PostgreSQL e grandes partes do sistema base do FreeBSD. Muitos programas funcionam sem mudanças. Os que quebram geralmente o fazem por causa de casts inseguros de pointer ou suposições sobre o tamanho de pointer.
Para um início mais rápido sem QEMU, a University of Cambridge também publica uma imagem Docker com a toolchain LLVM do CHERI pré-instalada. Você pode compilar binários CHERI e inspecionar o assembly capability-aware gerado sem bootar um OS completo.
O que a hardware memory safety significa para codebases existentes
O CHERI não é um substituto para escrever código seguro. É uma rede de segurança para o código que você já tem. Um sistema CHERI rodando C legacy ainda terá logic bugs, race conditions e erros de use-after-free. Mas não terá buffer overflows que sobrescrevam return addresses, corrompam metadata adjacente de heap ou vazem secrets além dos limites de alocação.
A progressão já é visível. A Arm incorporou features derivadas do CHERI em sua Memory Tagging Extension (MTE), que está sendo enviada hoje em dispositivos Android de produção. O MTE é mais grosseiro que o CHERI (ele tagga grânulos de 16 bytes em vez de alocações individuais), mas pega muitos dos mesmos bugs com menos overhead. O CHERI completo provavelmente seguirá à medida que o ecossistema amadurece.
Se você mantém código em C ou C++ que processa untrusted input, a pergunta não é se a hardware memory safety vai chegar. É se você estará pronto quando ela chegar. Comece auditando seu código por casts de pointer para integer, aritmética de pointer em objetos não relacionados e suposições sobre sizeof(void*). Esses são os patterns que quebram sob CHERI, e consertá-los deixa seu código mais limpo mesmo em hardware convencional.
O hardware finalmente está disposto a dizer não. Deveríamos deixá-lo.