Fuzz Testing em Zig: Encontrando Bugs Automaticamente

Fuzz testing (ou fuzzing) e uma tecnica de teste que alimenta seu programa com dados aleatorios ou semi-aleatorios para encontrar bugs, crashes e vulnerabilidades de seguranca. Zig possui suporte built-in para fuzzing, tornando trivial a descoberta de bugs que testes manuais nunca encontrariam.

Continuacao do artigo sobre test patterns em Zig.

O Que e Fuzz Testing

Fuzz testing inverte a logica dos testes tradicionais. Em vez de voce escrever casos de teste especificos, o fuzzer gera milhoes de entradas automaticamente e observa se o programa se comporta corretamente — sem crashes, sem undefined behavior, sem violacoes de memoria.

Por Que Fuzzing Importa

  • Projetos reais encontram bugs com fuzzing que nunca seriam descobertos manualmente
  • Google OSS-Fuzz ja encontrou mais de 40.000 bugs em projetos open source
  • Parsers, codecs e protocolos sao especialmente vulneraveis a entradas malformadas
  • Zig facilita fuzzing porque o modo debug detecta undefined behavior automaticamente

Fuzzer Built-in do Zig

Desde a versao 0.12, Zig possui um fuzzer integrado no comando zig test. Ele usa coverage-guided fuzzing para maximizar a exploracao do codigo.

Primeiro Fuzz Test

const std = @import("std");

/// Funcao que queremos testar com fuzzing
fn decodificarVarInt(dados: []const u8) !struct { valor: u64, bytes_lidos: usize } {
    if (dados.len == 0) return error.DadosInsuficientes;

    var resultado: u64 = 0;
    var shift: u6 = 0;

    for (dados, 0..) |byte, i| {
        if (i >= 10) return error.VarIntMuitoLongo; // Max 10 bytes para u64

        const valor_byte: u64 = @intCast(byte & 0x7F);
        resultado |= valor_byte << shift;

        if (byte & 0x80 == 0) {
            return .{ .valor = resultado, .bytes_lidos = i + 1 };
        }

        shift += 7;
    }

    return error.VarIntIncompleto;
}

test "fuzz decodificarVarInt" {
    // O bloco `fuzz` indica ao Zig que esta e uma entrada de fuzzing
    const input = std.testing.fuzzInput(.{});

    // Se decodificar com sucesso, verificar propriedades
    if (decodificarVarInt(input)) |resultado| {
        // Propriedade: bytes_lidos deve ser <= tamanho da entrada
        try std.testing.expect(resultado.bytes_lidos <= input.len);
        // Propriedade: bytes_lidos deve ser > 0
        try std.testing.expect(resultado.bytes_lidos > 0);
        // Propriedade: bytes_lidos deve ser <= 10
        try std.testing.expect(resultado.bytes_lidos <= 10);
    } else |err| {
        // Erros sao aceitaveis — fuzzing testa que nao ha crash
        switch (err) {
            error.DadosInsuficientes,
            error.VarIntMuitoLongo,
            error.VarIntIncompleto,
            => {},
        }
    }
}

Execute o fuzz test com:

zig test --fuzz arquivo.zig
# O fuzzer roda indefinidamente ate encontrar um problema
# ou ate voce pressionar Ctrl+C

Opcoes do Fuzzer

# Rodar fuzzing por 60 segundos
zig test --fuzz arquivo.zig -- --fuzz-timeout 60000

# Especificar corpus inicial
zig test --fuzz arquivo.zig -- --fuzz-corpus ./corpus/

# Limitar uso de memoria do fuzzer
zig test --fuzz arquivo.zig -- --fuzz-mem-limit 256000000

Fuzz Targets Eficientes

Fuzzing de Parser JSON

Parsers sao alvos classicos de fuzzing porque precisam lidar com qualquer entrada possivel:

const std = @import("std");

