Go Fuzzing
Go поддерживает fuzzing в своей стандартной toolchain, начиная с Go 1.18. Нативная поддержка Go fuzz-тестов поддерживается OSS-Fuzz.
Попробуйте учебник по fuzzing с использованием Go.
Обзор
Fuzzing — это вид автоматического тестирования, при котором непрерывно изменяются входные данные программы с целью выявления ошибок. Fuzzing в Go использует информацию о покрытии кода для интеллектуального перемещения по тестируемому коду, чтобы находить и сообщать о сбоях пользователю. Поскольку он может достигать граничных случаев, которые люди часто упускают, fuzz-тестирование особенно полезно для поиска эксплойтов и уязвимостей в безопасности.
Ниже приведен пример fuzz-теста, выделяющий его основные компоненты.

Написание fuzz-тестов
Требования
Ниже приведены правила, которым должны соответствовать fuzz-тесты.
- Fuzz-тест должен быть функцией с именем вида
FuzzXxx, принимающей только*testing.F, и не возвращающей никакого значения. - Fuzz-тесты должны находиться в файлах с расширением *_test.go, чтобы запускаться.
- Fuzz-таргет должен быть вызовом метода
(*testing.F).Fuzz, который принимает*testing.Tв качестве первого параметра, за которым следуют аргументы fuzzing. Возвращаемого значения нет. - В каждом fuzz-тесте должен быть ровно один fuzz-таргет.
- Все записи в seed-корпусе должны иметь типы, идентичные
аргументам fuzzing, в том же порядке.
Это относится к вызовам
(*testing.F).Addи любым файлам корпуса в директории testdata/fuzz fuzz-теста. - Аргументы fuzzing могут быть только следующих типов:
string,[]byteint,int8,int16,int32/rune,int64uint,uint8/byte,uint16,uint32,uint64float32,float64bool
Предложения
Ниже приведены предложения, которые помогут вам максимально эффективно использовать фаззинг.
- Цели фаззинга должны быть быстрыми и детерминированными, чтобы движок фаззинга мог работать эффективно, а новые ошибки и покрытие кода можно было легко воспроизвести.
- Поскольку цель фаззинга вызывается параллельно в нескольких рабочих процессах и в произвольном порядке, состояние цели фаззинга не должно сохраняться за пределами каждого вызова, а поведение цели фаззинга не должно зависеть от глобального состояния.
Запуск фаззинг-тестов
Существует два режима запуска фаззинг-теста: как модульный тест (по умолчанию go test), или с использованием фаззинга (go test -fuzz=FuzzTestName).
Фаззинг-тесты запускаются так же, как и модульные тесты, по умолчанию. Каждая запись начального корпуса будет протестирована против цели фаззинга, при этом будут сообщаться любые ошибки перед завершением.
Чтобы включить фаззинг, выполните go test с флагом -fuzz, указав регулярное выражение, соответствующее одному фаззинг-тесту. По умолчанию перед началом фаззинга будут выполнены все остальные тесты в этом пакете. Это необходимо для того, чтобы фаззинг не сообщал о проблемах, которые уже были бы выявлены существующим тестом.
Обратите внимание, что вы сами решаете, как долго запускать фаззинг. Возможно, выполнение фаззинга может продолжаться бесконечно, если ошибки не будут найдены. В будущем будет добавлена поддержка непрерывного запуска таких фаззинг-тестов с использованием инструментов, таких как OSS-Fuzz, см. Issue #50192.
Примечание: Фаззинг следует запускать на платформе, поддерживающей инструментирование покрытия (в настоящее время AMD64 и ARM64), чтобы корпус мог значимо расти при выполнении, и при фаззинге покрывалось больше кода.
Вывод командной строки
Во время выполнения фаззинга движок фаззинга генерирует новые входные данные и запускает их против заданной цели фаззинга. По умолчанию он продолжает работу до тех пор, пока не будет найден некорректный вход, или пользователь не отменит процесс (например, с помощью Ctrl^C).
Вывод будет выглядеть примерно так:
<code>~ go test -fuzz FuzzFoo fuzz: elapsed: 0s, gathering baseline coverage: 0/192 completed fuzz: elapsed: 0s, gathering baseline coverage: 192/192 completed, now fuzzing with 8 workers fuzz: elapsed: 3s, execs: 325017 (108336/sec), new interesting: 11 (total: 202) fuzz: elapsed: 6s, execs: 680218 (118402/sec), new interesting: 12 (total: 203) fuzz: elapsed: 9s, execs: 1039901 (119895/sec), new interesting: 19 (total: 210) fuzz: elapsed: 12s, execs: 1386684 (115594/sec), new interesting: 21 (total: 212) PASS ok foo 12.692s </code>
Первые строки указывают на то, что собирается «базовое покрытие» перед началом фаззинга.
Для сборки базового покрытия движок фаззинга выполняет как начальный корпус, так и сгенерированный корпус, чтобы убедиться, что ошибок не возникло, и понять, какое покрытие кода уже предоставляет существующий корпус.
Следующие строки предоставляют информацию о текущем процессе фаззинга:
- elapsed: количество времени, прошедшего с момента начала выполнения процесса
- execs: общее количество входных данных, которые были запущены против цели фаззинга (с средним количеством execs/секунду с момента последней записи в лог)
- new interesting: общее количество «интересных» входных данных, которые были добавлены в сгенерированный корпус во время этого процесса фаззинга (с общим размером всего корпуса)
Для того чтобы входные данные считались «интересными», они должны расширять покрытие кода beyond того, что может достичь существующий сгенерированный корпус. Обычно количество новых интересных входных данных растет быстро на начальном этапе и затем замедляется, с периодическими всплесками по мере обнаружения новых ветвлений.
Следует ожидать, что количество «новых интересных» входных данных будет снижаться со временем по мере того, как входные данные в корпусе начнут покрывать больше строк кода, с периодическими всплесками, если движок фаззинга найдет новый путь выполнения кода.
Ошибка входных данных
Ошибка может возникнуть во время фаззинга по нескольким причинам:
- Произошёл панический сбой в коде или тесте.
- Цель фаззинга вызвала
t.Fail, напрямую или через методы, такие какt.Errorилиt.Fatal. - Произошла неустранимая ошибка, например,
os.Exitили переполнение стека. - Цель фаззинга выполнялась слишком долго. В настоящее время таймаут выполнения цели фаззинга составляет 1 секунду. Это может завершиться неудачей из-за блокировки или бесконечного цикла, либо из-за намеренного поведения в коде. Именно поэтому рекомендуется, чтобы ваша цель фаззинга работала быстро.
Если возникает ошибка, движок фаззинга попытается минимизировать входные данные до наименьшего возможного и максимально понятного значения, которое всё ещё вызывает ошибку. Чтобы настроить это поведение, см. раздел пользовательские настройки.
После завершения минимизации сообщение об ошибке будет записано в лог, и вывод завершится примерно следующим образом:
<code> Failing input written to testdata/fuzz/FuzzFoo/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49 To re-run: go test -run=FuzzFoo/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49 FAIL exit status 1 FAIL foo 0.839s </code>
Движок фаззинга записал эти ошибочные входные данные в начальный корпус для этого теста фаззинга, и теперь они будут запускаться по умолчанию с помощью go test, выступая в роли регрессионного теста после устранения ошибки.
Следующим шагом будет диагностика проблемы, исправление ошибки, проверка исправления повторным запуском go test, а также отправка патча с новым файлом testdata, который будет служить регрессионным тестом.
Пользовательские настройки
Настройки команды go по умолчанию должны работать для большинства случаев использования фаззинга. Таким образом, обычно запуск фаззинга в командной строке должен выглядеть так:
<code>$ go test -fuzz={FuzzTestName}
</code>
Однако, команда go предоставляет несколько настроек при запуске фаззинга.
Они документированы в документации пакета cmd/go.
Выделим несколько из них:
-fuzztime: общее время или количество итераций, в течение которых целевой фаззинг будет выполняться перед завершением, по умолчанию — бесконечно.-fuzzminimizetime: время или количество итераций, в течение которых целевой фаззинг будет выполняться во время каждой попытки минимизации, по умолчанию — 60 секунд. Минимизацию можно полностью отключить, установив-fuzzminimizetime 0при фаззинге.-parallel: количество процессов фаззинга, выполняющихся одновременно, по умолчанию$GOMAXPROCS. В настоящее время установка флага -cpu во время фаззинга не имеет эффекта.
Формат файла корпуса
Файлы корпуса кодируются в специальном формате. Это тот же формат как для начального корпуса, так и для сгенерированного корпуса.
Ниже приведён пример файла корпуса:
<code>go test fuzz v1
[]byte("hello\\xbd\\xb2=\\xbc ⌘")
int64(572293)
</code>
Первая строка используется для того, чтобы сообщить движку фаззинга о версии кодирования файла. Хотя будущие версии формата кодирования в настоящее время не планируются, дизайн должен поддерживать эту возможность.
Каждая последующая строка представляет собой значения, составляющие запись корпуса, и может быть скопирована напрямую в код Go при необходимости.
В приведённом выше примере мы имеем []byte, за которым следует int64. Эти типы
должны точно соответствовать аргументам фаззинга, в том порядке, в котором они указаны.
Цель фаззинга для этих типов будет выглядеть так:
<code>f.Fuzz(func(*testing.T, []byte, int64) {})
</code>
Самый простой способ указать собственные начальные значения корпуса — использовать
метод (*testing.F).Add. В примере выше это будет выглядеть так:
<code>f.Add([]byte("hello\\xbd\\xb2=\\xbc ⌘"), int64(572293))
</code>
Однако, возможно, у вас есть большие двоичные файлы, которые вы предпочитаете не копировать как код
в ваш тест, а оставить в виде отдельных записей начального корпуса в директории
testdata/fuzz/{FuzzTestName}. Инструмент
file2fuzz из
golang.org/x/tools/cmd/file2fuzz может быть использован для преобразования этих двоичных файлов
в файлы корпуса, закодированные для []byte.
Чтобы использовать этот инструмент:
<code>$ go install golang.org/x/tools/cmd/file2fuzz@latest $ file2fuzz -h </code>
Ресурсы
- Обучение:
- Попробуйте руководство по фаззингу с помощью Go для глубокого погружения в новые концепции.
- Для более краткого, вводного руководства по фаззингу с помощью Go, пожалуйста, ознакомьтесь с постом в блоге.
- Документация:
- Технические детали:
Глоссарий
corpus entry: Вход в корпус, который может быть использован во время фаззинга. Это может быть специально отформатированный файл или вызов
(*testing.F).Add.
coverage guidance: Метод фаззинга, который использует расширения в покрытии кода для определения, какие записи корпуса стоит сохранить для последующего использования.
failing input: Вход, вызывающий ошибку или панику при запуске против цели фаззинга.
fuzz target: Функция теста фаззинга, которая выполняется для записей корпуса и сгенерированных значений во время фаззинга. Она передаётся в тест фаззинга путём передачи функции в
(*testing.F).Fuzz.
fuzz test: Функция в тестовом файле вида func FuzzXxx(*testing.F), которая может быть использована для фаззинга.
fuzzing: Тип автоматизированного тестирования, при котором непрерывно изменяются входные данные программы с целью обнаружения проблем, таких как ошибки или уязвимости, к которым может быть подвержена кодовая база.
fuzzing arguments: Типы, которые будут переданы цели фаззинга и изменены мутатором.
fuzzing engine: Инструмент, управляющий фаззингом, включая поддержание корпуса, вызов мутатора, выявление нового покрытия и сообщение об ошибках.
generated corpus: Корпус, который поддерживается инструментом фаззинга во времени во время фаззинга, чтобы отслеживать прогресс. Он хранится в $GOCACHE/fuzz.
Эти записи используются только во время фаззинга.
mutator: Инструмент, используемый во время фаззинга, который случайным образом изменяет записи корпуса перед передачей их цели фаззинга.
package: Коллекция исходных файлов в одном каталоге, которые компилируются вместе. См. раздел Packages в спецификации языка Go.
seed corpus: Корпус, предоставленный пользователем для теста фаззинга, который может использоваться для направления работы инструмента фаззинга. Он состоит из записей корпуса, предоставленных вызовами f.Add внутри теста фаззинга, и файлов в каталоге testdata/fuzz/{FuzzTestName} внутри пакета. Эти записи выполняются по умолчанию с помощью go test, независимо от того, запущен ли фаззинг.
test file: Файл формата xxx_test.go, который может содержать тесты, бенчмарки, примеры и тесты фаззинга.
уязвимость: Слабое место в коде, которое может быть использовано злоумышленником.
Обратная связь
Если вы столкнётесь с какими-либо проблемами или захотите предложить новую функцию, пожалуйста, создайте задачу.
Для обсуждения и получения общих отзывов о данной функции, вы также можете участвовать в #канале fuzzing в Gophers Slack.