Linux kernel revocable: o novo freio de mão contra UAF no kernel

Escrito por
Emanuel Negromonte
Emanuel Negromonte é Jornalista, Mestre em Tecnologia da Informação e atualmente cursa a segunda graduação em Engenharia de Software. Com 14 anos de experiência escrevendo sobre...

Quando o device some, o kernel não cai.

Sabe aquele frio na barriga quando você puxa um pen drive no momento errado e algo no sistema “dá ruim”? No kernel, o equivalente é bem mais sério: um use-after-free (UAF)—quando um pedaço de código ainda tenta usar um recurso que já foi liberado porque o dispositivo sumiu. É a receita clássica para crashes difíceis de reproduzir e CVEs nada divertidos.

Nas últimas semanas, surgiu uma proposta que mira exatamente esse problema, com um nome tão direto quanto a ideia: “revocable”. Ela introduz um pequeno (e engenhoso) alicerce de gestão de recursos revogáveis, construído sobre SRCU (Sleepable RCU), para que drivers façam acessos “fracos” a objetos que podem desaparecer a qualquer momento—e consigam verificar com segurança se ainda estão lá antes de usá-los.

O problema, sem rodeios

Em sistemas com hotplug (USB, mas não só), recursos podem “evaporar” enquanto alguém ainda tem um ponteiro para eles. Pense no cros_ec_chardev (o char device do ChromeOS EC): um processo mantém o arquivo aberto; o EC é removido; o ponteiro para a struct cros_ec_device vira lixo; na próxima ioctl()… boom, UAF.

zD1h6yZG image 1
Linux kernel revocable: o novo freio de mão contra UAF no kernel 3

A revocable entra aqui como um protocolo de gentileza entre quem fornece e quem consome um recurso. Em vez de o consumidor segurar um ponteiro “forte” (e potencialmente morto), ele passa a segurar uma alça revogável—uma espécie de weak reference que só vira um ponteiro utilizável dentro de um trecho protegido por SRCU. Se o provedor revogou o recurso, a tentativa de acesso retorna NULL, e o consumidor desiste de maneira limpa.

Como funciona, na prática

O design é minimalista:

  • Provedor: aloca um struct revocable_provider com o ponteiro real do recurso.
    Quando o dispositivo vai embora, o provedor chama algo como “revogar” (a série inicial usa revocable_provider_free(), já falo do nome), que zera o ponteiro interno e faz um synchronize_srcu()—esperando todos os leitores em andamento saírem da área segura antes de, de fato, desmontar tudo.
  • Consumidor: aloca um struct revocable ligado ao provedor. Para usar o recurso, chama revocable_try_access(): se o recurso ainda existe, entra numa seção de leitura SRCU e devolve o ponteiro; se já foi revogado, devolve NULL. Ao sair, o consumidor chama revocable_release() e pronto.

Para tornar o padrão difícil de errar, há um macro REVOCABLE(rev, res) que cria um bloco de uso único: você recebe res já validado (ou NULL), usa dentro do bloco, e a release acontece automaticamente ao sair do escopo—até em caminhos de erro. É literalmente um “abra-fecha com garantia”.

Por baixo dos panos, o revocable_provider mantém a SRCU (que permite dormir dentro da seção crítica—perfeito para caminhos de driver que não querem o custo/rigidez de um spinlock) e um kref simples para gerenciar o ciclo de vida da estrutura auxiliar. A semântica é a mesma que aprendemos a gostar em RCU: leituras baratas e uma sincronização explícita quando o lado escritor precisa mudar o mundo.

O caso-piloto: ChromeOS EC

A série de patches usa o cros_ec como “campo de provas”:

  • O device do EC passa a expor um revocable_provider que aponta para a própria struct cros_ec_device.
  • O cros_ec_chardev vira consumidor: toda vez que precisa falar com o EC, entra num bloco REVOCABLE(), pega a cros_ec_device* se existir, e segue; se não existir, retorna -ENODEV em vez de arriscar um UAF.
  • testes KUnit e kselftest cobrindo o básico (acesso válido), o caminho de revogação e o uso via macro. Ponto extra: explicar e testar bem esse tipo de primitivo vale ouro.

