Discussões de segurança muitas vezes se concentram em bugs individuais:

  • um estouro de buffer
  • uma falha de deputado confuso
  • um parser sem verificações suficientes
  • um caminho de escalonamento de privilégios

Esses bugs importam, mas são sintomas de uma pergunta mais profunda:

Quanto código precisa estar correto para que o sistema continue seguro?

Esse código é a base de computação confiável, normalmente abreviada como TCB.

O tamanho e a forma da TCB determinam quanto código precisa ser confiado, auditado, testado e compreendido. Um sistema pode ter abstrações fortes no papel, mas se elas dependem de uma grande quantidade de código privilegiado se comportando perfeitamente, o argumento de segurança fica muito mais fraco.

Este post explica o que é a TCB, por que seu tamanho afeta a superfície de ataque e como o EriX tenta manter código confiável pequeno e explícito.


O que é a TCB?

A base de computação confiável é o conjunto de componentes cuja correção é necessária para que as propriedades de segurança do sistema continuem valendo.

Se um componente da TCB for comprometido, as garantias de segurança do sistema podem deixar de ser verdadeiras.

Em um sistema operacional, a TCB frequentemente inclui:

  • o bootloader
  • o kernel
  • serviços privilegiados do sistema
  • lógica de autenticação e autorização
  • parsers de dados confiáveis de boot
  • código de verificação criptográfica
  • código que distribui ou transfere autoridade

A TCB exata depende do projeto do sistema.

Em um kernel monolítico tradicional, grande parte do kernel costuma fazer parte da TCB porque muitos subsistemas rodam com privilégio total de kernel. Um bug em um driver, sistema de arquivos ou stack de rede pode virar comprometimento do kernel.

Em um sistema microkernel, a TCB do kernel pode ser menor, mas o sistema confiável total ainda inclui os componentes que distribuem autoridade e aplicam política.

Essa distinção importa.

Microkernels reduzem a quantidade de código que roda com autoridade total sobre a máquina. Eles não tornam todos os serviços de espaço de usuário não confiáveis por padrão.


Confiável não significa seguro

A palavra “confiável” é fácil de entender errado.

Um componente confiável não é um componente que sabemos estar correto.

É um componente do qual o sistema depende para estar correto.

Essa definição é menos confortável, mas é a útil.

Se um serviço é confiável para distribuir capacidades, um bug nesse serviço pode conceder autoridade incorretamente. Se um parser é confiável para validar um executável antes do boot, um bug nele pode enfraquecer a cadeia de boot. Se um tratador de syscall do kernel é confiável para validar direitos de endpoints, uma verificação ausente pode quebrar o isolamento.

Confiança não é elogio.

Confiança é risco.

O objetivo, portanto, não é rotular o máximo de código possível como confiável. O objetivo é tornar o conjunto confiável tão pequeno, estreito e auditável quanto possível.


Por que o tamanho importa

O tamanho da TCB importa por várias razões.

1. Mais código significa mais bugs

Todo software tem bugs.

À medida que a quantidade de código confiável cresce, também cresce a probabilidade de bugs relevantes para segurança. Isso é especialmente verdadeiro para código que:

  • faz parsing de entrada não confiável
  • gerencia memória
  • lida com concorrência
  • interpreta permissões
  • traduz um modelo de autoridade em outro

Sistemas operacionais contêm todos esses padrões.

Reduzir o tamanho da TCB não elimina bugs, mas reduz a quantidade de código onde um bug pode comprometer o sistema inteiro.


2. Mais interfaces significam mais superfície de ataque

Superfície de ataque não é apenas número de linhas de código.

Também são pontos de entrada.

Cada interface para código confiável é um lugar onde um atacante pode fornecer entrada:

  • argumentos de syscall
  • mensagens IPC
  • metadados de imagem de boot
  • cabeçalhos ELF
  • estruturas de sistemas de arquivos
  • descritores de dispositivos
  • eventos de interrupção
  • tabelas fornecidas pelo firmware

Cada interface precisa de validação.

Um componente confiável pequeno com uma interface mal projetada ainda pode ser perigoso. Mas quando o número de interfaces confiáveis cresce, a carga de validação cresce junto.


3. Mais estado torna o raciocínio mais difícil

