---
title: "Zig Fuzzer: Fuzz Testing Nativo, Targets e CI"
url: "https://ziglang.com.br/artigos/zig-fuzz-testing-fuzzer-nativo/"
markdown_url: "https://ziglang.com.br/artigos/zig-fuzz-testing-fuzzer-nativo.MD"
description: "Guia prático de fuzz testing em Zig com std.testing.fuzz: como escrever fuzz targets, achar crashes em parsers, reduzir inputs e integrar o fuzzer ao CI."
date: "2026-09-01"
author: ""
---

# Zig Fuzzer: Fuzz Testing Nativo, Targets e CI

Guia prático de fuzz testing em Zig com std.testing.fuzz: como escrever fuzz targets, achar crashes em parsers, reduzir inputs e integrar o fuzzer ao CI.


Para **fazer fuzz testing em Zig**, escreva um `test` que chama `std.testing.fuzz` com uma função `testOne` recebendo `[]const u8`, transforme esses bytes em uma chamada real do seu código e deixe o fuzzer procurar crashes, panics, leaks e violações de invariante. O valor do fuzzer nativo aparece quando você testa parsers, deserializers, decoders e qualquer fronteira que aceite dados externos sem confiar no formato.

Fuzzing não substitui [testes unitários](/tutoriais/testes-zig/) nem [property-based testing](/tutoriais/zig-property-testing/). Ele cobre outro ângulo: em vez de você inventar o caso ruim, o computador gera milhares ou milhões de entradas e pressiona o caminho que costuma quebrar em produção. Este guia mostra o modelo mental, um target mínimo, padrões para parsers, redução de crashes, integração com CI e quando combinar o fuzzer da std com ferramentas externas.

Como a std e as flags de `zig test` ainda evoluem antes do 1.0, fixe a versão do Zig no repositório e confirme a assinatura de `std.testing.fuzz` na release do projeto.

## Resposta rápida

| Necessidade | Abordagem recomendada |
|---|---|
| Achar crash em parser/decoder | `std.testing.fuzz` + `testOne([]const u8)` |
| Validar invariante de domínio | property-based testing + geradores |
| Regressão determinística | salve o input que quebrou e vire teste unitário |
| PR / merge gate | corpus curto + seeds conhecidos |
| Busca longa por bugs raros | job noturno com orçamento de tempo/CPU |
| Código com alocação | `testing.allocator` ou GPA com detecção de leak |
| Entrada estruturada | interprete prefixos/bytes como enums e comprimentos |
| API instável pré-1.0 | pin da toolchain + smoke test do próprio fuzz harness |

## Por que fuzzing importa em Zig

Zig atrai parsers, protocolos, tools de build, runtimes e código perto do metal. Esses programas lidam com bytes que vêm de arquivo, socket, memória compartilhada ou outro processo. A falha típica não é `2 + 2 != 4`; é um comprimento mentiroso, um offset negativo interpretado como `usize`, um enum fora do intervalo ou um caminho que libera duas vezes.

Testes de exemplo pegam o que você lembrou de escrever. Fuzzing pressiona o restante:

- headers HTTP malformados;
- JSON truncado;
- payloads protobuf com campos repetidos;
- ELF/PE/WASM corrompidos;
- configurações TOML/YAML com escapes estranhos;
- mensagens de protocolo com tamanho maior que o buffer declarado.

A linguagem ajuda porque erros são valores, alocadores são explícitos e builds de teste podem instrumentar leaks. Isso torna o feedback do fuzzer mais acionável do que em stacks onde panics silenciosos ou alocação global escondem o problema.

## O alvo mínimo com std.testing.fuzz

O esqueleto mais útil é curto: receber bytes, chamar a API real e aceitar erros esperados sem engolir invariantes.

```zig
const std = @import("std");
const testing = std.testing;
const parseConfig = @import("config.zig").parseConfig;

test "fuzz parseConfig" {
    try testing.fuzz(.{}, struct {
        fn testOne(input: []const u8) !void {
            var gpa = std.heap.GeneralPurposeAllocator(.{}){};
            defer {
                const leaked = gpa.deinit();
                if (leaked == .leak) @panic("leak no fuzz target");
            }
            const allocator = gpa.allocator();

            const parsed = parseConfig(allocator, input) catch return;
            defer parsed.deinit(allocator);

            // Invariante barata: serializar e parsear de novo não pode crashar.
            const roundtrip = try parsed.toBytes(allocator);
            defer allocator.free(roundtrip);
            const again = parseConfig(allocator, roundtrip) catch return;
            defer again.deinit(allocator);
        }
    }.testOne);
}
```

Pontos importantes do exemplo:

1. **O fuzzer manda bytes crus.** Cabe a você interpretá-los.
2. **Erro de parse esperado é sucesso do target.** `catch return` evita transformar rejeição válida em “bug”.
3. **Leak e panic são falhas.** Use alocador de teste ou GPA e falhe alto.
4. **Round-trip barato aumenta o sinal.** Se o valor parseado puder ser serializado, force esse caminho sem depender de I/O.

Para um overview mais amplo do sistema de testes, combine este guia com o [guia completo de testes em Zig](/artigos/zig-testes-guia-completo/).

## Como desenhar um fuzz target que encontra bugs de verdade

Um target fraco só gera ruído. Um target forte tem cinco propriedades.

### 1. Exercita código de produção, não um stub

Se o fuzzer só chama uma função de validação superficial, ele não toca no decoder real. Prefira a mesma entrada que a aplicação usaria após ler um arquivo ou frame de rede.

### 2. Falha de forma observável

O fuzzer precisa de um oráculo. Em Zig, oráculos comuns são:

- `@panic` / `unreachable` atingidos;
- overflow em modos de segurança ativos;
- leak de memória;
- assert de invariante (`std.debug.assert`);
- diferença em round-trip;
- comparação com implementação de referência para o mesmo input.

### 3. Roda rápido e sem estado global

Evite abrir porta, falar com banco ou dormir. Estado global entre iterações cria falsos positivos e esconde bugs de uso-após-free. Recrie allocator e estruturas a cada `testOne`.

### 4. Aceita entrada maliciosa sem confiança prévia

Não “corrija” o input antes de chamar o parser. Se você normaliza demais, remove justamente os casos que quebram offsets e comprimentos.

### 5. Tem orçamento claro

Defina quantos segundos ou iterações cabem no CI. Fuzzing sem orçamento vira job infinito; fuzzing com 2 segundos no PR ainda ajuda quando há corpus de regressão.

## Transformando bytes em entradas estruturadas

Muitos bugs só aparecem com combinações específicas de campos. Em vez de esperar que o fuzzer “adivinhe” um header válido por acaso, interprete os primeiros bytes como roteador:

```zig
fn decodeCase(input: []const u8) void {
    if (input.len == 0) return;
    const kind = input[0] % 4;
    const payload = input[1..];

    switch (kind) {
        0 => parseJson(payload),
        1 => parseToml(payload),
        2 => parseBinaryFrame(payload),
        else => parseLegacy(payload),
    }
}
```

Outras estratégias úteis:

- usar 2–4 bytes para comprimentos e fatiar o restante;
- mapear bytes para enums de opcode;
- misturar um dicionário de tokens legítimos com mutações;
- alimentar o mesmo payload a duas implementações e comparar resultado.

Essa abordagem se aproxima de *structure-aware fuzzing* sem sair do harness nativo.

## Parsers: o caso de uso número um

Se você só puder fuzzar uma coisa no projeto, fuzz o parser. Exemplos de invariantes baratas:

| Domínio | Invariante útil |
|---|---|
| JSON/config | input rejeitado não aloca de forma ilimitada |
| Frame binário | `payload.len <= declared_len` |
| Codec | `decode(encode(x))` preserva campos canônicos |
| Protocolo | estado inválido nunca avança sem erro explícito |
| Path/URL | normalização não escapa o diretório base |

Exemplo para um decoder de frame:

```zig
const Frame = struct {
    opcode: u8,
    payload: []const u8,
};

fn decodeFrame(buffer: []const u8) !Frame {
    if (buffer.len < 2) return error.UnexpectedEof;
    const opcode = buffer[0];
    const declared = buffer[1];
    if (buffer.len < 2 + @as(usize, declared)) return error.UnexpectedEof;
    return .{
        .opcode = opcode,
        .payload = buffer[2 .. 2 + declared],
    };
}

test "fuzz decodeFrame" {
    try testing.fuzz(.{}, struct {
        fn testOne(input: []const u8) !void {
            const frame = decodeFrame(input) catch return;
            try testing.expect(frame.payload.len <= 255);
            try testing.expect(frame.payload.ptr >= input.ptr);
            try testing.expect(frame.payload.ptr + frame.payload.len <= input.ptr + input.len);
        }
    }.testOne);
}
```

Aqui o fuzzer procura, entre outras coisas, fatias que escapam do buffer original — classe clássica de bug em parsers manuais.

## Redução de crash: do input gigante ao caso mínimo

Quando o fuzzer encontra um crash, o input inicial pode ter kilobytes. Antes de abrir issue, reduza:

1. Salve o input bruto (`crash-001.bin`).
2. Tente remover sufixo/prefixo enquanto o bug permanece.
3. Substitua blocos por zeros ou por tokens curtos.
4. Congele o caso mínimo como teste unitário nomeado.

```zig
test "regressão: frame com declared_len mentiroso" {
    const input = [_]u8{ 0x01, 0x05, 'a', 'b' }; // declara 5, só tem 2
    try testing.expectError(error.UnexpectedEof, decodeFrame(&input));
}
```

Esse ciclo — fuzz encontra, humano reduz, unitário trava — é o que transforma fuzzing em qualidade permanente. Sem a etapa de regressão, o mesmo bug volta no mês seguinte.

## Fuzzing e propriedade: quando usar cada um

Use **fuzz testing** quando a pergunta for: “este código sobrevive a entradas hostis?”.

Use **property-based testing** quando a pergunta for: “esta regra de domínio permanece verdadeira para qualquer valor gerado?”.

Exemplos:

- fuzz: parser de HTTP, decoder de imagem, deserialização de cache em disco;
- propriedade: `sort` é idempotente, `set.insert` torna `contains` verdadeiro, compressão+descompressão preserva bytes.

Os dois compartilham geradores e oráculos. Em projetos Zig é comum começar com fuzz de bytes e, depois, extrair propriedades estáveis para a suíte rápida. O tutorial de [property-based testing em Zig](/tutoriais/zig-property-testing/) cobre o segundo caminho em detalhe.

## Integração com CI sem quebrar o pipeline

Fuzzing contínuo precisa de duas velocidades.

### Gate de pull request

- rode `zig test` normal;
- execute fuzz targets por poucos segundos ou com corpus pequeno;
- falhe se qualquer seed de crash conhecido voltar a quebrar.

### Busca profunda

- job agendado (noite/fim de semana);
- orçamento de minutos ou horas por target;
- artefatos com inputs interessantes;
- notificação só quando houver crash novo.

Em GitHub Actions ou GitLab CI, isole o job de fuzz do job de unitários para não punir feedback de PR. Os guias de [GitHub Actions multiplataforma](/artigos/zig-github-actions-release-multiplataforma/) e [GitLab CI para build/test/release](/artigos/zig-gitlab-ci-pipeline-build-test-release/) mostram a espinha dorsal de pipeline; acrescente um job específico de fuzz em cima deles.

Modelo mental de stages:

```text
lint/build -> unit/property -> fuzz-smoke (PR) -> fuzz-deep (schedule)
```

## Corpus, dicionários e seeds

Mesmo com fuzzer nativo simples, a qualidade sobe quando você alimenta material inicial:

- **Corpus**: arquivos reais enxutos (configs válidas, frames típicos, respostas de API).
- **Dicionário**: tokens do protocolo (`Content-Length`, magic numbers, chaves JSON).
- **Seeds de crash**: inputs que já quebraram o sistema uma vez.

Organize no repositório:

```text
fuzz/
  corpus/config/
  corpus/frames/
  crashes/
  dict/protocol.dict
```

No PR, rode primeiro os crashes. Na busca profunda, parta do corpus e deixe a mutação trabalhar. Evite corpus enorme com binários de produção inteiros; prefira recortes que exercitem ramos distintos.

## Instrumentação e modos de segurança

Zig já oferece alavancas úteis sem tool externo:

- builds de teste com checagens de segurança ativas;
- `testing.allocator` para detectar leaks;
- asserts em pré-condições de parsers;
- logs só em falha, para não afogar o harness.

Quando o bug for de memória mais sutil, combine com [profiling e flamegraph](/artigos/zig-profiling-perf-flamegraph/) depois de isolar o input. Fuzzer acha o caso; profiler explica o custo; debugger explica o caminho. Para inspeção interativa, o guia de [debugging em Zig](/tutoriais/zig-debugging/) continua sendo a referência operacional.

## Quando ir além do fuzzer da std

O fuzzer nativo é excelente para o dia a dia do repositório. Ainda assim, há cenários em que ferramentas externas compensam:

| Cenário | Opção |
|---|---|
| Campanha longa em protocolo complexo | libFuzzer / AFL++ via harness C ABI |
| Comparação com implementação em C | driver FFI compartilhado |
| Cobertura guiada muito madura | engate com sanitizers do ecossistema C |
| Target multi-linguagem | corpus comum + wrappers por linguagem |

A ponte natural em Zig é exportar uma função C que recebe ponteiro+tamanho e chama o decoder Zig. Assim você reutiliza a mesma lógica sob AFL++/libFuzzer sem duplicar o parser. Comece pelo nativo; só adicione complexidade quando o corpus e os targets internos já estiverem sólidos.

## Checklist prático antes de declarar “temos fuzzing”

