Você tem uma codebase em C que é grande demais para ser rewrite em Rust e crítica demais para ser deixada vulnerável a buffer overflows. O hardware com capabilities do CHERI promete detectar violações de segurança de memória no nível da CPU, mas a internet insiste em dizer que os ponteiros do CHERI têm 128 bits e seu código assume 64.
A boa notícia é que o CHERI não impõe uma migração de tudo ou nada. O ABI híbrido permite que você compile código em C existente com mudanças mínimas, execute em hardware CHERI real e aperte a segurança incrementalmente onde mais importa. Você não precisa portar um milhão de linhas de código antes de obter seu primeiro ponteiro protegido.
Por que o porting incremental é possível agora
As primeiras arquiteturas de capabilities eram intransigentes. O iAPX 432 exigia que todo ponteiro fosse uma capability, sem exceções. Isso quebrou todos os compiladores e sistemas operacionais existentes, e o projeto fracassou.
O CHERI aprendeu com aquele fracasso. Ele oferece suporte a três modos de compilação: baseline (apenas ponteiros inteiros legados), híbrido (ponteiros inteiros e de capability coexistem) e purecap (todo ponteiro é uma capability). O ABI híbrido é a ponte. Ele permite que você mantenha seu código existente praticamente inalterado enquanto introduz capabilities seletivamente onde eles agregam mais valor.
No modo híbrido, void* ainda tem 64 bits. Um ponteiro de capability é um tipo distinto, void* __capability, que você adota explicitamente. Suas structs, suas listas encadeadas e suas tabelas hash não mudam de tamanho a menos que você peça. Isso não é uma camada de compatibilidade. É um ABI de primeira classe suportado pelo hardware e pelo compilador.
O que de fato quebra quando você compila para CHERI híbrido
O primeiro passo é tentar compilar seu código com uma toolchain do CHERI e ver o que explode. A maior parte do código bem comportado em C compila sem alterações. Os problemas se concentram em um punhado de padrões que o CHERI torna ilegais, que é exatamente o ponto.
Conversões de ponteiro para inteiro. Código que converte um ponteiro para uintptr_t, mascara alguns bits e converte de volta vai falhar. No CHERI, uintptr_t é um tipo de capability, não um inteiro simples. Operações bit a bit em capabilities removem o bit de tag, e o valor resultante gera uma trap no momento do dereference.
Suposições sobre o tamanho do ponteiro. Código que codifica sizeof(void*) == 8 ou serializa ponteiros como valores de 8 bytes vai quebrar no modo purecap. No modo híbrido isso funciona na maioria dos casos, mas se torna um problema no momento em que você mistura ponteiros de capability e inteiros na mesma struct.
Aritmética de ponteiros entre objetos não relacionados. Programadores em C às vezes calculam a distância entre dois ponteiros arbitrários ou comparam ponteiros de alocações diferentes. As capabilities do CHERI carregam metadados de limites, e subtrair capabilities de alocações diferentes é indefinido. O compilador vai rejeitar ou o hardware vai gerar uma trap.
Assembly inline. Qualquer assembly escrito à mão que move ponteiros em registradores inteiros precisará ser atualizado. O CHERI possui registradores de capability dedicados, e o compilador precisa saber quais deles você está tocando.
A toolchain LLVM do CHERI oferece diagnósticos excelentes. Você não recebe erros crípticos do linker. Você recebe mensagens claras como “cast from capability to integer is not allowed” ou “arithmetic on capabilities from different allocations.” Corrija o primeiro erro, recompile e persiga o próximo.
Um exemplo concreto: envolvendo um parser com capabilities
Imagine que você tem um parser de pacotes de rede que lê entrada não confiável em um buffer e depois analisa cabeçalhos. Esse é exatamente o tipo de código que se beneficia da verificação de limites em hardware. Veja como envolvê-lo sem fazer rewrite do parser em si.
Primeiro, compile o parser no modo híbrido. A toolchain do CHERI é o LLVM padrão com um target CHERI:
# Compila um único arquivo com o ABI híbrido
$ clang --target=riscv64-unknown-freebsd \
-march=rv64imafdcxcheri \
-mabi=lp64d \
-mno-relax \
-c parser.c -o parser.o
A flag -mabi=lp64d mantém os ponteiros inteiros com 64 bits. Seus tipos existentes void* e char* permanecem inalterados. O parser compila como está.
Agora adicione um wrapper com awareness de capability em um arquivo separado:
#include <cheriintrin.h>
#include <stddef.h>
#include <stdint.h>
// Parser existente de parser.c
extern int parse_packet(const char *data, size_t len);
// Ponto de entrada com awareness de capability
int parse_packet_safe(const char * __capability data, size_t len) {
// Restringe a capability ao tamanho exato do buffer.
// Mesmo que o chamador tenha passado uma alocação maior,
// o parser não pode ler além de 'len' bytes.
const char * __capability narrowed =
cheri_bounds_set(data, len);
// No modo híbrido, passamos um ponteiro simples para o parser legado.
// O compilador insere uma conversão de capability para inteiro aqui.
// Se a capability não couber em 64 bits (não caberá para
// endereços de capability grandes), isso gerará um warning ou erro.
//
// Para a migração para purecap, você modificaria parse_packet
// para aceitar um ponteiro de capability.
return parse_packet((const char *)narrowed, len);
}
Neste exemplo, parse_packet ainda usa ponteiros inteiros legados. O wrapper restringe a capability do chamador ao tamanho exato da entrada e depois converte de volta. A etapa de restrição significa que, mesmo que o chamador acidentalmente passe um buffer de 4KB quando pretendia passar 64 bytes, o hardware vai impor o limite de 64 bytes na capability restringida.
Isso não é o estado final. É um degrau. Você obtém bounds enforcement na fronteira da API hoje, e pode migrar parse_packet para purecap em uma refactor posterior.
O caminho de migração do híbrido para o purecap
O modo híbrido é um ponto de partida, não um destino. Ele oferece compatibilidade, mas não o benefício completo de segurança das capabilities. O objetivo de longo prazo é o purecap, onde cada ponteiro carrega limites.
A migração prática se parece com isso:
-
Compile no modo híbrido e corrija os erros de build. Isso geralmente significa corrigir conversões de ponteiro para inteiro e suposições sobre
sizeof(void*). Não altere suas estruturas de dados ainda. Apenas obtenha um build limpo. -
Identifique alvos de alto valor. Parsers de rede, decodificadores de formatos de arquivo e rotinas de desserialização são os melhores candidatos para capability enforcement. Eles processam entrada não confiável e é onde a maioria dos bugs de segurança de memória reside.
-
Envolva esses alvos com capabilities restritas. Use
cheri_bounds_setpara criar capabilities restritas nas fronteiras de confiança. Passe-os para seu código existente. -
Migre funções folha para purecap. Comece com funções utilitárias que alocam e retornam ponteiros. Altere suas assinaturas para usar
__capabilitye compile-as com-mabi=purecap. Suba pela pilha de chamadas. -
Eventualmente, mude todo o module para purecap. Quando uma unidade de compilação não tiver mais ponteiros inteiros, compile-a com
-mabi=purecape link-a com o restante do seu código híbrido. A toolchain do CHERI oferece suporte a linking com ABIs mistos.
Isso não é um projeto de fim de semana para uma codebase grande. Mas também não é um rewrite. Você pode proteger seus caminhos de código mais vulneráveis em dias, e migrar o restante incrementalmente conforme for tocando nele.
Como os trade-offs de fato se parecem
O ABI híbrido tem custos reais, e você deve conhecê-los antes de se comprometer.
Primeiro, ABIs mistos complicam seu build. Agora você tem arquivos objeto compilados com diferentes tamanhos de ponteiro no mesmo binário. O linker deve lidar com realocações de capability e inteiro. A toolchain do CHERI oferece suporte a isso, mas seu sistema de build provavelmente ainda não sabe disso. Você precisará ensiná-lo.
Segundo, a conversão de capability para inteiro nas fronteiras híbridas é com perdas. Se você restringe uma capability e depois a converte para um ponteiro simples, você perde os limites. A memória subjacente ainda está protegida pela capability com a qual você começou, mas a função legada não recebe hardware enforcement. É por isso que o purecap é o objetivo: cada ponteiro na cadeia de chamadas carrega seus próprios limites.
Terceiro, a depuração muda. O GDB no CHERI entende registradores de capability e pode imprimir metadados de limites. O suporte ao LLDB está melhorando. Se seu fluxo de trabalho de depuração atual depende de inspecionar valores brutos de ponteiros, você precisará aprender os nomes dos registradores do CHERI.
A sobrecarga de desempenho do modo híbrido geralmente é negligenciável para código que usa principalmente ponteiros inteiros. A sobrecarga do purecap tipicamente é de dígitos únicos de porcentagem para a maioria das cargas de trabalho, subindo para 10-15% para estruturas de dados com muitos ponteiros. Isso é competitivo com mitigações de software como ASAN, e diferente do ASAN, o CHERI roda em velocidade total em produção.
Você pode começar hoje com o QEMU
Você não precisa de uma placa Morello para experimentar. O projeto CHERI mantém suporte ao QEMU e imagens Docker com a toolchain LLVM pré-instalada.
# Baixa a imagem Docker da toolchain do CHERI
$ docker run --rm -it ctsrd/cheri-sdk:latest
# Dentro do container, compile seu código para RISC-V CHERI
$ clang --target=riscv64-unknown-freebsd \
-march=rv64imafdcxcheri \
-mabi=lp64d \
-o myapp myapp.c
Comece compilando seu projeto no modo híbrido e contando os erros. O número vai lhe dizer quanto trabalho existe pela frente. Um build limpo na primeira tentativa é raro, mas não impossível para C moderno e compatível com os padrões. Algumas centenas de erros é típico para bases de código mais antigas com muitas conversões de ponteiros.
Corrija os erros nesta ordem: conversões de inteiro para ponteiro primeiro, depois aritmética de ponteiros entre alocações, e por último assembly inline. Cada categoria tem uma correção mecânica. O projeto CHERI publica um guia de porting com exemplos de antes e depois para os padrões mais comuns.
Hardware com capabilities não é um futuro teórico. É uma toolchain funcional, um ABI suportado e um caminho de migração que não exige queimar sua codebase no chão. Comece com um arquivo, uma função, uma capability restrita. O hardware fará o resto.