Falhas de segurança muitas vezes não acontecem porque uma única verificação está faltando isoladamente, mas porque o estado muda em uma ordem inesperada.

Por exemplo:

  • uma capacidade é copiada antes de seus direitos serem reduzidos
  • um serviço inicia antes de seu pacote de startup ser totalmente validado
  • um slot local antigo é tratado como prova de autoridade
  • um dispositivo é considerado presente antes de a descoberta terminar
  • um processo mantém autoridade depois de uma rota de lançamento falhar

Quanto mais estado mutável confiável um sistema tem, mais difícil fica provar que cada transição preserva as invariantes pretendidas.

É por isso que o EriX enfatiza startup determinístico, registros explícitos de transferência e comportamento fail-closed.


4. Mais privilégio significa maior raio de impacto

O mesmo bug tem consequências diferentes dependendo de onde ocorre.

Um bug de parsing em uma ferramenta sem privilégios pode derrubar essa ferramenta.

Um bug de parsing no bootloader pode comprometer o sistema inteiro antes de o kernel iniciar.

Um bug em um driver de espaço de usuário com acesso apenas a uma faixa específica de E/S é sério, mas é diferente de um bug em um driver que roda dentro do kernel com autoridade total sobre a máquina.

Reduzir a TCB também é reduzir o raio de impacto de bugs individuais.


O kernel é apenas parte da TCB

É tentador dizer:

A TCB é o kernel.

Isso geralmente é simples demais.

O kernel é central, mas um sistema seguro também depende do código que prepara o kernel, inicia a primeira tarefa de espaço de usuário, define formatos de autoridade e distribui capacidades.

No EriX, o kernel está explicitamente na TCB.

Ele possui tabelas de capacidades do espaço do kernel, estado de escalonamento e recursos da máquina. Ele valida a passagem do bootloader para o kernel, cria a tarefa root, gerencia objetos básicos do kernel e expõe pontos de entrada de trap, syscall e interrupção específicos da arquitetura.

Se o kernel falhar ao aplicar verificações de capacidades, direitos de endpoints, limites de espaços de endereçamento ou ciclos de vida de objetos, o modelo de isolamento do sistema falha.

Mas o kernel não é a história inteira.


Código de boot também é confiável

O bootloader roda antes do kernel.

Isso o torna crítico para a segurança.

No EriX, o bootloader é responsável por carregar e verificar um boot.img assinado, fazer parsing das imagens do kernel e dos serviços, construir uma estrutura de handoff determinística e transferir controle ao kernel.

Isso coloca o bootloader na TCB.

Ele possui autoridade fornecida pelo firmware durante o boot e controla o salto final para o kernel. Ele deve tratar a mídia de boot como não confiável, validar estrutura e integridade criptográfica da imagem, rejeitar binários ELF malformados e falhar fechado diante de ambiguidades.

Se o bootloader aceitar uma imagem adulterada ou construir um handoff inconsistente, o kernel pode começar a execução a partir de uma base comprometida.

Por isso código de boot deve ser pequeno, estrito e entediante.


Parsers podem ser fronteiras de TCB

Parsers são frequentemente subestimados em segurança de sistemas.

Eles ficam exatamente onde bytes não confiáveis viram estrutura confiável.

O EriX trata vários crates de parsing e ABI como componentes da TCB ou adjacentes à TCB:

  • lib-bootimg faz parsing e verifica estrutura, hashes e assinaturas de boot.img
  • lib-elf valida binários ELF64 antes de o bootloader confiar em segmentos de carga
  • lib-handoff valida estruturas de handoff versionadas entre estágios de boot
  • lib-ipc define e valida layouts de mensagens IPC
  • lib-capabi define tipos de capacidades, direitos, constantes de slots e descritores de transferência

Essas bibliotecas podem não possuir capacidades de runtime por si mesmas.

Isso não as torna irrelevantes para a TCB.

Se lib-bootimg aceitar uma imagem de boot modificada, o bootloader pode confiar em código que deveria rejeitar. Se lib-elf aceitar um executável malformado, a cadeia de boot pode carregar bytes errados ou confiar em faixas de segmentos inválidas. Se lib-ipc decodificar uma mensagem incorretamente, uma operação pode ser interpretada de forma errada. Se lib-capabi definir uma política de papel ampla demais, um serviço pode receber autoridade que nunca deveria possuir.

