Skip to content
PПромтбук
RUEN
04Тестирование

Дизайн fuzz-тестирования

Где fuzz даёт ROI: парсеры, валидаторы, сериализаторы. Corpus + мутации, что считать crash, tooling.

Спроектируй fuzz-тестирование для {{target}} на {{lang}}.

Базовая аксиома: fuzz не заменяет unit-тесты, а ищет то, чего ты не придумал. Если функция принимает байты / строки / JSON / число, у неё есть бесконечное входное пространство и человек его не охватит. Fuzz перебирает варианты, которые ломают инварианты — крэши, паники, утечки памяти, неправильный результат.

1. Где fuzz имеет ROI

ХорошоПлохо
Парсеры (URL, JSON, HTML, протоколы)UI-формы (лучше property-based на бизнес-логике)
Валидаторы / sanitizers (input → bool/Result)Чистый CRUD без преобразований
Сериализаторы / десериализаторыТонкая обёртка над библиотекой, которую и так фуззят
Декомпрессоры / декодеры (image, audio)Внешние API-вызовы — fuzz не достанет
Криптография и encoding (base64, hex, varint)UI-вёрстка, дизайн-системы
Любой код, обрабатывающий untrusted bytesЧистая бизнес-логика без сериализации
Round-trip пары (encode(decode(x)) == x)Зависящее от часов / I/O / сетки

Правило большого пальца: если функция чистая, имеет input от пользователя / сети / файла, и её сложно «прочитать в голове», — фуззь.

2. Property — что проверяем кроме «не упало»

«Не паникует» — слабая цель. Полные свойства:

СвойствоПример
No-panic / no-crashparse(any_bytes) returns Result, не падает
No UB / no memory errorASan + fuzz; нет use-after-free, out-of-bounds
Round-tripdecode(encode(x)) == x для любого x
Idempotencyf(f(x)) == f(x) (sanitize, normalize)
Equivalence to referencemy_parser(x) == serde_json::from_str(x)
Invariant preservationПосле insert дерево остаётся сбалансированным
No hangРаботает за < N ms для любого input размера M
Bounded memoryRSS < N MB для любого input

3. Что считать «crash»

СимптомСерьёзностьЧто делать
Сегфолт / abort / panicCriticalMin repro → fix → regression test
ASan/UBSan errorCriticalЧасто security CVE-уровень
Hang > N сек на input < M байтHighDoS-вектор, ограничить парсинг
OOM (быстрый рост памяти)HighBounded allocation, ограничение глубины
Wrong output (differs from reference)HighЛогическая бага
Slow path (×100 от среднего)MediumAlgorithmic complexity attack
Большая утечка между итерациямиMediumReset state, проверить Drop

4. Tooling по языкам

Rust

cargo install cargo-fuzz
cargo fuzz init
cargo fuzz add parse_target
# fuzz_targets/parse_target.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
    let _ = my_crate::parse(data);
});

cargo fuzz run parse_target -- -max_len=4096. AFL++: cargo-afl. Property-based — proptest / quickcheck (мельче зерном).

Go

testing (Go 1.18+) — нативный fuzz:

func FuzzParse(f *testing.F) {
    f.Add([]byte("{}"))           // seed
    f.Fuzz(func(t *testing.T, b []byte) {
        _, _ = parse(b)            // no panic
    })
}

go test -fuzz=FuzzParse -fuzztime=10m.

Python

  • Atheris (Google, libFuzzer-backed) — для C-расширений
  • Hypothesis — property-based, легче в обиходе:
from hypothesis import given, strategies as st
@given(st.text())
def test_parse_no_crash(s):
    parse(s)

JVM

  • Jazzer (libFuzzer для JVM) — для Java/Kotlin/Scala
  • jqwik — property-based

Node.js / TS

  • fast-check — property-based
  • jsfuzz / node-libfuzzer — coverage-guided

C / C++

  • libFuzzer (внутри clang) — стандарт
  • AFL++ — стенды, кластеры
  • honggfuzz — альтернатива

Структурированный input

Arbitrary (Rust), f.Fuzz с struct (Go), @composite (Hypothesis) — фуззишь не байты, а типизированные значения: AST, конфиги, protobuf.

