Tema representativo de entrevista

Entrevista técnica: ¿Cómo convertir las fallas de fuzzing en Go en un corpus de regresión?

CodingDifícil
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

Diseña una prueba de fuzzing en Go para un analizador sintáctico (parser) de entrada. Comienza con un corpus de semillas, define el invariante y explica cómo una entrada fallida minimizada se convierte en una prueba de regresión bajo un go test ordinario.

Pregunta y escenarios adecuados

Mantienes un analizador sintáctico (parser) de entrada en Go que ocasionalmente recibe Unicode, bytes truncados o longitudes extremas en producción. Explica cómo diseñar un objetivo de fuzzing testing.F, elegir semillas, expresar propiedades cuando la salida exacta no se puede conocer y preservar una falla como una prueba de regresión después de la corrección.

Esto se adapta a roles de backend, infraestructura y herramientas de prueba (test-tooling). La guía de entrevistas técnicas de Microsoft evalúa explícitamente las pruebas, los límites y las implicaciones de seguridad; la guía para SDE de Amazon incluye la programación y temas centrales de software. La señal que busca la entrevista es el ciclo de ingeniería, no la memorización de un comando.

Qué está evaluando el entrevistador

  • Distinguir los valores esperados de una prueba de ejemplo frente a las propiedades de una prueba de fuzzing.
  • Elegir un objetivo rápido y sin efectos secundarios externos.
  • Explicar el corpus de semillas, la guía por cobertura, la minimización y la reproducción (replay).
  • Manejar UTF-8 inválido, entradas vacías, entradas de tamaño excesivo y límites de recursos.
  • Conectar el descubrimiento, el diagnóstico, la reparación, la reejecución y el corpus confirmado (committed).

Una respuesta débil dice "generar entradas aleatorias". Una respuesta sólida nombra el invariante, los comandos, la ruta de la falla y la división en CI.

Aclaraciones antes de responder

  1. ¿La entrada del parser es un string, []byte o varios campos? Esto determina los argumentos de fuzz y la codificación del corpus.
  2. ¿Qué entradas inválidas representan errores esperados? Un contrato de errores cambia la aserción.
  3. ¿Cuál es el presupuesto de recursos por llamada? Una entrada lenta puede ser una señal de denegación de servicio.
  4. ¿Buscamos panics, errores semánticos o regresiones de compatibilidad? Cada uno necesita propiedades y semillas diferentes.

Estructura de respuesta de 30 segundos

Comienzo con una propiedad verificable, como "la codificación después del parseo preserva la misma estructura" o "una entrada inválida devuelve un error controlado". Agrego casos de borde reales con f.Add y luego mantengo f.Fuzz rápido, determinista y acotado. Un go test ordinario ejecuta las semillas y las fallas guardadas; un trabajo de CI independiente ejecuta go test -fuzz con un límite de tiempo. Go minimiza una entrada fallida en testdata/fuzz/<name>. Después de corregir la causa raíz, reproduzco esa entrada, ejecuto toda la suite de pruebas y confirmo (commit) el corpus para que el descubrimiento se convierta en una restricción de regresión.

Respuesta detallada paso a paso

1. Comenzar con una propiedad, no con una respuesta esperada

Para un parser, utiliza una propiedad de ida y vuelta (round-trip): Encode(Parse(x)) debe representar x después de la normalización. Para una transformación de strings, usa idempotencia, preservación de longitud o validez de UTF-8. Permite la normalización especificada; no confundas la disposición de bytes con el contrato.

2. Construir un objetivo de fuzzing acotado

go
func FuzzParseRoundTrip(f *testing.F) {
    f.Add([]byte("name=alice"))
    f.Add([]byte{})
    f.Fuzz(func(t *testing.T, input []byte) {
        t.Helper()
        if len(input) > 1<<20 {
            t.Skip()
        }
        got, err := Parse(input)
        if err != nil {
            return
        }
        again, err := Parse(Encode(got))
        if err != nil || !Equal(got, again) {
            t.Fatalf("round trip failed: %v", err)
        }
    })
}

Los tipos y el orden de f.Add deben coincidir con la función de devolución de llamada (callback). El objetivo no debe escribir archivos, llamar a la red ni depender del tiempo; de lo contrario, el fuzzing en paralelo produce fallas no reproducibles.

3. Sembrar límites de negocio

Las semillas deben cubrir versiones del protocolo, valores vacíos, campos duplicados, texto que no sea ASCII, truncamiento y muestras saneadas de producción. Go ejecuta las semillas durante las pruebas ordinarias, por lo que cada semilla debe ser ligera y estable.

4. Dividir los modos de ejecución

