Fuzz-тестирование в Go
Зачем знать: Fuzz-тесты ищут падения там, где разработчик не подумал — невалидные данные, пограничные случаи, странные кодировки. С Go 1.18 fuzzing встроен в
testing, а в 2026 он уже выловил сотни багов в stdlib (net/http,encoding/json,archive/tar). На middle 1 от вас ждут понимания: где fuzz применим (парсеры, валидаторы, decoder’ы), как читать crash-файлы, как комбинировать fuzz с race-детектором и интегрировать в CI. Без этих знаний сложно отвечать за надёжность critical-сервиса.
Содержание
Заголовок раздела «Содержание»- Базовая концепция
- Под капотом / Best practices
- Gotchas
- Производительность
- Вопросы на собеседовании
- Practice
- Источники
1. Базовая концепция
Заголовок раздела «1. Базовая концепция»Fuzz testing (fuzzing) — автоматическая подача случайных или направленных-случайных данных в функцию, чтобы найти input, на котором она падает (panic), зацикливается или ведёт себя некорректно (например, нарушает инварианты).
В отличие от unit-теста:
| Аспект | Unit | Fuzz |
|---|---|---|
| Цель | Проверить заданный кейс | Найти кейс, на котором падает |
| Входы | Захардкодены | Сгенерированы (random + corpus-guided) |
| Длительность | Миллисекунды | Секунды-часы (на CI обычно 30-300 секунд per fuzz) |
| Что считается fail | Различие got vs want | panic, error, нарушение инварианта |
| Когда писать | Всегда | Парсеры, валидаторы, decoder, security-критика |
История
Заголовок раздела «История»- До Go 1.18: внешний инструмент
dvyukov/go-fuzz. Требовал отдельной компиляции, отдельной command-line утилиты. - Go 1.18 (март 2022):
testing.Fвстроен в stdlib. Аналог: компиляция с инструментацией, генерация corpus, crash-сохранение — всё черезgo test. - 2023-2025: fuzz нашёл реальные баги в
encoding/json,net/http,archive/tar,regexpи др. - 2026: рекомендован для CI на security-чувствительных сервисах.
Минимальный пример
Заголовок раздела «Минимальный пример»package myparser
import "testing"
// FuzzParse тестирует Parse на random входах.func FuzzParse(f *testing.F) { // Корпус — стартовые seed'ы. f.Add([]byte(`{"a":1}`)) f.Add([]byte(`{}`)) f.Add([]byte(`null`))
f.Fuzz(func(t *testing.T, data []byte) { // Эта функция вызывается с разными data: сначала seed, потом random. v, err := Parse(data) if err != nil { // err допустим, паника — нет. return } // Property: парсинг идемпотентен. re, err := json.Marshal(v) if err != nil { t.Fatalf("re-marshal: %v", err) } v2, err := Parse(re) if err != nil { t.Fatalf("re-parse: %v", err) } if !reflect.DeepEqual(v, v2) { t.Fatalf("not idempotent") } })}Запуск:
go test -fuzz=FuzzParse -fuzztime=30sЕсли фуззер находит вход, на котором тест падает, он сохраняет файл в testdata/fuzz/FuzzParse/<hash> — после этого этот вход становится частью обычных тестов: go test ./... пройдёт по нему как regression.
2. Под капотом / Best practices
Заголовок раздела «2. Под капотом / Best practices»2.1 Структура fuzz-теста
Заголовок раздела «2.1 Структура fuzz-теста»func FuzzXxx(f *testing.F) { // 1) Seed corpus: примеры валидных/интересных входов. f.Add(seed1) f.Add(seed2)
// 2) Можно подгрузить корпус из файла программно. // Корпус из testdata/fuzz/FuzzXxx подхватывается автоматически.
// 3) Сама функция тестирования. f.Fuzz(func(t *testing.T, args ...) { // args — те же типы, что и f.Add(...). // 1 аргумент — 1 параметр fuzz. })}Допустимые типы аргументов в f.Fuzz/f.Add:
string,[]byteint,int8-int64,uint,uint8-uint64,rune,bytefloat32,float64bool
Сложные типы (struct, slice кроме []byte) не поддерживаются — нужно сериализовать в строку/байты и разобрать внутри.
2.2 f.Add vs testdata/fuzz/
Заголовок раздела «2.2 f.Add vs testdata/fuzz/»Два источника seed corpus:
f.Add(...)— внутри fuzz-теста, в коде. Эти seeds выполняются всегда (даже в обычномgo test).testdata/fuzz/FuzzName/...— файлы со специальным форматом. Загружаются автоматически.
Формат файла corpus:
go test fuzz v1[]byte("hello\xff")int(42)Каждая строка — тип(значение). Чтобы вручную добавить корпус — обычно проще писать f.Add(), а файлы появляются сами от падений или gotip save.
2.3 Запуск
Заголовок раздела «2.3 Запуск»# Только regression (без fuzzing) — стандартный тест:go test ./parser
# Запуск fuzzing на 30 секунд:go test -fuzz=FuzzParse -fuzztime=30s ./parser
# С race detector:go test -fuzz=FuzzParse -fuzztime=30s -race ./parser
# Параллельно (по умолчанию = GOMAXPROCS):go test -fuzz=FuzzParse -parallel=8 -fuzztime=1m
# Воспроизведение crash:go test -run=FuzzParse/abcdef12345 ./parser-fuzztime может принимать 30s, 5m, 2h или count’у запусков: 100x, 1000000x.
2.4 Что попадает в testdata/fuzz/
Заголовок раздела «2.4 Что попадает в testdata/fuzz/»При нахождении вход, на котором тест падает, fuzzer записывает входы (по одному файлу на параметр функции) в:
testdata/fuzz/FuzzParse/<sha256-hash>Эти файлы должны коммититься в репозиторий. После коммита:
go test ./...без-fuzzпройдёт по ним как regression.- Если кто-то починит баг, файл всё равно остаётся — гарантия, что не вернётся.
2.5 Reproducing crash
Заголовок раздела «2.5 Reproducing crash»go test -run=FuzzParse/abc123def ./parserЗдесь abc123def — имя файла из testdata/fuzz/FuzzParse/. Это уникальный кейс, который раньше падал. Полезно для отладки в IDE: ставишь breakpoint, запускаешь конкретный.
2.6 Coverage-guided fuzzing
Заголовок раздела «2.6 Coverage-guided fuzzing»go test -fuzz использует coverage-guided fuzzing (как AFL/libFuzzer):
- Компилирует код с дополнительной инструментацией для отслеживания, какие branch’и (basic blocks) выполняются.
- Запускает функцию с входом.
- Если новый вход покрыл новую ветку — добавляет его в очередь интересных.
- Мутирует интересные входы (byte flips, arithmetic, splice) — генерирует новые.
Это эффективнее random: миллионы random попыток дадут мало coverage, а guided — за минуты доходит до глубоких веток.
Под капотом: Go компилирует с -buildmode=... инструментацией (внутри cmd/go), хранит coverage map в shared memory между runner и worker.
2.7 Запуск параллельно (workers)
Заголовок раздела «2.7 Запуск параллельно (workers)»Fuzzer спавнит N workers (по умолчанию GOMAXPROCS). Каждый worker:
- Хранит свой PRNG.
- Получает задание от координатора.
- Запускает функцию, отчитывается о покрытии.
- Если crash — координатор минимизирует input (shrinks) и сохраняет.
2.8 Differential fuzzing
Заголовок раздела «2.8 Differential fuzzing»Сравнение двух реализаций одной функции:
func FuzzDecodeCompare(f *testing.F) { f.Add([]byte(`hello`)) f.Fuzz(func(t *testing.T, data []byte) { gotA, errA := decoderA(data) gotB, errB := decoderB(data)
if (errA == nil) != (errB == nil) { t.Fatalf("err mismatch: A=%v B=%v on %q", errA, errB, data) } if errA == nil && !bytes.Equal(gotA, gotB) { t.Fatalf("diff: A=%x B=%x on %q", gotA, gotB, data) } })}Полезно для:
- Своя реализация vs reference (stdlib).
- Старая vs новая (refactoring).
- Compatible decoders (JSON variant A vs B).
2.9 Property-based vs fuzz
Заголовок раздела «2.9 Property-based vs fuzz»Похожи, но разные:
| Аспект | Property-based (rapid, gopter) | Fuzz (testing.F) |
|---|---|---|
| Генератор | Структурный (Int, String, struct) | Byte-уровень + coverage-guided |
| Цель | Проверить свойство на N кейсах | Найти crash на любом входе |
| Типы аргументов | Любые (struct, slice) | Только примитивы |
| Минимизация (shrink) | Да | Да (но проще) |
| Сохранение seed’ов | Нет (или вручную) | Автоматически (testdata) |
| Coverage-guided | Нет | Да |
| Long-running | Обычно секунды | Минуты-часы |
В реальных проектах используют оба: property для бизнес-инвариантов (на структурах), fuzz для парсеров и I/O.
2.10 OSS-Fuzz
Заголовок раздела «2.10 OSS-Fuzz»Google OSS-Fuzz (https://google.github.io/oss-fuzz/) — бесплатное непрерывное fuzzing для open-source проектов. Подключают:
- Регистрируешь проект в OSS-Fuzz.
- Описываешь
project.yaml,Dockerfile,build.sh. - Указываешь fuzz targets (
FuzzXxxфункции). - Google запускает их 24/7 на их инфраструктуре, репортит баги.
Многие популярные Go проекты (containerd, etcd, prometheus, kubernetes utils) — в OSS-Fuzz.
2.11 Best practices: что фуззить
Заголовок раздела «2.11 Best practices: что фуззить»Хорошие кандидаты:
- Парсеры: JSON, YAML, TOML, XML, HTTP-headers, URL, S-expressions, configs.
- Decoder’ы: protobuf, MsgPack, base64, ASN.1.
- Валидаторы: regex, email, URL, IBAN.
- State machines с явным входом (FSM по байтам).
- Криптография (с осторожностью).
Плохие кандидаты:
- Pure compute без ветвлений (например,
fmt.Sprintf("%d", x)). - Deterministic logic без зависимости от данных (сортировка интов — будет coverage один и тот же).
- Сетевые операции — flaky.
- Что требует БД/файлы — slow + non-deterministic.
2.12 Real-world bugs
Заголовок раздела «2.12 Real-world bugs»Примеры реальных багов, найденных fuzz в Go:
encoding/json: panic на конкретных формах nested arrays.archive/tar: out-of-bounds read.net/http: CRLF injection в HeaderValues.regexp: stack overflow на регэкспах с глубокой рекурсией.golang.org/x/text: панические Unicode normalisations.
Google и Cloudflare регулярно публикуют отчёты о найденных багах через fuzz.
2.13 Идеи как структурировать fuzz
Заголовок раздела «2.13 Идеи как структурировать fuzz»Idempotency
Заголовок раздела «Idempotency»func FuzzMarshalRoundTrip(f *testing.F) { f.Add([]byte(`{"a":1}`)) f.Fuzz(func(t *testing.T, data []byte) { var v any if err := json.Unmarshal(data, &v); err != nil { return } b, err := json.Marshal(v) if err != nil { t.Fatalf("marshal: %v", err) } var v2 any if err := json.Unmarshal(b, &v2); err != nil { t.Fatalf("re-unmarshal: %v err=%v", b, err) } if !reflect.DeepEqual(v, v2) { t.Fatalf("roundtrip mismatch") } })}No panic (smoke test)
Заголовок раздела «No panic (smoke test)»func FuzzNoPanic(f *testing.F) { f.Add([]byte("hello")) f.Fuzz(func(t *testing.T, data []byte) { defer func() { if r := recover(); r != nil { t.Fatalf("panic: %v", r) } }() _ = MyParse(data) })}(на самом деле, в testing.F panic в fuzz-функции и так считается failure — recover не обязателен, но добавляет message.)
Invariants
Заголовок раздела «Invariants»func FuzzSorted(f *testing.F) { f.Add(int64(42), int64(7), int64(15)) f.Fuzz(func(t *testing.T, a, b, c int64) { sorted := mySort([]int64{a, b, c}) for i := 1; i < len(sorted); i++ { if sorted[i-1] > sorted[i] { t.Fatalf("not sorted: %v", sorted) } } })}3. Gotchas
Заголовок раздела «3. Gotchas»3.1 Только примитивные типы
Заголовок раздела «3.1 Только примитивные типы»f.Add(MyStruct{...}) // КОМПИЛЯЦИЯ НЕ ПРОЙДЁТНельзя struct, []T (кроме []byte). Сериализуйте: f.Add([]byte(buf)), внутри f.Fuzz разворачивайте.
3.2 Несовпадение типов в f.Add и f.Fuzz
Заголовок раздела «3.2 Несовпадение типов в f.Add и f.Fuzz»f.Add(int(42))f.Fuzz(func(t *testing.T, x int64) {}) // panic: типы int и int64 не совпадаютtesting.F строго проверяет: типы f.Add(...) должны совпадать точно с типами в f.Fuzz (после первого аргумента *testing.T).
3.3 Slow fuzz function = slow fuzzing
Заголовок раздела «3.3 Slow fuzz function = slow fuzzing»Если ваша функция работает 100ms, за минуту будет всего 600 итераций. Эффективный fuzz требует функций в микросекунды.
Оптимизируйте: убрать I/O, БД, аллокации, лишние логи.
3.4 Бесконечные циклы
Заголовок раздела «3.4 Бесконечные циклы»Если функция зацикливается, fuzz зависнет. Используйте timeout внутри:
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)defer cancel()_ = MyFunc(ctx, data)Или ограничьте размер data:
if len(data) > 1<<20 { return // skip too large inputs}3.5 Crash в setup или global state
Заголовок раздела «3.5 Crash в setup или global state»var counter int
func FuzzBad(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { counter++ // RACE между workers! // ... })}Workers запускаются параллельно. Global state — RACE. Используйте локальные переменные.
3.6 testdata/fuzz и .gitignore
Заголовок раздела «3.6 testdata/fuzz и .gitignore»Файлы из testdata/fuzz/FuzzName/ должны коммититься (regression suite). Не добавляйте в .gitignore.
Но! testdata/fuzz/FuzzName/cache/ — кэш фаззера (большой), его игнорируем. По умолчанию хранится в $GOCACHE/fuzz/.
3.7 Coverage в fuzz
Заголовок раздела «3.7 Coverage в fuzz»go test -fuzz=... -cover работает, но coverage от fuzz runs обычно невысокий (фокус на crash, не на coverage отчёт). Для покрытия — отдельная сессия go test -cover.
3.8 Fuzz и build tags
Заголовок раздела «3.8 Fuzz и build tags»//go:build fuzz
package mypkg
func FuzzX(f *testing.F) { ... }Можно изолировать fuzz-тесты от обычных, если они тяжёлые. Запускать: go test -tags=fuzz -fuzz=FuzzX.
3.9 -fuzz несовместим с другими флагами
Заголовок раздела «3.9 -fuzz несовместим с другими флагами»go test -fuzz=FuzzX -run=TestY # ошибкаМожно запускать только один fuzz target за раз. Параметр -run фильтрует regression-тесты в том же запуске, но -fuzz=FuzzX запускает именно FuzzX.
3.10 testing.T vs testing.F
Заголовок раздела «3.10 testing.T vs testing.F»В f.Fuzz(func(t *testing.T, data []byte) {...}) внутри t — это *testing.T, не *testing.F. Используйте t.Errorf, t.Fatalf, t.Helper как обычно.
*testing.F доступен только в FuzzXxx для f.Add, f.Fuzz, f.Skip (и наследует методы T).
3.11 Параллельность
Заголовок раздела «3.11 Параллельность»t.Parallel() внутри f.Fuzz не имеет смысла — fuzz runner и так запускает параллельно. Не пишите t.Parallel() внутри.
3.12 Минимизация (shrinking)
Заголовок раздела «3.12 Минимизация (shrinking)»Когда fuzzer находит crash, он минимизирует вход: пытается убрать байты/уменьшить числа, чтобы найти minimal failing input. Это автоматически, но если ваша функция долгая, минимизация займёт время.
3.13 Невоспроизводимый crash
Заголовок раздела «3.13 Невоспроизводимый crash»Если функция использует goroutine, time, randomness — crash может быть невоспроизводим:
go test -run=FuzzX/abc123 # PASSgo test -run=FuzzX/abc123 # FAILЛечение: детерминизм. Передавайте rand.Source, clock, не запускайте goroutine.
3.14 Hard-to-discover crashes
Заголовок раздела «3.14 Hard-to-discover crashes»Глубоко зарытые баги (нужна конкретная последовательность из 100 байт) fuzzer может не найти за час, но найти за день. Не отчаивайтесь.
3.15 Fuzz на CI
Заголовок раздела «3.15 Fuzz на CI»Идиомы:
- run: go test -fuzz=FuzzParse -fuzztime=10m ./parser # потом артефакт testdata/fuzz сохраняемНо не на каждый PR — это долго. Обычно: nightly или manual.
3.16 Test cache vs fuzz cache
Заголовок раздела «3.16 Test cache vs fuzz cache»Go хранит:
$GOCACHE/test/— кэшgo testрезультатов.$GOCACHE/fuzz/<pkg>/<FuzzName>/— наработки фаззера (coverage map, interesting inputs).
При запуске fuzz он использует свой кэш — даже если вы запускаете повторно, не начинает с нуля. Чтобы reset:
go clean -fuzzcacheПолезно: между major переписками функции.
3.17 fuzz и go modules
Заголовок раздела «3.17 fuzz и go modules»Fuzz работает только для пакетов внутри текущего модуля. Нельзя go test -fuzz=... для зависимости из vendor.
3.18 Fuzz arguments vs seed types
Заголовок раздела «3.18 Fuzz arguments vs seed types»f.Add строго проверяет типы:
f.Add(int64(42))f.Fuzz(func(t *testing.T, x int) {}) // PANIC: int64 != intИспользуйте те же типы. int и int64 — разные!
3.19 Несколько fuzz target’ов в одном пакете
Заголовок раздела «3.19 Несколько fuzz target’ов в одном пакете»Можно. Но запускается только один за раз:
func FuzzA(f *testing.F) { ... }func FuzzB(f *testing.F) { ... }go test -fuzz=FuzzA ./pkg# Если -fuzz=Fuzz — runner вернёт ошибку (несколько targets матчатся).3.20 Fuzz и flakiness
Заголовок раздела «3.20 Fuzz и flakiness»Fuzz должен быть детерминистичен на одном входе: дважды запустили с тем же data → одинаковый результат. Иначе minimisation и reproduction не работают.
Если функция использует random, time, goroutines — это проблема. Решения:
- Передавать
rand.Sourceизвне. - Использовать
time.Timeпараметром, неtime.Now(). - Не запускать goroutines в fuzz-функции.
4. Производительность
Заголовок раздела «4. Производительность»4.1 Скорость fuzz function
Заголовок раздела «4.1 Скорость fuzz function»Цель — функция в микросекунды. Замеряйте:
func BenchmarkParse(b *testing.B) { data := []byte(`{"a":1}`) for i := 0; i < b.N; i++ { _, _ = Parse(data) }}Если BenchmarkParse показывает 50µs/op, за минуту fuzz сделает ~1.2M вызовов на 1 core, ~10M на 8 cores. Это нормально.
4.2 Аллокации
Заголовок раздела «4.2 Аллокации»Каждая аллокация — ~50ns. Если функция аллоцирует 100 объектов на вызов — 5µs на GC. Используйте sync.Pool, переиспользуйте буферы (но осторожно — fuzz mutates input!).
f.Fuzz(func(t *testing.T, data []byte) { // НЕ делайте: cached := buf.Get() и кешируйте data в Pool — между вызовами!})4.3 Параллельность
Заголовок раздела «4.3 Параллельность»-parallel=N или -fuzz сама использует GOMAXPROCS. На 16-ядерном — будет 16 workers. Каждый кушает ~1 core.
4.4 Память
Заголовок раздела «4.4 Память»Worker’ы держат corpus в RAM. Большой corpus = много памяти. Очищайте старые с go clean -fuzzcache.
4.5 Race detector + fuzz
Заголовок раздела «4.5 Race detector + fuzz»go test -race -fuzz=FuzzX -fuzztime=1m. Замедление в 2-10x. Но race-баги без race-детектора не найти. Рекомендация: запускать race fuzz отдельно от обычного, например, nightly.
4.6 Memory leak в fuzz
Заголовок раздела «4.6 Memory leak в fuzz»Если ваша функция течёт памятью, после миллионов итераций OOM. Тестируйте на benchmark с b.N=1000000.
4.7 Sanitizer’ы
Заголовок раздела «4.7 Sanitizer’ы»Race detector — единственный встроенный sanitizer в Go (нет ASan/MSan как в C++). Но Go и так memory-safe — большинство ошибок это panic/nil deref, а не buffer overflow. Race + fuzz покрывают большинство проблем.
4.8 Coverage-guided эффективность
Заголовок раздела «4.8 Coverage-guided эффективность»С coverage-guided мутацией fuzzing на порядки эффективнее random:
- Random: 10⁶ попыток дают <1% coverage сложного парсера.
- Coverage-guided: 10⁵ попыток достигают 80%+.
4.9 Distributed fuzzing
Заголовок раздела «4.9 Distributed fuzzing»OSS-Fuzz распределяет fuzzing на десятки CPU. Локально — параллельно на одном хосте. Distributed multi-host для Go в stdlib нет — нужны внешние решения (ClusterFuzz).
4.10 Fuzzing throughput метрики
Заголовок раздела «4.10 Fuzzing throughput метрики»Fuzzer выводит:
fuzz: elapsed: 0s, gathering baseline coverage: 0/156 completedfuzz: elapsed: 3s, gathering baseline coverage: 156/156 completed, now fuzzing with 8 workersfuzz: elapsed: 6s, execs: 12345 (4115/sec), new interesting: 12 (total: 168)execs/sec— основная метрика. Целевые значения:-
100k/sec — отличная функция (мелкая, без I/O).
- 10k-100k/sec — норма для парсеров.
- <1k/sec — медленно, оптимизируйте функцию.
-
new interesting— растёт первые минуты, потом плато. Если плато низкое — coverage слабый, фаззер не нашёл новых веток.
4.11 Memory profiling fuzz
Заголовок раздела «4.11 Memory profiling fuzz»go test -fuzz=FuzzX -fuzztime=30s -memprofile=mem.profgo tool pprof mem.profЕсли функция аллоцирует на каждой итерации — pprof покажет hot path. Оптимизируйте через sync.Pool (но не мутируйте input).
4.12 CPU profiling fuzz
Заголовок раздела «4.12 CPU profiling fuzz»go test -fuzz=FuzzX -fuzztime=30s -cpuprofile=cpu.profgo tool pprof cpu.profВидно, где функция тратит время. Часто узкое место — JSON/regex/reflection.
4.13 Параллельность и заминка
Заголовок раздела «4.13 Параллельность и заминка»При -parallel=N каждый worker — отдельный Go process. Память умножается на N. На 16-ядерной машине с тяжёлой setup-функцией memory может вылететь.
Лечение: уменьшить -parallel, либо облегчить setup.
4.14 Fuzz target compose
Заголовок раздела «4.14 Fuzz target compose»Можно вызывать другую функцию из fuzz:
func FuzzAll(f *testing.F) { f.Add([]byte("...")) f.Fuzz(func(t *testing.T, data []byte) { result1 := ParseA(data) result2 := ParseB(result1) result3 := ParseC(result2) // конец цепочки })}Эффективно: один прогон тестит всю pipeline. Но shrinking сложнее — фаззер не знает, в каком этапе крах.
5. Вопросы на собеседовании
Заголовок раздела «5. Вопросы на собеседовании»1. Что такое fuzz testing? Автоматическая подача случайных или направленных-случайных данных в функцию для поиска панических ситуаций, ошибок, нарушений инвариантов.
2. С какой версии Go fuzz встроен в stdlib?
Go 1.18 (март 2022). До этого был dvyukov/go-fuzz.
3. Что такое f.Add?
Добавляет seed-вход в corpus. Запускается всегда, плюс используется как стартовая точка для мутаций fuzzer’ом.
4. Какие типы можно использовать в f.Add / f.Fuzz?
Примитивные: string, []byte, целые (int, int32, …), uint*, float32/64, bool, rune, byte. Не: struct, slice кроме []byte.
5. Что делает -fuzztime?
Длительность fuzzing-сессии. -fuzztime=30s — 30 секунд, -fuzztime=100x — 100 итераций.
6. Где сохраняются падающие inputs?
В testdata/fuzz/FuzzName/<hash>. Эти файлы коммитятся в репозиторий как regression suite.
7. Как воспроизвести crash?
go test -run=FuzzName/HASH ./pkg — запустит конкретный crash как regular test.
8. Что такое coverage-guided fuzzing? Фаззер отслеживает, какие code branches выполняются. Если новый input покрыл новую ветку — добавляется в очередь и мутируется. Эффективнее random.
9. В чём разница fuzz и property-based? Property-based генерирует структурно (struct, slice), не coverage-guided. Fuzz — на байтовом уровне, coverage-guided, сохраняет corpus автоматически.
10. Что фуззить, а что нет? Парсеры, декодеры, валидаторы, security-критичный код — да. Pure compute, простую логику без ветвлений — нет.
11. Можно ли совмещать fuzz и -race?
Да: go test -fuzz=FuzzX -race. Замедляет 2-10x, но ловит data races.
12. Что такое minimisation (shrinking)? После crash фаззер пытается уменьшить input до минимально воспроизводимого. Помогает разработчику быстрее понять баг.
13. Что такое differential fuzzing? Подаём один и тот же вход двум реализациям (своя vs reference). Если результаты разные — баг.
14. Что такое OSS-Fuzz? Сервис Google, бесплатно фуззит open-source проекты на их инфраструктуре. Поддерживает Go с 2020.
15. Параллельность fuzz?
По умолчанию GOMAXPROCS worker’ов. Можно -parallel=N.
16. Что произойдёт, если fuzz-функция бесконечно зацикливается? Worker зависнет. Используйте timeout внутри (context) или ограничивайте размер input.
17. Как fuzz tests интегрируются в CI?
Обычно nightly или manual: -fuzztime=5m дополнительно к regular tests. Crash-файлы автоматически становятся regression-тестами.
18. Какие реальные баги нашёл fuzz в stdlib?
В encoding/json, net/http, archive/tar, regexp — десятки issues с 2020 года.
19. Когда не делать fuzz? Pure compute без ветвлений (сортировка интов), deterministic logic (HMAC от фиксированного ключа без branch’ей по входу).
20. Что лучше: fuzz или unit-тесты? Это разные инструменты. Unit — для конкретных кейсов. Fuzz — для поиска неизвестных кейсов. В большом проекте — оба.
21. Можно ли использовать struct в f.Fuzz?
Нет, только примитивные типы. Сериализуйте struct в []byte (например, через gob или binary), разворачивайте внутри.
22. Что произойдёт, если fuzz-функция panic’ует?
Считается failure, вход сохраняется в testdata/fuzz/FuzzName/<hash>. Минимизируется.
23. Что такое seed corpus?
Стартовые входы для фаззера: через f.Add или файлы в testdata/fuzz/. Без них фаззер начинает с пустых байтов.
24. Как фаззер мутирует входы? Byte flips (XOR bit), arithmetic changes (+1, -1), splice (combine two inputs), insert/delete bytes, dictionary substitution.
25. Почему fuzz coverage-guided эффективнее random? Сохраняет интересные входы (которые открыли новую ветку) и мутирует их. Random не имеет памяти — миллионы попыток на одном и том же.
26. Как организовать fuzz target для security audit? Декомпозируйте: парсер → валидатор → бизнес-логика. Каждый этап — отдельный fuzz target. Так minimisation легче.
27. Как минимизация работает? Когда найден crash, фаззер пробует уменьшить вход (отрезать байты, уменьшить числа), пока тест ещё падает. Итог — минимальный воспроизводимый вход.
28. Можно ли использовать t.Parallel внутри f.Fuzz?
Технически да, но бессмысленно: fuzz runner и так параллелит инвокации. Не используйте.
29. Что выводит go test -fuzz=FuzzX -v?
Каждые ~1 секунду — elapsed, execs (вызовов), execs/sec, new interesting (новых интересных входов).
30. Что делать, если fuzz нашёл баг, который сложно воспроизвести? Проверьте детерминизм: time, goroutines, random source. Если зависит — uplift в передаваемый параметр.
6. Practice
Заголовок раздела «6. Practice»Задача 1: Простой fuzz для парсера CSV
Заголовок раздела «Задача 1: Простой fuzz для парсера CSV»Напишите ParseCSV([]byte) ([][]string, error). Fuzz должен находить inputs, на которых ваш парсер panic’ует (включая \r\n, обрезанные кавычки, NUL-байты).
Задача 2: Round-trip fuzz для JSON
Заголовок раздела «Задача 2: Round-trip fuzz для JSON»Напишите fuzz FuzzJSONRoundTrip для собственного encoder/decoder. Property: decode(encode(x)) == x.
Задача 3: Differential fuzz
Заголовок раздела «Задача 3: Differential fuzz»Реализуйте свой base64Decode и сравните с encoding/base64.StdEncoding.DecodeString. Найдите расхождения через fuzz.
Задача 4: Fuzz валидатора email
Заголовок раздела «Задача 4: Fuzz валидатора email»Напишите ValidateEmail(s string) bool. Fuzz должен находить:
- email с
\nвнутри (не валидный, но ваш код пропускает). - слишком длинные локальные части (>64).
Задача 5: Fuzz state machine
Заголовок раздела «Задача 5: Fuzz state machine»Реализуйте FSM (например, простой парсер JSON-like). Fuzz должен находить последовательности байтов, на которых FSM попадает в недопустимое состояние.
Задача 6: Coverage оценка
Заголовок раздела «Задача 6: Coverage оценка»Запустите fuzz на 1 минуту с -fuzztime=1m, потом go test -cover — сравните покрытие до и после fuzz сессии (через изучение testdata/fuzz/).
Задача 7: Fuzz с invariant
Заголовок раздела «Задача 7: Fuzz с invariant»Реализуйте Sort([]int) []int. Fuzz проверяет: длина не изменилась, всё отсортировано, multiset исходного и результата совпадают.
Задача 8: Reproduce crash
Заголовок раздела «Задача 8: Reproduce crash»Создайте функцию с известным багом (например, divide(a, b) = a/b без проверки b==0). Запустите fuzz, найдите файл в testdata/fuzz/, исправьте баг, убедитесь что regression проходит.
7. Источники
Заголовок раздела «7. Источники»- Go fuzzing docs: https://go.dev/doc/fuzz/ — туториал и reference.
- Go Blog “Fuzzing is Beta Ready” (2021) и “Fuzzing in Go” (1.18): https://go.dev/blog/fuzz-beta, https://go.dev/blog/go1.18.
- testing.F docs: https://pkg.go.dev/testing#F.
- OSS-Fuzz Go docs: https://google.github.io/oss-fuzz/getting-started/new-project-guide/go-lang/.
- Article “Go Fuzzing in Practice” (Cloudflare blog, 2022).
- Уязвимости найденные fuzz: https://github.com/golang/go/issues?q=label%3AFuzzing.
- rapid (property-based): https://pkg.go.dev/pgregory.net/rapid.
- Talk “Native Go Fuzzing” Katie Hockman, GopherCon 2022.
- gophers slack #fuzzing channel — обсуждения практик.