5. Corpus и мутации

Corpus = seed-inputs, на которых fuzzer начинает мутации. Хороший corpus = быстрый coverage:

  • Положи реальные примеры (валидные JSON-доки, реальные URL'ы, реальные изображения)
  • Положи граничные кейсы (пустой, в 1 символ, в 1 МБ)
  • Положи то, что когда-то ломалось — каждый найденный bug превращается в новый corpus-файл (regression)
fuzz/corpus/parse_target/
  empty.bin
  minimal.json
  with-unicode.json
  deeply-nested.json
  bug-2025-05-01.bin   # был crash, теперь regression

Мутации (делает fuzzer сам):

  • bit flip, byte swap, splice двух inputs
  • insert / delete bytes
  • mutation guided by coverage (libFuzzer/AFL)

Dictionary — слова/токены протокола (http.dict, json.dict) — резко ускоряет coverage на структурированных форматах.

6. Coverage-guided vs random

ТипКтоКогда
Randomстарые fuzzer'ыОчень примитивно, медленно находит
Coverage-guidedlibFuzzer, AFL, Jazzer, Go nativeСтандарт сегодня
Structure-awareLibProtobufMutator, Arbitrary traitСложные форматы
Differentialтвой harnessСравниваешь две реализации
Symbolic / concolicKLEE, SAGEHardcore, для крит-секций

7. Что делать с найденным крэшем

  1. Min repro — fuzzer сам минимизирует (cargo fuzz tmin, go test -fuzz сохраняет в testdata)
  2. Regression test — добавь min repro в seed corpus / unit-suite
  3. Root cause — не «обёрни в try/catch», пойми инвариант
  4. Triage — security implication? Если да — приватный канал, эмбарго, CVE
  5. CI — гонять corpus как regression suite в каждом PR, полный fuzz — еженочно

8. CI стратегия

КогдаЧтоВремя
На каждый PRregression: прогнать корпус как unit-testсекунды
Nightlycoverage-guided fuzz30-60 мин на target
Долгий runOSS-Fuzz / ClusterFuzzдни/недели, бесплатно для OSS
После big-changefuzz по специфике изменения1-4 часа
# GH Actions — пример
- run: cargo fuzz run parse_target -- -max_total_time=600
- run: cargo fuzz tmin parse_target artifacts/...  # минимизация

9. Анти-паттерны

  • ❌ Фуззить функцию с побочными эффектами (пишет в БД, шлёт сеть) — fuzzer не очистит state
  • ❌ Фуззить с assert(true) — никакого свойства не проверяешь, кроме «не упало»
  • ❌ Игнорировать hang/timeout — DoS-вектор
  • ❌ Без sanitizer'ов (ASan/UBSan/MSan для C/Rust) — пропустишь memory issues
  • ❌ Без corpus → cold start, fuzzer часами ищет валидный input
  • ❌ Запустить раз и забыть — fuzz эффективен долго и регулярно
  • ❌ Не сохранять найденные crashes в regression — повторишь тот же баг
  • catch(...) { /* ignore */ } чтобы fuzz «не падал» — маскируешь баг, fuzz перестаёт находить новое
  • ❌ Фуззить через сеть / в живой системе — медленно (мс на input), бесполезно по сравнению с in-process
  • ❌ Использовать random unit-test вместо coverage-guided fuzz — пропускаешь interesting paths
  • ❌ Не диффить две реализации, когда есть reference (старый парсер vs новый) — упускаешь differential bugs
  • ❌ Hardcode Vec::with_capacity(input.len()) без cap — fuzz найдёт OOM за минуты

10. Что отдать

  • Список fuzz-target'ов с обоснованием ROI (что именно ищем)
  • Harness-код для каждого (5-20 строк)
  • Корпус: минимум 10 валидных + 5 граничных examples
  • Свойства, которые проверяются (no-panic + что-то ещё, см. §2)
  • CI-конфиг: regression на каждый PR + nightly fuzz job
  • Лог first-run: что нашли, как пофиксили, какие regression-тесты добавили
  • План on-going: кто смотрит nightly отчёты, SLA на найденные баги
К подразделу «Тестирование»
Похожие промты