Antes de un commit, ejecuta go test -run=FuzzParseRoundTrip para verificar las semillas. Un trabajo dedicado puede ejecutar go test -fuzz=FuzzParseRoundTrip -fuzztime=10s. La bandera de tiempo limita la exploración; CI gestiona el paralelismo y los presupuestos por paquete en lugar del código de producción.

5. Reproducir y diagnosticar la entrada minimizada

Go minimiza una entrada que aún reproduce la falla y la escribe en testdata/fuzz/FuzzParseRoundTrip/. Reproduce con go test -run=FuzzParseRoundTrip/<id> y luego clasifica la causa raíz: aserción, panic, agotamiento de recursos o no determinismo de la prueba. La muestra debe explicar el error, no ser un blob opaco.

6. Hacer que la corrección sea permanente

Reproduce la falla después de la corrección y luego ejecuta todos los go test. Las fallas guardadas también se ejecutan sin -fuzz, así que revisa el corpus en busca de secretos, redacción de datos y límites de tamaño antes de confirmarlo en el repositorio.

7. Medir la calidad del fuzzing

Si la cobertura deja de crecer, agrega semillas estructuradas o mejora la generación en lugar de solo aumentar la duración. Las fallas frecuentes por tiempo de espera (timeout) requieren entradas más pequeñas o el aislamiento de rutas costosas; trátalas como defectos de rendimiento. Una tasa de descubrimiento a la baja no es prueba de corrección.

Ejemplo de respuesta de alta calidad

Defino el fuzzing en torno a una propiedad reproducible, no a un resultado de negocio esperado para cada entrada aleatoria. Para un parser, la entrada válida debe parsearse y volver a codificarse en la misma estructura; la entrada inválida debe devolver un error controlado y nunca entrar en panic. Siembro entradas vacías, el tamaño máximo aceptado, campos duplicados, Unicode y casos de producción saneados. El objetivo está acotado y libre de efectos secundarios. Los desarrolladores ejecutan go test -run=FuzzX; CI explora con un -fuzztime fijo. Cuando Go escribe una falla minimizada, la reproduzco, identifico la causa raíz, corrijo la implementación, ejecuto las pruebas ordinarias y el fuzzing, y hago commit de testdata/fuzz/FuzzX. De este modo, un descubrimiento se convierte en una verificación de regresión en cada cambio.

Errores comunes

  • Tratar el fuzzing como una carga aleatoria → no existe un oráculo → define primero un invariante y un contrato de errores.
  • Llamar a un servicio en vivo desde el objetivo → la red y el estado hacen que las fallas sean intermitentes (flaky) → utiliza un fake determinista.
  • Ejecutar fallas guardadas únicamente en modo fuzz → las correcciones pueden sufrir regresiones → consérvalas en testdata/fuzz.
  • Aceptar entradas no acotadas → un solo caso consume el presupuesto → impón un límite y registra las omisiones.
  • Hacer commit de cada caso generado → ruido y crecimiento del repositorio → conserva los casos que reproducen errores o cubren ramas clave.

Preguntas de seguimiento y respuestas

¿Es una aserción de ida y vuelta (round-trip) demasiado estricta cuando el parseo normaliza la entrada?

Sí. Compara árboles de sintaxis abstracta (AST) normalizados o conjuntos de campos y especifica qué diferencias de ordenamiento, espacios en blanco o mayúsculas/minúsculas se ignoran intencionalmente.

¿Se puede hacer commit de una muestra fallida que contenga secretos de usuario?

No. Revisa credenciales y datos personales, redacta y verifica que la muestra redactada siga fallando. Si no se puede confirmar de forma segura, mantén la reproducción interna controlada y envía un equivalente sintético.

El presupuesto de CI es de cinco minutos. ¿Cómo debería encajar el fuzzing?

Ejecuta las semillas y las fallas conocidas en cada build. Limita la exploración por tiempo, paquetes y recursos; registra los tiempos de espera en lugar de omitirlos silenciosamente.

¿Cómo saber si la prueba de fuzzing está rota?

Reproduce el caso minimizado e inspecciona el estado global, la aleatoriedad y la temporización de las goroutines. Luego escribe una prueba unitaria determinista; si es intermitente, corrige el aislamiento antes de tocar el código de producción.

¿Pueden múltiples objetivos compartir un corpus?

Comparte la lógica de conversión solo cuando el formato de entrada y la semántica coincidan. Mantén directorios e invariantes separados para que el corpus de un objetivo no pueda ocultar una brecha de cobertura de otro.

Fuentes públicas

Preguntas relacionadas

Herramienta de entrevista relacionada

Usa Captura para un ejercicio de código

Captura el problema y luego aborda en orden las restricciones, la solución, el código, los casos extremos y la complejidad.

Ver la herramienta