Código puro ainda pode ser código confiável.

A propriedade importante é que essas bibliotecas são estreitas. Elas não fazem E/S, não possuem política do sistema e evitam autoridade ambiente. Seu trabalho é fazer parsing, validar e rejeitar.


Serviços de espaço de usuário podem ser componentes confiáveis

Mover política para fora do kernel não faz a política desaparecer.

Ela se move para serviços de espaço de usuário, onde pode ser isolada, restringida e auditada separadamente.

No EriX, rootd é a primeira autoridade de espaço de usuário que carrega política. Ele valida o handoff do kernel para root, faz parsing da configuração de boot, executa o DAG de startup e transfere capacidades de menor privilégio aos serviços necessários.

rootd é altamente privilegiado.

Mas ele não é o kernel.

Essa distinção é importante. rootd é confiável para política inicial e distribuição de capacidades, mas não implementa objetos do kernel nem possui autoridade direta sobre a máquina. Seu trabalho é distribuir autoridade de acordo com contratos explícitos de startup.

Outros serviços também ficam dentro de fronteiras confiáveis específicas:

  • procd é confiável para orquestração do ciclo de vida de processos
  • deviced é confiável para política de drivers e entrega de capacidades de drivers
  • vfsd é confiável como fronteira do namespace público do sistema de arquivos
  • provedores privados de sistemas de arquivos são confiáveis apenas para seu papel de provedor

Isso não torna esses serviços sem importância.

Torna sua autoridade mais estreita do que a autoridade total do kernel.


Superfície de ataque em um projeto monolítico

Em um kernel monolítico, muitos subsistemas compartilham um único espaço de endereçamento privilegiado.

Isso pode simplificar caminhos rápidos, mas também cria uma grande superfície de ataque.

Uma vulnerabilidade em qualquer subsistema dentro do kernel pode virar uma vulnerabilidade do kernel:

  • metadados de sistema de arquivos malformados
  • manipulação defeituosa de pacotes de rede
  • código inseguro de drivers
  • descritores de hardware inesperados
  • condições de corrida em estado compartilhado do kernel

O atacante precisa de apenas um caminho para código privilegiado.

Isso não significa que kernels monolíticos não possam ser seguros. Eles podem ser projetados, endurecidos, fuzzados, sandboxados e auditados extensivamente.

Mas a arquitetura começa com uma grande superfície privilegiada.

O argumento de segurança então precisa explicar como essa grande superfície é controlada.


Superfície de ataque em um projeto microkernel

Um microkernel muda a forma da superfície de ataque.

O kernel ainda expõe interfaces críticas:

  • syscalls
  • entrega IPC
  • escalonamento
  • operações de espaço de endereçamento
  • operações de capacidades
  • tratamento de interrupções

Essas interfaces precisam estar corretas.

Mas serviços de nível mais alto não rodam automaticamente com privilégio total de kernel. Se um provedor de sistema de arquivos manipula mídia malformada, o objetivo é que o bug fique dentro da autoridade desse provedor. Se um driver falha, o objetivo é que ele falhe apenas com a autoridade de dispositivo que recebeu explicitamente.

Isso transforma uma grande superfície privilegiada em várias superfícies de autoridade menores.

Isso não é automaticamente mais simples.

Só funciona se as fronteiras forem rígidas e a autoridade passada por elas for estreita.


Como o EriX minimiza a TCB

O EriX reduz o tamanho da TCB e a superfície de ataque por meio de várias decisões de projeto.

1. Um kernel mínimo em política

O kernel do EriX é responsável por mecanismos, não por política do sistema.

Ele lida com:

  • objetos básicos do kernel
  • semântica de capacidades
  • escalonamento e execução de tarefas
  • primitivas de espaços de endereçamento
  • IPC e despacho de endpoints
  • pontos de entrada de interrupções e exceções

Ele não possui:

  • política de startup de serviços
  • política de orquestração de processos
  • política de namespace de sistemas de arquivos
  • política de ativação de drivers
  • política de alocação de memória de alto nível

