KFuzzTest: um upgrade pequeno (e afiado) para fuzzing dentro do 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...

Alguns dos piores bugs se escondem onde as syscalls não chegam. Pense naquelas helpers internas do kernel — parsers de dados, conversores de formato, decodificadores ASN.1 — que quase nunca são exercitadas diretamente a partir da interface de sistema. O KFuzzTest, um framework leve de fuzzing in-kernel, nasce exatamente para esse ponto cego: ele permite que mantenedores fuzem funções internas in situ com pouquíssima infraestrutura. É o tipo de ferramenta que dá vontade de usar no mesmo dia.

Por quê isso importa? O fuzzing via syscall (ou até com encadeamento de reproduções) muitas vezes pena para “dirigir” o kernel até caminhos estreitos e de baixo estado. O KFuzzTest inverte a lógica: você declara um alvo de fuzz pequenino, ao lado do código que te interessa, conecta com um macro só, e alimenta esse alvo com bytes estruturados vindos do espaço de usuário. Resultado: adoção simples e um caminho direto até o coração do código.

A experiência de quem desenvolve: um macro, um arquivo, pronto

A API gira em torno de um único macro amigável: FUZZ_TEST(nome, tipo_da_struct). Você coloca isso num .c (geralmente em /tests do subsistema) e escreve o corpo do teste. O framework cuida do resto: cria um arquivo de escrita em /sys/kernel/debug/kfuzztest/<nome>/input. O que você escrever ali vira a entrada “hidratada” para a sua função. Sem portar nada para userspace, sem “transformar biblioteca”, sem gambiarras de stub — é chamar a helper interna com dados estruturados e pronto.

Para ajudar os fuzzers, o KFuzzTest oferece dois temperos que você pode salpicar no harness:

  • Restrições (KFUZZTEST_EXPECT_*): pré-condições aplicadas em runtime (ex.: “este ponteiro não pode ser NULL”) e, de quebra, dicas descobertas por ferramentas de userland.
  • Anotações (KFUZZTEST_ANNOTATE_*): pistas sem custo de runtime (ex.: “este campo é o comprimento daquele buffer”, “isto é string”).

Essas informações vão parar em seções ELF dedicadas — .kfuzztest_target, .kfuzztest_constraint, .kfuzztest_annotation — para que ferramentas externas listem alvos e entendam como gerar entradas válidas com mais inteligência.

Debaixo do capô: serialização binária que respeita C

Linux kernel fuzzing

O pulo do gato é o formato binário de entrada. Em vez de alocar um zoológico de objetos no kernel, o KFuzzTest recebe um blob plano e “realoca” em uma estrutura C válida:

  • Um cabeçalho de 8 bytes (magic + versão).
  • Um array de regiões com offsets/tamanhos das partes lógicas da entrada (pense “struct + buffers apontados”).
  • Uma tabela de relocações dizendo “há um ponteiro na região X, offset Y, que deve apontar para a região Z (ou NULL)”.
  • O payload: bytes crus de todas as regiões, concatenados e alinhados.

As regiões são ordenadas por offset, cada uma começa com alinhamento adequado ao seu tipo C e a um alinhamento mínimo global, e — ponto crucial — os vãos entre regiões são envenenados (via KASAN) para que acessos OOB virem relatórios precisos. O valor de alinhamento mínimo também é exposto por debugfs, facilitando a vida de quem codifica os blobs do lado de fora. É enxuto e, sobretudo, “no estilo kernel”.

Como preparação, o patchset ainda introduz kasan_poison_range() para envenenar intervalos arbitrários; isso garante que aqueles “acolchoamentos” entre regiões virem armadilhas perfeitas para o KASAN.

Uma ponte para blob fuzzers (e para humanos)

Não quer escrever um empacotador do zero? O conjunto traz uma ferramenta de espaço de usuário, a kfuzztest-bridge. Você descreve a entrada com uma DSL curtinha, parecida com C (regiões, arrays, ponteiros, campos “len de X”), aponta para um fluxo de bytes (por exemplo, /dev/urandom ou um corpus), e ela cospe um blob validado para o arquivo …/input. Ideal para smoke tests e para plugar fuzzers orientados a blob no seu alvo de Linux kernel fuzzing sem dor.

Funciona mesmo? O caso do PKCS#7

Para provar a ideia, os autores injetaram de propósito um off-by-one de leitura no caminho de parsing PKCS#7:

- ret = asn1_ber_decoder(&pkcs7_decoder, ctx, data, datalen);
+ ret = asn1_ber_decoder(&pkcs7_decoder, ctx, data, datalen + 1);

Depois criaram um alvo KFuzzTest para pkcs7_parse_message e apontaram um fork de desenvolvimento do syzkaller para ele. Resultado: partindo do zero, o syzkaller estourou o bug dentro de asn1_ber_decoder em menos de 30 segundos. Experimentos similares em outros alvos também acharam falhas injetadas rapidamente. A moral não é “o syzkaller é bom” (ele é), mas sim que fuzzing direto e ciente de estrutura em caminhos internos fica banal com o KFuzzTest.

O que muda para mantenedores

Se você já pensou “eu fuzaria esse parser se tivesse como alimentá-lo direito”, o KFuzzTest derruba a barreira quase a zero:

  • Harnesses rápidos: um macro, uma pequena struct de entrada, e você já está chamando o que interessa.
  • Entradas melhores: restrições e anotações informam o fuzzer sobre formatos e relações — tipo len == sizeof(buf) — reduzindo ruído.
  • Visibilidade de memória: o acolchoamento envenenado entre regiões transforma OOB em relatórios KASAN limpinhos.
  • Ganchos para tooling: com alvos e dicas visíveis nas seções .kfuzztest_*, ferramentas conseguem listar tudo e gerar blobs alinhados e bem-formados de maneira consistente.

Importante: KFuzzTest é para debug. Ele expõe funções internas ao userland via debugfs e não deve aparecer em builds de produção. A documentação reforça isso e recomenda habilitar KASAN/KCOV para ter o máximo proveito.

Onde isso se encaixa no ecossistema

O KFuzzTest não substitui o fuzzing por syscall; ele complementa. Syscalls continuam essenciais para cobrir integração, lifetimes e interações transversais. Mas para a classe “agulha no parser”, este framework dá uma ferramenta cirúrgica, barata de manter, fácil de rodar em CI e fácil de evoluir conforme o código muda. É um passo pequeno no esforço de Linux kernel fuzzing, mas com alto impacto prático: mais caminhos internos cobertos, mais cedo, com menos atrito.

Compartilhe este artigo