Setenta por cento dos CVEs são bugs de memory safety. Buffer overflows, use-after-free, double frees. O tipo de vulnerabilidades que permitem a um atacante pivotar de um JPEG malformado até acesso root.
Sabemos como impedir a maioria desses em hardware desde 1975. O computador Cambridge CAP usava endereçamento baseado em capabilities. A Hydra da Carnegie Mellon também. A ideia era simples: cada acesso à memória carrega sua própria permissão. O hardware a impõe. Sem permissão, sem acesso.
E então o hardware de capabilities passou as próximas quatro décadas perdendo para os modelos de memória plana. É apenas agora, com a placa Morello da ARM e o conjunto de instruções CHERI, que as arquiteturas de capabilities estão protagonizando um retorno credível.
A pergunta não é se as capabilities funcionam. Funcionam. A pergunta é por que levou quarenta anos para a indústria se importar.
Um capability pointer é um ticket infalsificável, não apenas um endereço
Um ponteiro normal é um inteiro. Um endereço. Não tem comprimento, nem permissões, nem procedência. Se você consegue adivinhar um endereço, consegue acessá-lo. Isso não é uma característica. É um bug de design que C herdou do PDP-11.
Um capability pointer, em contraste, é uma tupla imposta por hardware. Contém o endereço virtual, a base e o comprimento da região autorizada, permissões de leitura/escrita/execução, e uma tag de validade de 1 bit. A tag é armazenada fora da memória normal, em metadados de hardware dedicados, o que torna os capabilities infalsificáveis. Você não pode construir um capability válido a partir de um inteiro. Só pode recebê-lo de algo que já tinha autoridade.
No CHERI, isso se parece com um ponteiro de 128 bits em uma máquina de 64 bits. Os 64 bits extras carregam bounds e permissões. O bit de tag reside em uma tabela shadow ou em bits ECC sobressalentes, dependendo da implementação.
Aqui está como a restrição de um capability se parece na prática em um sistema CHERI:
#include <cheriintrin.h>
#include <stdio.h>
int main(void) {
char buffer[64];
// In the purecap ABI, &buffer[0] is already a capability
// carrying 64-byte bounds and read/write permissions.
char *cap = buffer;
printf("Original length: %zu\n", cheri_length_get(cap));
// Narrow authority to the first 16 bytes.
char *narrow = cheri_bounds_set(cap, 16);
printf("Narrowed length: %zu\n", cheri_length_get(narrow));
// This access is safe and hardware-authorized.
narrow[15] = 'x';
// This would trap with a CHERI bounds violation:
// narrow[20] = 'y';
return 0;
}
Se um atacante corrompe um capability pointer, o bit de tag é apagado. O hardware levanta uma exceção na próxima desreferenciação. A cadeia de exploração morre no primeiro salto.
Esse é um modelo de segurança radicalmente melhor que ASLR e stack canaries, que são apenas lombadas de velocidade.
O iAPX 432 envenenou o poço para uma geração
Então por que passamos quarenta anos fingindo que ponteiros eram apenas inteiros?
A Intel lançou o iAPX 432 em 1981. Ele tinha capabilities de hardware, segmentos de memória orientados a objetos e até garbage collection assistido por hardware. Ele também era aproximadamente cinco a dez vezes mais lento que o 8086, enormemente complexo e incompatível com cada compiler existente.
A IBM teve mais sorte com o System/38, que usou capabilities para seu single-level store. Funcionou. Foi estável. Também era um sistema proprietário de médio porte sem caminho para o ecossistema aberto emergente. Quando a revolução UNIX chegou, trouxe espaços de endereço planos, ponteiros de C e a suposição de que um ponteiro cabe em um registrador. Essa suposição se tornou a ABI de toda a indústria.
Na década de 1990, o hardware de capabilities era uma curiosidade de pesquisa. O imperativo comercial era compatibilidade retroativa e performance bruta. Memory safety era uma preocupação a nível de linguagem, se é que era. C e C++ simplesmente a declararam como problema do programador.
Isso funcionou bem o suficiente até a Internet tornar cada buffer overflow explorável remotamente.
O modo híbrido do CHERI quebra o deadlock de compatibilidade
O hardware de capabilities moderno, especificamente o CHERI, aprendeu a lição do iAPX 432. Ele não força uma migration de tudo ou nada.
O CHERI suporta um ABI híbrido onde ponteiros de inteiros legacy e capability pointers coexistem no mesmo processo. Você pode compilar apenas seu parser de rede ou sua biblioteca sandboxed com capabilities, enquanto o resto da sua aplicação usa ponteiros normais. O sistema operacional (CheriBSD, ou o port de Linux CHERI) gerencia a transição.
Isso importa porque a barreira real nunca foi a área de silício. Era a inércia do software. Existem bilhões de linhas de C e C++ que assumem sizeof(void *) == 8. Uma arquitetura de capabilities que exige reescrever todas elas está morta na chegada. Uma que pode ser adotada incrementalmente tem uma chance.
O mecanismo de hardware é elegante. Loads e stores conscientes de capabilities usam instruções dedicadas que verificam a tag e os bounds em paralelo com a tradução de endereços. Ponteiros de inteiros evitam as verificações inteiramente. Você paga pela segurança apenas onde a usa.
O custo real é a pressão de TLB e o inchaço de ponteiros, não a contagem de ciclos
Capabilities não são gratuitas. O overhead cai em três baldes.
Primeiro, tamanho do ponteiro. Em um ABI purecap, cada ponteiro é de 128 bits. Isso dobra a pressão de cache para estruturas de dados pointer-heavy. Listas encadeadas, árvores e vtables ficam todas mais gordas.
Segundo, granularidade de bounds. O CHERI impõe bounds com granularidade de byte, o que significa que o subsistema de memória deve verificar bounds a cada desreferenciação. A verificação em si é rápida, mas capabilities de grão fino podem aumentar a pressão de TLB e cache se você criar muitas regiões pequenas protegidas.
Terceiro, e mais importante, mudanças de software. compilers devem gerar prólogos conscientes de capabilities. ABIs mudam. Allocators de memória devem retornar capabilities limitadas em vez de ponteiros raw. Debuggers precisam entender registradores de 128 bits.
O overhead medido em runtime em hardware CHERI está tipicamente na faixa de um único dígito percentual para a maioria das cargas de trabalho, e até dez a quinze por cento para benchmarks pointer-heavy. Isso é menos que o overhead de muitas mitigações modernas como memory tagging ou sandboxing, e proporciona garantias mais fortes.
O trade-off não é performance. O trade-off é agitação do ecossistema.
Você pode executar isso hoje em uma placa de desenvolvimento
Se você quer experimentar hardware de capabilities, não precisa de uma máquina do tempo ou de uma bolsa de pesquisa.
A placa Morello da ARM implementa extensões CHERI em um núcleo Neoverse N1. O CheriBSD roda nela out of the box. Se você não tem o hardware, o QEMU-CHERI emula a arquitetura de capabilities completa.
A toolchain é LLVM padrão. Você compila com cheri-clang e a flag -march=morello+cheri. Existem ports de FreeBSD e Linux. O assembler integrado, linker e debugger do LLVM entendem todos capabilities.
Comece com uma única função. Envolver um parser ou uma rotina de desserialização em um capability restrito. Execute seu fuzzer contra ele. Quando uma escrita fora de bounds teria corrompido o heap, você receberá uma exceção de hardware limpa em vez disso.
O hardware de capabilities não é uma correção teórica para memory safety. É uma tecnologia funcional e embarcada que a indústria ignorou por quarenta anos porque os incentivos estavam errados. A memória plana foi mais rápida de construir, mais fácil de portar e boa o suficiente até que a exploração remota se tornasse o threat model padrão.
O silício funciona. Os compilers funcionam. Os sistemas operacionais funcionam. A única coisa que resta é decidir quais partes da sua codebase valem a pena proteger primeiro.