Isso mantém o kernel focado em aplicar isolamento em vez de decidir o formato de todo o sistema.


2. Capacidades explícitas em vez de autoridade ambiente

O EriX modela autoridade por meio de capacidades.

Um componente só pode agir se possui uma capacidade com os direitos exigidos. O kernel valida referências de capacidades no uso, aplica direitos de endpoints no despacho de syscalls e trata transferências como eventos explícitos.

Isso evita depender de nomes globais ou números convencionais de slot como permissão.

Conhecer um número de slot não é autoridade.

Possuir a capacidade correta no CSpace local é autoridade.

Essa distinção é central para reduzir a TCB: código confiável não precisa inferir permissões a partir de estado global quando a autoridade é carregada de forma explícita.


3. Endpoints estreitos de controle do kernel

Projetos anteriores de sistemas operacionais frequentemente concentram controle atrás de interfaces privilegiadas amplas.

O EriX segue na direção oposta.

O runtime atual usa famílias estreitas de endpoints de controle do kernel para trabalhos específicos:

  • tempo
  • interrupções
  • hotplug
  • configuração PCI
  • console e framebuffer
  • E/S COM1
  • E/S i8042
  • retyping de memória
  • mapeamento de VSpace
  • resolução de falhas do pager
  • controle de processos
  • leituras ACPI

O boot normal de runtime não entrega aos serviços um endpoint root amplo para todas as operações de kernel.

Em vez disso, cada serviço recebe a família de endpoint específica de que precisa. timed recebe controle de tempo. irqd recebe controle de interrupções. drv-serial recebe E/S específica de COM1. drv-i8042 recebe E/S específica de i8042. drv-acpi recebe autoridade de leitura ACPI.

Isso estreita tanto a interface confiável quanto o dano causado por mau uso.


4. Contratos exatos de startup

O EriX trata transferência de capacidades de startup como contrato, não como sugestão.

Envelopes de startup descrevem quais capacidades um serviço deve receber, onde elas devem aparecer e quais direitos devem carregar.

Serviços validam os slots locais reais que receberam antes de declarar prontidão. Transferências de endpoints são verificadas por slot de origem, slot de destino, direitos e tipo esperado de endpoint. Transferências desconhecidas, erradas ou extras são rejeitadas.

Isso evita um bug comum de autoridade:

“Um slot está ocupado, então deve ser a coisa certa.”

No EriX, ocupação de slot por si só não é prova de autoridade.

A capacidade precisa corresponder ao formato de autoridade declarado.


5. Criação de processos em etapas

Criação de processos é uma operação de alto risco porque combina execução, espaços de endereçamento, endpoints e autoridade inicial.

O EriX roteia criação de processos em runtime por criação de filhos em etapas em vez de uma operação direta legada.

O fluxo em etapas é explícito:

  1. criar um filho em etapa
  2. receber um grant de instalação e um alias de endpoint do filho
  3. instalar o pacote declarado de capacidades de startup
  4. iniciar o processo somente depois de a população estar completa

O kernel nega o início do processo enquanto grants de instalação vivos ainda apontam para aquela etapa de filho.

Isso impede que processos parcialmente populados se tornem executáveis com um estado de autoridade ambíguo.


6. Autoridade de drivers específica por papel

Drivers são uma grande fonte de risco em sistemas operacionais.

O EriX não trata todo controle de hardware como uma permissão ampla única.

A autoridade de drivers é específica por papel:

  • drv-serial recebe autoridade de E/S apenas para COM1
  • drv-i8042 recebe autoridade de E/S apenas para i8042
  • drv-acpi recebe autoridade de leitura ACPI
  • probed recebe autoridade de leitura de configuração PCI
  • drv-virtio-block recebe um frame de dispositivo validado em vez de autoridade geral de memória

deviced gerencia a política de drivers, mas não entrega simplesmente a cada driver uma capacidade genérica de controle de dispositivos. Ele usa instalação explícita de capacidades de startup e superfícies de autoridade específicas por papel.

Isso limita o impacto de bugs de drivers na TCB.


7. Memória de dispositivo é um tipo separado de capacidade

Memória de dispositivo é perigosa porque pode afetar diretamente o estado do hardware.