Entre a base e o topo: onde isso deve viver?

Aqui começou a parte animada do debate—como sempre, a melhor parte do kernel:

  • Greg Kroah-Hartman (Greg KH) gostou da abordagem (“é o que queríamos há anos”), elogiou a presença de testes e documentação, e basicamente disse: vamos em frente. Também brincou que podemos discutir o nome do macro, mas “quem escreve, batiza”—sinal verde para experimentar.
  • Danilo Krummrich (que implementou peça semelhante em Rust no kernel) trouxe duas provocações úteis:
    1. Nome da função de revogação: revocable_provider_free() soa como “liberar memória”, mas o que ela faz primeiro é revogar o recurso e só depois, quando o último consumidor se for, liberar. A sugestão é renomear para algo como *_revoke()—mais fiel à semântica.
    2. API simétrica com a versão em Rust: lá, existe um método estilo closure (tente-acessar-e-execute) que lembra muito o REVOCABLE(). O paralelismo ajuda a criar “memória muscular” de uso entre linguagens.
  • Bartosz Golaszewski puxou a discussão para cima: ótimo arrumar cros_ec, mas o problema é sistêmico. Será que essa base serve para I²C, GPIO (onde já existe uma solução custom baseada em SRCU), e outras camadas grandes? Em outras palavras: não queremos só um helper simpático—queremos um padrão reutilizável.
  • Laurent Pinchart foi além: por que não subir esse padrão para camadas de mais alto nível—por exemplo, o char device (cdev)? A corrida entre chamadas de syscalls e .remove() existe em diversos subsistemas (V4L2 que o diga). Se a lógica de “entrar/ sair da região segura” ficasse embutida nas rotas de cdev, drivers não precisariam “pintar” cada caminho manualmente; bastaria sinalizar a remoção e deixar o core bloquear novos acessos e drenar os em andamento. É um norte ambicioso: baixo nível disponível para casos especiais; alto nível fazendo o grosso.

O consenso que emerge—e eu concordo—é que a “Linux kernel revocable” deve existir como primitivo de fundação (drivers podem usá-la diretamente), mas a vitória grande virá quando subsystems (e, idealmente, cdev) a incorporarem, reduzindo a chance de cada driver reinventar seus próprios guard-rails.

Por que isso é importante?

Porque UAF por ciclo de vida desalinhado é uma dor antiga—e traiçoeira. Você pode despejar camadas de get/put e locks e ainda perder uma corrida. O “revocable” troca esse jogo por um contrato claro:

  • Consumidores usam o recurso dentro de uma seção onde o provedor garante observabilidade estável ou nega o acesso com NULL.
  • Provedores revogam e sincronizam sem medo de “puxar o tapete” de leitores invisíveis.

E como é SRCU, os consumidores podem dormir. Isso importa para caminhos de IO reais, não só para micro-benchmarks.

O que esperar a seguir

Minha aposta:

  1. Ajustes de UX da API—especialmente o *_free()*_revoke()—para evitar confusões mentais sobre “liberar vs. revogar”.
  2. Mais estudos de caso: I²C, GPIO, talvez TTY e blocos multimídia (V4L2/DRM) onde o padrão de “arquivo aberto + dispositivo se foi” é cotidiano.
  3. Experimentos no cdev: mesmo que não dê para “mover tudo” de cara, encapsular o enter/exit no core diminuiria muito o risco de usos incorretos em drivers.

Se você escreve driver, a moral da história é simples: esse é um tijolo pequeno que resolve um problema grande. Comece onde dói—o seu caminho de char device, a sua ponte I²C, aquele buffer que às vezes “some”. Use a alça revogável em volta do acesso, trate NULL como “o device já era”, e siga. O kernel agradece; os backtraces, também.

Em suma: o “Linux kernel revocable” não é só mais um nome em include/linux/. É uma mudança de postura sobre ownership e lifetime em hotplug. Se a comunidade empurrar isso para os lugares certos (dos drivers até o cdev), a gente troca uma classe inteira de UAFs por retornos de erro previsíveis—e noites de sono um pouco mais tranquilas.

Compartilhe este artigo