/// Parser JSON simplificado para demonstracao
const JsonParser = struct {
    dados: []const u8,
    posicao: usize = 0,

    const JsonValue = union(enum) {
        null_value,
        boolean: bool,
        number: f64,
        string: []const u8,
        array: []const JsonValue,
        object: void, // Simplificado
    };

    const ParseError = error{
        TokenInesperado,
        FimInesperado,
        NumeroInvalido,
        StringNaoFechada,
        NestingMuitoProfundo,
    };

    pub fn parse(self: *JsonParser) ParseError!JsonValue {
        self.pularEspacos();
        if (self.posicao >= self.dados.len) return error.FimInesperado;

        return switch (self.dados[self.posicao]) {
            'n' => self.parseNull(),
            't', 'f' => self.parseBool(),
            '"' => self.parseString(),
            '0'...'9', '-' => self.parseNumber(),
            '[' => self.parseArray(0),
            else => error.TokenInesperado,
        };
    }

    fn parseNull(self: *JsonParser) ParseError!JsonValue {
        if (self.posicao + 4 > self.dados.len) return error.FimInesperado;
        if (!std.mem.eql(u8, self.dados[self.posicao..][0..4], "null"))
            return error.TokenInesperado;
        self.posicao += 4;
        return .null_value;
    }

    fn parseBool(self: *JsonParser) ParseError!JsonValue {
        if (self.dados[self.posicao] == 't') {
            if (self.posicao + 4 > self.dados.len) return error.FimInesperado;
            if (!std.mem.eql(u8, self.dados[self.posicao..][0..4], "true"))
                return error.TokenInesperado;
            self.posicao += 4;
            return .{ .boolean = true };
        } else {
            if (self.posicao + 5 > self.dados.len) return error.FimInesperado;
            if (!std.mem.eql(u8, self.dados[self.posicao..][0..5], "false"))
                return error.TokenInesperado;
            self.posicao += 5;
            return .{ .boolean = false };
        }
    }

    fn parseString(self: *JsonParser) ParseError!JsonValue {
        self.posicao += 1; // Pular '"'
        const inicio = self.posicao;
        while (self.posicao < self.dados.len) {
            if (self.dados[self.posicao] == '"') {
                const str = self.dados[inicio..self.posicao];
                self.posicao += 1;
                return .{ .string = str };
            }
            if (self.dados[self.posicao] == '\\') {
                self.posicao += 1; // Pular escape
            }
            self.posicao += 1;
        }
        return error.StringNaoFechada;
    }

    fn parseNumber(self: *JsonParser) ParseError!JsonValue {
        const inicio = self.posicao;
        if (self.dados[self.posicao] == '-') self.posicao += 1;

        while (self.posicao < self.dados.len and
            (self.dados[self.posicao] >= '0' and self.dados[self.posicao] <= '9' or
            self.dados[self.posicao] == '.' or
            self.dados[self.posicao] == 'e' or
            self.dados[self.posicao] == 'E'))
        {
            self.posicao += 1;
        }

        const str = self.dados[inicio..self.posicao];
        const num = std.fmt.parseFloat(f64, str) catch return error.NumeroInvalido;
        return .{ .number = num };
    }

    fn parseArray(self: *JsonParser, depth: usize) ParseError!JsonValue {
        if (depth > 32) return error.NestingMuitoProfundo;
        self.posicao += 1; // Pular '['
        self.pularEspacos();

        // Array vazio
        if (self.posicao < self.dados.len and self.dados[self.posicao] == ']') {
            self.posicao += 1;
            return .{ .array = &[_]JsonValue{} };
        }

        // Parse elementos (simplificado — nao armazena)
        while (self.posicao < self.dados.len) {
            _ = try self.parse();
            self.pularEspacos();

            if (self.posicao >= self.dados.len) return error.FimInesperado;
            if (self.dados[self.posicao] == ']') {
                self.posicao += 1;
                return .{ .array = &[_]JsonValue{} };
            }
            if (self.dados[self.posicao] == ',') {
                self.posicao += 1;
                self.pularEspacos();
            }
        }

        return error.FimInesperado;
    }

    fn pularEspacos(self: *JsonParser) void {
        while (self.posicao < self.dados.len and
            (self.dados[self.posicao] == ' ' or
            self.dados[self.posicao] == '\t' or
            self.dados[self.posicao] == '\n' or
            self.dados[self.posicao] == '\r'))
        {
            self.posicao += 1;
        }
    }
};

