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
- Test Patterns em Zig — Artigo anterior
- Testes de Integracao em Zig — Proximo artigo
- Testes Baseados em Propriedades — Property testing
- Zig para Seguranca Cibernetica — Seguranca
- Automacao de Testes com CI/CD — CI/CD