O EriX distingue memória de dispositivo de RAM comum com CAP_TYPE_DEVICE_FRAME.

O caminho de armazenamento pode derivar um frame MMIO baseado em BAR para deviced, e deviced pode instalar apenas esse frame de dispositivo derivado no pacote de startup de um driver.

Esse frame de dispositivo não é tratado como memória alocável comum.

Isso importa porque confundir memória de dispositivo com frames normais ampliaria o modelo de autoridade e tornaria mais difícil raciocinar sobre segurança de memória.


8. Dependências de sala limpa

Dependências podem ampliar a TCB silenciosamente.

Se código confiável depende de uma biblioteca externa de propósito geral, o sistema pode herdar:

  • caminhos de código de que não precisa
  • suposições que não combinam com o OS
  • comportamento de parsing permissivo demais
  • risco de atualização e cadeia de suprimentos

O EriX evita crates de terceiros e implementa suas bibliotecas críticas dentro do projeto.

Isso aumenta o trabalho de implementação, mas mantém visível a fronteira confiável. Quando um parser, crate de ABI ou helper criptográfico faz parte do caminho de boot ou autoridade, ele faz parte da superfície de revisão do sistema.


9. Comportamento fail-closed

Uma TCB pequena não basta se as falhas são ambíguas.

O EriX tenta tornar falhas sensíveis para segurança explícitas e terminais:

  • handoff de boot malformado para o boot
  • versões não suportadas são rejeitadas
  • autoridade de startup inválida impede prontidão
  • tipos errados de endpoint falham na validação
  • falha de serviço requerido interrompe progressão
  • falhas após start disparam teardown fail-closed

Falhar fechado é importante porque heurísticas de recuperação frequentemente se tornam política oculta.

Política oculta expande o comportamento confiável do sistema.


Menor não significa trivial

Reduzir a TCB não torna o projeto do sistema fácil.

Frequentemente o torna mais explícito.

Em vez de colocar toda a lógica em um único espaço de endereçamento privilegiado, o sistema precisa definir:

  • qual componente possui cada decisão
  • qual autoridade cada componente recebe
  • como a autoridade é transferida
  • como falhas são relatadas
  • como startup parcial é limpo
  • como capacidades antigas são invalidadas ou descartadas

Isso dá mais trabalho no início.

Mas produz um sistema em que o argumento de segurança é mais fácil de inspecionar.

A pergunta se torna:

Qual componente precisa ser confiável para esta propriedade específica?

Essa é uma pergunta melhor do que:

O kernel inteiro está correto?


Uma visão prática da TCB do EriX

Uma visão prática da TCB do EriX parece em camadas.

Na base está a cadeia de boot:

  • bootloader
  • verificação da imagem de boot
  • caminho de leitura/verificação de lib-bootimg
  • parsing ELF
  • validação de handoff

Depois o kernel:

  • objetos de capacidades
  • aplicação de CSpace e VSpace
  • IPC e direitos de endpoints
  • escalonamento e tratamento de traps
  • entrega de interrupções

Depois o espaço de usuário confiável inicial:

  • rootd para política de startup e distribuição de capacidades
  • procd para ciclo de vida de processos
  • deviced para política de drivers
  • bibliotecas selecionadas de ABI e validação compartilhadas entre serviços

Depois serviços confiáveis mais estreitos:

  • mediação do namespace de sistemas de arquivos em vfsd
  • provedores backend privados
  • serviços de entrada, console, logging, bloco, tempo e interrupções

Nem todos esses componentes têm o mesmo privilégio.

Esse é o ponto.

O EriX tenta evitar um único mundo confiável plano. Em vez disso, cada componente deve ser confiável apenas para seu papel documentado e apenas com as capacidades que recebeu explicitamente.


Olhando adiante

A discussão sobre TCB leva naturalmente à linguagem de implementação.

Se código confiável precisa ser pequeno, explícito e auditável, então segurança de memória importa. Fronteiras unsafe, correção de parsers, layout de dados e a disciplina necessária para trabalhar perto do hardware também importam.

O próximo post examinará por que o EriX é escrito principalmente em Rust, o que Rust resolve e não resolve para desenvolvimento de kernel, e como se compara à abordagem tradicional em C para programação de sistemas.