test "fuzz json parser" {
    const input = std.testing.fuzzInput(.{});

    var parser = JsonParser{ .dados = input };

    // O parser nunca deve crashar, independente da entrada
    _ = parser.parse() catch |err| {
        // Todos os erros sao esperados para entradas invalidas
        switch (err) {
            error.TokenInesperado,
            error.FimInesperado,
            error.NumeroInvalido,
            error.StringNaoFechada,
            error.NestingMuitoProfundo,
            => {},
        }
    };
}

Fuzzing com Corpus Inicial

Um corpus inicial acelera o fuzzing fornecendo exemplos validos como ponto de partida:

test "fuzz json com corpus" {
    // Corpus de sementes — o fuzzer vai mutar estas entradas
    const seeds = [_][]const u8{
        "null",
        "true",
        "false",
        "42",
        "-3.14",
        "\"hello\"",
        "[1, 2, 3]",
        "\"\\\"escaped\\\"\"",
        "[null, true, 42]",
    };

    const input = std.testing.fuzzInput(.{ .corpus = &seeds });

    var parser = JsonParser{ .dados = input };
    _ = parser.parse() catch {};
}

Fuzzing para Seguranca

Encontrando Buffer Overflows

/// Funcao com bug intencional para demonstrar fuzzing
fn processarHeader(dados: []const u8) !void {
    // O fuzzer vai encontrar entradas que causam problemas
    var buffer: [64]u8 = undefined;

    var i: usize = 0;
    for (dados) |byte| {
        if (byte == '\n') break;
        if (i >= buffer.len) return error.HeaderMuitoLongo;
        buffer[i] = byte;
        i += 1;
    }

    // Processar o header
    const header = buffer[0..i];
    _ = header;
}

test "fuzz processarHeader" {
    const input = std.testing.fuzzInput(.{});

    // Sem crash = teste passa
    processarHeader(input) catch {};
}

Verificando Propriedades com Fuzzing

Combine fuzzing com verificacao de propriedades para encontrar bugs logicos:

/// Propriedade: encode(decode(x)) == x
test "fuzz roundtrip base64" {
    const input = std.testing.fuzzInput(.{});

    const base64 = std.base64.standard;

    // Tentar decodificar a entrada como Base64
    var decode_buf: [4096]u8 = undefined;
    const decoded = base64.Decoder.decode(&decode_buf, input) catch return;

    // Re-encodar e verificar roundtrip
    var encode_buf: [8192]u8 = undefined;
    const encoded = base64.Encoder.encode(&encode_buf, decoded);

    // Propriedade: re-encodar o decodificado deve dar o mesmo resultado
    // (normalizando padding)
    var decode_buf2: [4096]u8 = undefined;
    const decoded2 = base64.Decoder.decode(&decode_buf2, encoded) catch unreachable;

    try std.testing.expectEqualSlices(u8, decoded, decoded2);
}

Tecnicas Avancadas de Fuzzing

Structure-Aware Fuzzing

Em vez de fuzzing com bytes brutos, gere entradas estruturadas:

const std = @import("std");

/// Gerar uma estrutura valida a partir de bytes de fuzzing
const ProtocoloMsg = struct {
    tipo: u8,
    flags: u8,
    payload_len: u16,
    payload: []const u8,
};

