Fuzzing en Go 1.18+: go test -fuzz, corpus y encontrar bugs automáticamente

Desde Go 1.18, el toolchain incluye un motor de fuzzing nativo que genera automáticamente miles de inputs mutados para encontrar crashes, panics y comportamientos inesperados. A diferencia de los tests unitarios, el fuzzer explora el espacio de entradas sin que tengas que pensar en casos extremos. Es especialmente eficaz en parsers, deserializadores y funciones que procesan datos externos.

Sintaxis básica: FuzzXxx, f.Add y f.Fuzz

package parserfecha_test

import (
    "testing"
    "time"
)

func FuzzParseFecha(f *testing.F) {
    // Corpus inicial: ejemplos válidos y casos borde
    f.Add("2024-01-15")
    f.Add("2000-02-29") // año bisiesto
    f.Add("")
    f.Add("no-es-una-fecha")

    // El fuzzer muta estos seeds y llama a esta función con cada variante
    f.Fuzz(func(t *testing.T, input string) {
        // La función no debe entrar en pánico
        fecha, err := ParseFecha(input)
        if err != nil {
            return // error esperado para entradas inválidas
        }

        // Propiedad: si parseó bien, debe re-serializar igual
        if fecha.Format("2006-01-02") != input {
            t.Errorf("ida y vuelta fallida: %q ? %v ? %q",
                input, fecha, fecha.Format("2006-01-02"))
        }
    })
}

func ParseFecha(s string) (time.Time, error) {
    return time.Parse("2006-01-02", s)
}

Ejecutar el fuzzer

// Solo ejecutar los seeds del corpus (igual que go test normal)
go test -run FuzzParseFecha

// Fuzzing real: genera inputs durante 30 segundos
go test -fuzz=FuzzParseFecha -fuzztime=30s

// Fuzzing hasta encontrar un fallo o interrumpir manualmente
go test -fuzz=FuzzParseFecha

Cuando el fuzzer encuentra un fallo, guarda el input que lo provocó en testdata/fuzz/FuzzParseFecha/. A partir de ese momento, go test (sin -fuzz) reproduce ese caso automáticamente en cada ejecución.

Ejemplo 2: fuzzing de un parser CSV

func FuzzParseCSV(f *testing.F) {
    f.Add("a,b,cn1,2,3n")
    f.Add("")
    f.Add("campo con "comillas",otro")

    f.Fuzz(func(t *testing.T, csv string) {
        records, err := ParseCSV(strings.NewReader(csv))
        if err != nil {
            return
        }
        // Propiedad: todos los registros deben tener el mismo número de columnas
        if len(records) > 1 {
            cols := len(records[0])
            for i, row := range records[1:] {
                if len(row) != cols {
                    t.Errorf("fila %d tiene %d columnas, esperadas %d", i+1, len(row), cols)
                }
            }
        }
    })
}

Ejemplo 3: invariantes de un tipo Moneda

func FuzzMoneda(f *testing.F) {
    f.Add("10.50")
    f.Add("0.00")
    f.Add("-5.99")
    f.Add("9999999.99")

    f.Fuzz(func(t *testing.T, s string) {
        m, err := ParseMoneda(s)
        if err != nil {
            return
        }
        // Invariante: la moneda no puede tener más de 2 decimales
        texto := m.String()
        partes := strings.Split(texto, ".")
        if len(partes) == 2 && len(partes[1]) > 2 {
            t.Errorf("moneda %q tiene más de 2 decimales: %q", s, texto)
        }
    })
}

Ejemplo 4: fuzzing de JSON round-trip

func FuzzJSONRoundTrip(f *testing.F) {
    f.Add(`{"nombre":"Ana","edad":30}`)
    f.Add(`{}`)
    f.Add(`{"valor":1e308}`)

    f.Fuzz(func(t *testing.T, input string) {
        var m map[string]any
        if err := json.Unmarshal([]byte(input), &m); err != nil {
            return // input no es JSON válido
        }

        // Re-serializar y volver a deserializar
        data, err := json.Marshal(m)
        if err != nil {
            t.Fatalf("marshal falló con input válido: %v", err)
        }

        var m2 map[string]any
        if err := json.Unmarshal(data, &m2); err != nil {
            t.Fatalf("unmarshal del resultado falló: %v", err)
        }
    })
}

Gestión del corpus

Los inputs que encuentra el fuzzer se guardan en testdata/fuzz/NombreFuzz/ como ficheros de texto. Añade tus propios casos al corpus para guiar la exploración:

// testdata/fuzz/FuzzParseCSV/caso_especial
go test corpus v1
string("a,,brn")

El corpus de la comunidad de Go (https://storage.googleapis.com/go-fuzzcache/) también acumula casos encontrados por otros desarrolladores sobre los paquetes estándar. Si trabajas con parsers propios, ejecutar el fuzzer durante horas en CI puede descubrir vulnerabilidades antes de que lleguen a producción.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP