Fuzzing tem um problema de reputação: muitos engenheiros ainda o imaginam como jogar bytes aleatórios em um programa até ele travar. Essa descrição era aproximadamente precisa em 1990 e tem sido enganosa por uma década. O fuzzing moderno é guiado por cobertura — o fuzzer instrumenta o alvo, observa quais caminhos de código cada input alcança, e orienta a mutação em direção a inputs que exploram novo território. Aquele loop de feedback é a diferença entre pôr aleatoriamente um parser e sistematicamente caminhar por seu espaço de estado, e é por isso que fuzzing agora encontra milhares de CVEs reais por ano em software que foi revisado por experts.
A paisagem de 2026 maturou bem além de uma única ferramenta, porém, e os desenvolvimentos interessantes são sobre alcançar alvos que fuzzing convencional não conseguia: kernels de sistema operacional, código enterrado atrás de setup caro, e aplicações cuja estrutura de entrada derrota mutação de byte. Este guia cobre aquela paisagem — os clássicos guiados por cobertura, syzkaller para kernels, Snapchange para fuzzing de snapshot, e LibAFL para construir o fuzzer que seu alvo realmente precisa — mais cargo-fuzz e honggfuzz para trabalho cotidiano.
Por que orientação de cobertura mudou tudo
O mecanismo vale a pena entender porque explica o que fuzzing é bom em fazer e onde ele stall. Um fuzzer guiado por cobertura compila o alvo com instrumentação que registra quais bordas do grafo de fluxo de controle uma execução tocou. Ele mantém um corpus de inputs, e quando um input mutado alcança uma borda que nenhum input anterior alcançou, esse input é julgado interessante e adicionado ao corpus para ser mutado ainda mais. Ao longo de milhões de iterações, o corpus acumula inputs que coletivamente exercem caminhos profundos e incomuns — incluindo caminhos que nenhum humano escreveu um teste para.
Isso produz comportamento que parece quase inteligente. Dado um corpus seeded com um PNG válido, um fuzzer descobrirá a estrutura de chunk, então tipos válidos de chunk, então ramos de parsing para cada tipo, progressivamente construindo inputs que alcançam mais profundamente no decodificador. Ele nunca "entende" PNG; apenas mantém tudo que moveu cobertura.
O corolário é aonde fuzzing stall: verificações duras que ele não consegue adivinhar. Uma comparação de constante mágico, uma checksum ou uma assinatura criptográfica cria uma parede — mutação aleatória essencialmente nunca produzirá os 8 bytes certos, então tudo atrás dessa verificação fica inexplorado. As respostas práticas são semear o corpus com inputs válidos, fornecer um dicionário de valores mágicos, ou patchear checksums em um build de fuzz. Reconhecer um plateau de cobertura como "atingi uma parede" em vez de "não há mais bugs" é um dos instintos mais úteis neste trabalho.
Fuzzing de kernel: syzkaller
Kernels de sistema operacional são um alvo hostil para fuzzers convencionais. O input não é um arquivo mas uma sequência de syscalls com argumentos interdependentes — um file descriptor de open deve fluir para read, e a maioria de sequências aleatórias de syscall falha imediatamente com EINVAL. Crashes derrubam a máquina inteira em vez de um processo, e cobertura deve ser coletada do espaço de kernel.
syzkaller resolve os três. Ele descreve syscalls em uma linguagem declarativa (syzlang) então consegue gerar sequências plausíveis com argumentos corretamente digitados e interdependentes; ele executa alvos dentro de VMs descartáveis então crashes são sobrevivíveis e automaticamente coletados; e ele usa KCOV para feedback de cobertura de kernel mais KASAN para capturar erros de memória que de outra forma seriam corrupção silenciosa. O syzbot contínuo do Google executa isso continuamente contra Linux e reportou milhares de bugs.
A lição generaliza além de kernels: syzkaller funciona porque alguém codificou conhecimento da estrutura do input em descrições. Quando inputs de um alvo têm gramática, ensinar ao fuzzer essa gramática bate mutação de byte puro por uma margem ampla. O custo correspondente é real — estender syzlang para um subsistema sub-testado é trabalho genuíno, e também é a contribuição de mais alto valor que a maioria das pessoas consegue fazer para fuzzing de kernel.
Fuzzing de snapshot: passando o setup
A segunda fronteira é alvos onde o código interessante senta atrás de inicialização cara. Considere fuzzing de um parseador de query de banco de dados: cada iteração precisaria iniciar o servidor, inicializar armazenamento, autenticar e estabelecer uma sessão antes de uma única query ser parseada. Em talvez dez iterações por segundo, fuzzing guiado por cobertura é hopeless — a técnica precisa de milhares.
Fuzzing de snapshot inverte isto. Você executa o alvo uma vez para exatamente o momento de interesse, tira um snapshot de memória do estado completo da máquina, e então restaura aquele snapshot para cada iteração subsequente. Todo o custo de setup é pago uma vez. Snapchange (da AWS) implementa isto com KVM: você captura um snapshot com QEMU, escreve um pequeno harness Rust descrevendo onde injetar input e quando uma iteração termina, e ele reproduz a partir daquele estado em taxas muito altas.
Isto destranca categorias que previamente eram impráticas: protocolos de rede stateful fuzzed mid-session, código atrás de autenticação, código de hypervisor e kernel, e qualquer aplicação com startup pesado. O trade é esforço — você escreve um fuzzer Rust em vez de executar um comando, e você deve entender o layout de memória do alvo bem o bastante para injetar input corretamente. É uma técnica especialista que compensa exatamente quando a alternativa é não fuzzar o alvo em tudo.
Frameworks: construa o fuzzer que o alvo precisa
O terceiro desenvolvimento é filosófico. AFL++ e libFuzzer são excelentes no que foram projetados para e desajeitados quando seu alvo não se encaixa — um protocolo binário customizado, uma imagem de firmware emulada, um input que é uma árvore em vez de um buffer. Historicamente você dobrava a ferramenta, geralmente mal.
LibAFL, da equipe AFL++, trata um fuzzer como partes compostas: observers que registram dados, feedbacks que julgam interestingness, mutators que transformam inputs, schedulers, stages e executors. Você monta a combinação que seu alvo precisa, define seu próprio tipo de entrada e mutators se a entrada é estruturada, escolhe um executor (in-process, forkserver, emulação QEMU, instrumentação Frida, snapshot), e obtém escalamento multi-core gratuitamente.
O framing honesto é que isto é um compromisso maior que executar uma ferramenta, então a sequência importa: comece com cargo-fuzz para Rust ou honggfuzz/AFL++ para alvos nativos, e mude para LibAFL apenas quando você conseguir articular especificamente por que eles não se encaixam. "O input é uma máquina de estado de protocolo e mutação de byte nunca produz uma segunda mensagem válida" é tal razão; "eu quero que seja mais rápido" geralmente não é.
Fuzzing cotidiano que equipes realmente sustentam
A maioria do valor, para a maioria das equipes, vem de fuzzing contínuo desinteressante de parsers e manipuladores de input não confiável. cargo-fuzz torna isto quase sem fricção para Rust: escreva uma função levando &[u8] (ou melhor, um valor digitado via crate arbitrary) e execute um comando. honggfuzz é similarmente fácil para código nativo e adiciona cobertura baseada em hardware via Intel PT/BTS, que permite você fuzz binários que você não consegue recompilar — valioso para dependências de código fechado.
Duas práticas separam equipes que obtêm valor de fuzzing de equipes que abandonam fuzzing após uma semana. Primeiro, semeie o corpus com inputs válidos reais; um fuzzer começando de um corpus vazio gasta tempo enorme redescobrir validade básica de formato que você poderia ter entregado. Segundo, execute continuamente e trate descobertas como testes: converta cada crash minimizado em um teste de regressão para que permaneça corrigido, e deixe o corpus persistir entre execuções para que progresso acumule. Fuzzing não é uma auditoria de uma tarde; é um processo de background que continua encontrando coisas conforme código muda.
Sanitizers merecem uma menção porque multiplicam eficácia. AddressSanitizer transforma corrupção de memória silenciosa em um crash imediato e diagnosticável, e UBSan captura comportamento indefinido que pode aparecer como uma miscompilação misteriosa mais tarde. Fuzzing sem sanitizers encontra apenas bugs que acontecem de travar por conta própria — tipicamente uma pequena fração do que realmente está lá.
Triagem: o trabalho que começa quando o crash chega
Encontrar um crash é o início, e equipes rotineiramente subestimam o esforço entre "o fuzzer parou" e "um desenvolvedor consegue consertar isto". Quatro passos fazem a diferença entre um relatório útil e um ignorado.
Minimize o input. Um input travando de um fuzzer é tipicamente cheio de bytes irrelevantes que sobreviveram apenas porque nada os removeu. Todo fuzzer sério envia um minimizer (cargo fuzz tmin, afl-tmin, syz-repro de syzkaller), e executar-lo transforma um blob de 4KB em um punhado de bytes que isolam o gatilho real. Isto importa enormemente para o desenvolvedor que tem que entender.
Deduplicar. Um fuzzer que executa durante a noite reportará o mesmo bug dúzias de vezes através de inputs diferentes. Agrupar por localização de crash e assinatura de stack transforma 200 crashes em seis bugs distintos. Sem este passo, triagem parece impossível cara e pessoas desistem.
Avalie exploitabilidade, cuidadosamente. Nem todo crash é uma vulnerabilidade. Um null-pointer dereference em um parser é geralmente uma negação de serviço; um heap buffer overflow com comprimento controlado por atacker é potencialmente muito pior. Saída de sanitizer ajuda enormemente aqui — ASan diz você o tipo de erro de memória, os tamanhos, e ambos os stacks de alocação e acesso. Resista à tentação de rotular tudo como crítico, porque uma equipe que recebe severidades infladas para confiar nos relatórios.
Converta para um teste de regressão. O input minimizado se torna um teste unitário committed junto com a correção. Isto é o que mantém o bug corrigido e o que torna fuzzing composto ao longo do tempo em vez de redescobrir os mesmos problemas após um refactor.
As equipes que obtêm valor sustentado de fuzzing são as que constroem este pipeline uma vez, não as que encontram o máximo de crashes.
Escolhendo aonde começar
A decisão segue o alvo. Para código Rust, use cargo-fuzz, e use arbitrary para fuzz APIs digitadas em vez de apenas parsers de byte. Para binários userspace nativos com fonte, honggfuzz ou AFL++ com sanitizers. Para binários sem fonte, feedback de hardware de honggfuzz ou modo QEMU de AFL++. Para kernels SO, syzkaller e considere estender syzlang para o subsistema que você se importa. Para código atrás de setup caro ou sessões stateful, Snapchange ou uma configuração Snapchange-capable de LibAFL. E para inputs com estrutura real — protocolos, ASTs, formatos de arquivo com gramática — ou um mutator ciente de gramática ou um fuzzer LibAFL customizado, porque mutação de byte plateaará cedo.
Através de todos estes, a mesma disciplina aplica: semeie bem, execute com sanitizers, execute continuamente, minimize crashes, e converta descobertas em testes de regressão. A ferramenta importa menos que o loop continuar.
O resumo
Fuzzing em 2026 é uma disciplina de descoberta de vulnerabilidade sistemática construída em feedback de cobertura, e sua fronteira é alcançar alvos que previamente estavam fora de escopo. syzkaller fuzz kernels codificando estrutura de syscall e executando em VMs descartáveis; Snapchange usa snapshots KVM para fuzz código enterrado atrás de setup caro; LibAFL permite você construir um fuzzer combinado para um alvo incomum em vez de dobrar uma ferramenta monolítica; e cargo-fuzz e honggfuzz tornam fuzzing contínuo cotidiano barato o bastante para realmente sustentar. Comece com a ferramenta fácil para sua linguagem, semeie o corpus com inputs reais, sempre habilite sanitizers, trate um plateau de cobertura como uma parede para engenharia passar em vez de um all-clear, e transforme todo crash em um teste.
Referências e Recursos
Ferramentas
Fundo e análise
Cheatsheets 1337skills relacionados