fn interpretarComoMensagem(dados: []const u8) ?ProtocoloMsg {
    if (dados.len < 4) return null;

    const payload_len = std.mem.readInt(u16, dados[2..4], .big);
    if (dados.len < 4 + payload_len) return null;

    return .{
        .tipo = dados[0],
        .flags = dados[1],
        .payload_len = payload_len,
        .payload = dados[4..][0..payload_len],
    };
}

test "fuzz protocolo mensagem" {
    const input = std.testing.fuzzInput(.{});

    if (interpretarComoMensagem(input)) |msg| {
        // Verificar invariantes da mensagem
        try std.testing.expect(msg.payload.len == msg.payload_len);
        try std.testing.expect(msg.payload.len <= 65535);

        // Processar a mensagem nao deve crashar
        processarMensagem(msg) catch {};
    }
}

fn processarMensagem(msg: ProtocoloMsg) !void {
    switch (msg.tipo) {
        0x01 => { // PING
            if (msg.payload.len != 8) return error.PayloadInvalido;
        },
        0x02 => { // DATA
            if (msg.flags & 0x01 != 0) {
                // Flag de compressao ativa
                if (msg.payload.len < 2) return error.PayloadInvalido;
            }
        },
        else => return error.TipoDesconhecido,
    }
}

Differential Fuzzing

Compare duas implementacoes para encontrar discrepancias:

test "fuzz differential: meu parser vs std.json" {
    const input = std.testing.fuzzInput(.{});

    // Implementacao A: nosso parser
    var parser_a = JsonParser{ .dados = input };
    const resultado_a = parser_a.parse();

    // Implementacao B: std.json do Zig
    const resultado_b = std.json.parseFromSlice(
        std.json.Value,
        std.testing.allocator,
        input,
        .{},
    );

    // Ambos devem concordar se a entrada e valida ou nao
    const a_ok = if (resultado_a) |_| true else |_| false;
    const b_ok = if (resultado_b) |_| true else |_| false;

    if (a_ok != b_ok) {
        // Discrepancia encontrada! Um parser aceita e outro rejeita
        std.debug.print("Discrepancia com entrada: {s}\n", .{input});
        return error.TestDiscrepancia;
    }

    if (resultado_b) |val| val.deinit();
}

Integrando Fuzzing no Fluxo de Desenvolvimento

Organizacao de Fuzz Tests

src/
├── parser.zig
├── protocolo.zig
└── crypto.zig
test/
├── fuzz/
│   ├── fuzz_parser.zig
│   ├── fuzz_protocolo.zig
│   └── fuzz_crypto.zig
└── corpus/
    ├── parser/        # Entradas iniciais para parser
    ├── protocolo/     # Entradas iniciais para protocolo
    └── crypto/        # Entradas iniciais para crypto

Script de Fuzzing Continuo

#!/bin/bash
# fuzz.sh — roda fuzzing em rotacao por todos os targets

TARGETS=("test/fuzz/fuzz_parser.zig" "test/fuzz/fuzz_protocolo.zig" "test/fuzz/fuzz_crypto.zig")
TIMEOUT=300  # 5 minutos por target

for target in "${TARGETS[@]}"; do
    echo "Fuzzing: $target (${TIMEOUT}s)"
    zig test --fuzz "$target" -- --fuzz-timeout "${TIMEOUT}000" || {
        echo "BUG ENCONTRADO em $target!"
        exit 1
    }
done

echo "Fuzzing completo — nenhum bug encontrado"

Conclusao

Fuzz testing e uma das ferramentas mais poderosas para encontrar bugs que testes manuais nao conseguem alcançar. O fuzzer built-in do Zig torna trivial adicionar fuzzing ao seu projeto, sem dependencias externas ou configuracoes complexas. Parsers, codecs, protocolos e qualquer codigo que processa entrada externa devem ter fuzz tests.

Proximo Artigo

No Artigo 4: Testes de Integracao, vamos explorar como testar componentes que interagem com o sistema operacional, rede e file system.

Conteudo Relacionado

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.