- [ ] Existe pelo menos um fuzz target no caminho crítico de parsing.
- [ ] O target usa alocador com detecção de leak.
- [ ] Erros esperados não são tratados como crash.
- [ ] Crashes viram testes unitários determinísticos.
- [ ] PR roda smoke de fuzz; schedule roda busca longa.
- [ ] Corpus e crashes versionados cabem no git.
- [ ] Toolchain Zig está pinada e documentada.
- [ ] Há link da suíte de fuzz na documentação de contribuição.

## Erros comuns

**Engolir todo erro.** Se `catch {}` esconde invariante violada, o fuzzer “passa” sem achar nada.

**Fuzzar só o happy path.** Normalizar input até ficar sempre válido derrota o propósito.

**Estado global entre iterações.** Singletons, caches sem reset e PRNG global contaminam o próximo caso.

**Job único sem orçamento.** Ou estoura o CI ou ninguém roda. Separe smoke e deep.

**Não salvar o input.** Sem artefato, o bug é anedota. Sempre persista o binário mínimo.

**Confundir cobertura com segurança.** 80% de cobertura com parser frágil ainda é parser frágil. Olhe crashes e invariantes, não só porcentagem.

## Exemplo de roteiro em um serviço HTTP

Suponha um serviço que já cobre [servidor HTTP](/tutoriais/zig-http-server/), [compressão](/artigos/zig-http-compressao-gzip-brotli/) e [CORS](/artigos/zig-cors-api-http/). Uma ordem sensata de fuzz targets é:

1. parser de request line e headers;
2. decoder de chunked transfer;
3. caminho de descompressão gzip/brotli com limite de expansão;
4. parser de cookies/sessão;
5. payload JSON das rotas internas.

Cada target deve impor limites: tamanho máximo, número de headers, profundidade de JSON e tempo por iteração. Fuzzer sem limite vira DoS contra a própria suíte.

## Como falar de fuzzing em currículo e code review

Em code review, cobre evidência:

- qual target cobre a mudança;
- se o corpus ganhou um caso novo;
- se o crash virou regressão unitária;
- se o orçamento de CI ainda fecha.

Em portfólio, um repositório Zig com `fuzz/corpus`, job noturno e crashes reduzidos comunica maturidade de sistemas melhor do que só listar “usei Zig”. Para o lado de carreira e mercado, conecte essa disciplina aos materiais de [carreira em Zig](/carreira/) e ao [guia de testes completo](/artigos/zig-testes-guia-completo/).

## Perguntas frequentes

### Zig tem fuzzer nativo?

Sim. Desde o Zig 0.12 a std expõe `std.testing.fuzz` para repetir um alvo com entradas geradas. O padrão usual é um `test` com `testOne(input: []const u8)`. Confirme flags e assinatura na versão pinada do projeto.

### Qual a diferença entre fuzz testing e property-based testing?

Fuzzing caça crashes e comportamentos inseguros sob bytes adversários. Property-based testing verifica propriedades explícitas com valores gerados. Use os dois: fuzz na borda do sistema, propriedades no núcleo do domínio.

### O que um bom fuzz target deve fazer?

Chamar código real, terminar rápido, evitar I/O externo, falhar alto em panic/leak/invariante e aceitar erros de parse esperados sem mascarar bugs.

### Como integrar fuzzing ao CI?

Smoke curto no PR com corpus/seeds, busca longa em schedule, artefatos de crash e promoção imediata do input mínimo para teste unitário.

### Fuzzing substitui testes unitários?

Não. Unitários documentam contratos. Fuzzing explora o que você não escreveu. A combinação com table-driven tests e propriedades é o arranjo estável.

### As APIs mudam antes do Zig 1.0?

Podem mudar. Pin da toolchain, smoke do harness e leitura das notes da release evitam surpresa. Trate exemplos da internet como modelo, não como especificação eterna.

## Conclusão

Fuzz testing em Zig é uma das formas mais baratas de endurecer parsers e fronteiras de confiança. Com `std.testing.fuzz`, um target rápido, corpus enxuto e regressões determinísticas, você transforma entradas caóticas em bugs reproduzíveis. Comece pelo decoder mais exposto, salve cada crash como teste e só então invista em campanhas longas ou harness externo.

Se você está estruturando a suíte do zero, siga esta ordem: [testes básicos](/tutoriais/testes-zig/), [padrões e table-driven tests](/tutoriais/zig-testing-avancado/artigo-2-test-patterns/), este guia de fuzzer nativo, [property-based testing](/tutoriais/zig-property-testing/) e automação de [CI/CD](/tutoriais/zig-testing-avancado/artigo-5-ci-cd-testing/). O fuzzer não substitui pensamento de projeto — ele pune a parte do projeto que ainda confia demais na entrada.
