
Rust-макросы и подкоманда Cargo для автоматизации фаззинга с помощью afl.rs, включая генерацию корпуса и реализацию харнеса, интегрированные с тестовым фреймворком Rust.
test-fuzz — это подкоманда Cargo и набор макросов Rust для автоматизации задач, связанных с фаззингом с помощью afl.rs, включая:
test-fuzz выполняет эти задачи (частично) с помощью средств тестирования Rust. Например, для генерации фаззинг-корпуса test-fuzz записывает аргументы целевой функции при каждом её вызове во время выполнения cargo test. Аналогично, test-fuzz реализует фаззинг-харнес как дополнительный тест в бинарном файле, созданном cargo-test. Именно такая тесная интеграция со средствами тестирования Rust объясняет название test-fuzz.
Содержание
test_fuzz]test_fuzz_impl]cargo test-fuzz]test-fuzz]Установите cargo-test-fuzz и afl.rs следующей командой:```sh
cargo install cargo-test-fuzz cargo-afl
## Обзор
Фаззинг с помощью `test-fuzz` — это, по сути, три шага:\*
1. **Определите цель фаззинга**:
- Добавьте следующие `dependencies` в файл `Cargo.toml` целевого крейта:
```toml
serde = "*"
test-fuzz = "*"
```
- Поместите перед целевой функцией макрос [`test_fuzz`]:
```rust
#[test_fuzz::test_fuzz]
fn foo(...) {
...
}
```
2. **Создайте корпус**, запустив `cargo test`: ```
cargo test
cargo test-fuzz: ```
cargo test-fuzz foo
* Дополнительный предварительный шаг может потребоваться после перезагрузки:```sh cargo afl system-config
Обратите внимание, что приведённая выше команда запускает `sudo` внутри себя. Поэтому у вас может быть запрошен пароль.
## Компоненты
### Макрос `test_fuzz`
Размещение макроса `test_fuzz` перед функцией означает, что функция является фаззинг-целью.
Основные эффекты макроса `test_fuzz`:
- Добавляет инструментирование в цель, чтобы сериализовать её аргументы и записывать их в файл корпуса при каждом вызове цели. Инструментирование защищено `#[cfg(test)]`, поэтому файлы корпуса создаются только при запуске тестов (однако см. [`enable_in_production`] ниже).
- Добавляет тест для чтения и десериализации аргументов из стандартного ввода и применения к ним цели. Тест проверяет переменную окружения, устанавливаемую [`cargo test-fuzz`], чтобы при обычном запуске `cargo test` тест не блокировался попыткой чтения из стандартного ввода. Тест помещается в модуль, чтобы снизить вероятность конфликта имён. В настоящее время модуль называется `target_fuzz`, где `target` — имя цели (однако см. [`rename`] ниже).
#### Аргументы
##### `bounds = "where_predicates"`
Накладывает `where_predicates` (например, ограничения трейтов) на структуру, используемую для сериализации/десериализации аргументов. Это может быть необходимо, например, если тип аргумента цели является ассоциированным типом. Пример см. в [associated_type.rs] в этом репозитории.
##### `generic_args = "parameters"`
Используйте `parameters` в качестве параметров типа цели при фаззинге. Пример:```rust
#[test_fuzz(generic_args = "String")]
fn foo<T: Clone + Debug + Serialize>(x: &T) {
...
}
Примечание: аргументы цели должны быть сериализуемыми для каждой инстанциации её параметров типов. Но аргументы цели должны быть десериализуемыми только тогда, когда цель инстанцируется с parameters.
impl_generic_args = "parameters"Используйте parameters в качестве параметров типа Self цели при фаззинге. Пример:```rust
#[test_fuzz_impl]
impl<T: Clone + Debug + Serialize> for Foo {
#[test_fuzz(impl_generic_args = "String")]
fn bar(&self, x: &T) {
...
}
}
Примечание: аргументы цели должны быть сериализуемыми для **каждой** инстанциации её параметров типа `Self`. Но аргументы цели должны быть десериализуемыми только когда `Self` цели инстанцирован с `parameters`.
##### `convert = "X, Y"`
При сериализации аргументов цели преобразуйте значения типа `X` в тип `Y`, используя реализацию `From<X>` в `Y`, или значения типа `&X` в тип `Y` с использованием реализации нестандартного трейта `test_fuzz::FromRef<X>` в `Y`. При десериализации преобразуйте эти значения обратно в тип `X`, используя реализацию нестандартного трейта `test_fuzz::Into<X>` в `Y`.
То есть использование `convert = "X, Y"` должно сопровождаться определёнными реализациями. Если `X` реализует [`Clone`], то `Y` может реализовать следующее:```rust
impl From<X> for Y {
fn from(x: X) -> Self {
...
}
}
Если X не реализует Clone, то Y должен реализовать следующее:```rust
impl test_fuzz::FromRef for Y {
fn from_ref(x: &X) -> Self {
...
}
}
Кроме того, `Y` должен реализовывать следующее (независимо от того, реализует ли `X` [`Clone`]):```rust
impl test_fuzz::Into<X> for Y {
fn into(self) -> X {
...
}
}
Определение test_fuzz::Into идентично определению std::convert::Into. Причина использования нестандартного трейта — избежать конфликтов, которые могут возникнуть из-за бланкетных реализаций стандартных трейтов.
enable_in_productionГенерируйте файлы корпуса при запуске вне тестов, если установлена переменная окружения TEST_FUZZ_WRITE. По умолчанию файлы корпуса генерируются только при запуске тестов, независимо от того, установлена ли TEST_FUZZ_WRITE. При запуске целевой функции за пределами каталога её пакета установите TEST_FUZZ_MANIFEST_PATH в путь к файлу Cargo.toml пакета.
ПРЕДУПРЕЖДЕНИЕ: Установка enable_in_production может привести к появлению вектора отказа в обслуживании. Например, установка этой опции для функции, вызываемой много раз с разными аргументами, может заполнить диск. Проверка TEST_FUZZ_WRITE предназначена для обеспечения некоторой защиты от такой возможности. Тем не менее, прежде чем использовать эту опцию, внимательно её обдумайте.
execute_with = "function"Вместо прямого вызова целевой функции:
FnOnce() -> R, где R — возвращаемый тип целевой функции, так чтобы вызов замыкания вызывал целевую функцию;function с этим замыканием.Вызов целевой функции таким образом позволяет function настроить окружение вызова. Это может быть полезно, например, для фаззинга [экстерналий Substrate].
no_auto_generateНе пытайтесь [автоматически генерировать файлы корпуса] для целевой функции.
only_generic_argsЗаписывайте обобщённые аргументы целевой функции при запуске тестов, но не генерируйте файлы корпуса и не реализуйте фаззинг-харнес. Это может быть полезно, когда целевая функция является обобщённой, но неясно, какие параметры типа следует использовать для фаззинга.
Предполагаемый рабочий процесс: включите only_generic_args, затем выполните cargo test, а затем cargo test-fuzz --display generic-args. Один из полученных обобщённых аргументов может подойти в качестве parameters для generic_args. Аналогично, обобщённые аргументы, полученные в результате cargo test-fuzz --display impl-generic-args, могут подойти в качестве parameters для impl_generic_args.
Обратите внимание, однако, что сам по себе факт вызова целевой функции с определёнными параметрами во время тестов не означает, что аргументы целевой функции сериализуемы/десериализуемы при использовании этих параметров. Результаты --display generic-args/--display impl-generic-args носят лишь ориентировочный характер.
rename = "name"Считайте, что целевая функция называется name, при добавлении модуля в охватывающую область видимости. Раскрытие макроса test_fuzz добавляет определение модуля в охватывающую область видимости. По умолчанию модуль называется следующим образом:
impl, модуль называется target_fuzz__, где target — имя целевой функции.impl, модуль называется path_target_fuzz__, где path — последний сегмент пути типа Self из блока impl.Однако использование этой опции приводит к тому, что модуль вместо этого называется name_fuzz__. Пример:```rust
#[test_fuzz(rename = "bar")]
fn foo() {}
// Without the use of rename, a name collision and compile error would result.
mod foo_fuzz__ {}
#### Атрибуты полей Serde для аргументов функций
Макрос `test_fuzz` позволяет применять [атрибуты полей Serde] к аргументам функций. Это даёт ещё один инструмент для работы со сложными типами.
Ниже приведён пример. Трейты `serde::Serialize` и `serde::Deserialize` не могут быть выведены для `Context`, поскольку он содержит `Mutex`. Однако `Context` реализует `Default`. Поэтому применение `#[serde(skip)]` к аргументу `Context` приводит к тому, что он пропускается при сериализации и принимает своё значение по умолчанию при десериализации.```rust
use std::sync::Mutex;
// Traits `serde::Serialize` and `serde::Deserialize` cannot be derived for `Context` because it
// contains a `Mutex`.
#[derive(Default)]
struct Context {
lock: Mutex<()>,
}
impl Clone for Context {
fn clone(&self) -> Self {
Self {
lock: Mutex::new(()),
}
}
}
#[test_fuzz::test_fuzz]
fn target(#[serde(skip)] context: Context, x: i32) {
assert!(x >= 0);
}
Note that when Serde field attributes are applied to an argument, the test_fuzz macro performs no other conversions on the argument.
test_fuzz_implКаждый раз, когда макрос test_fuzz используется в блоке impl, перед impl должен находиться макрос test_fuzz_impl. Пример:```rust
#[test_fuzz_impl]
impl Foo {
#[test_fuzz]
fn bar(&self, x: &str) {
...
}
}
Причина этого требования заключается в следующем. Разворачивание макроса [`test_fuzz`] добавляет определение модуля в окружающую область видимости. Однако определение модуля не может находиться внутри блока `impl`. Предваряя `impl` макросом `test_fuzz_impl`, модуль добавляется вне блока `impl`.
Если вы видите ошибку, подобную следующей, это, скорее всего, означает, что отсутствует использование макроса `test_fuzz_impl`:```
error: module is not supported in `trait`s or `impl`s
test_fuzz_impl в настоящее время не имеет опций.
cargo test-fuzzКоманда cargo test-fuzz используется для взаимодействия с fuzz-целями, а также для управления их корпусами, сбоями (crashes), зависаниями (hangs) и рабочими очередями. Примеры вызова:
foo ```
cargo test-fuzz foo --display corpus
foo ```
cargo test-fuzz foo
foo ```
cargo test-fuzz foo --replay crashes
Usage: cargo test-fuzz [OPTIONS] [TARGETNAME] [-- ...]
Arguments: [TARGETNAME] String that fuzz target's name must contain [ARGS]... Arguments for the fuzzer
Options:
--backtrace Display backtraces
--consolidate Move one target's crashes, hangs, and work queue to its corpus; to
consolidate all targets, use --consolidate-all
--coverage Generate coverage for corpus, crashes, hangs, or work queue. Note
that generating coverage for instrumented fuzz targets is not
supported.
--cpus Fuzz using at most cpus; default is all but one
--display Display corpus, crashes, generic args, impl generic args, hangs,
or work queue. By default, an uninstrumented fuzz target is used.
To display with instrumentation, append -instrumented to
, e.g., --display corpus-instrumented.
--exact Target name is an exact name rather than a substring
--exit-code Exit with 0 if the time limit was reached, 1 for other
programmatic aborts, and 2 if an error occurred; implies --no-ui,
does not imply --run-until-crash or --max-total-time
--features Space or comma separated list of features to activate
--list List fuzz targets
--manifest-path Path to Cargo.toml
--max-total-time Fuzz at most of time (equivalent to -- -V )
--no-default-features Do not activate the default feature
--no-run Compile, but don't fuzz
--no-ui Disable user interface
-p, --package Package containing fuzz target
--persistent Enable persistent mode fuzzing
--pretty Pretty-print debug output when generating coverage, displaying, or
replaying
--release Build in release mode
--replay Replay corpus, crashes, hangs, or work queue. By default, an
uninstrumented fuzz target is used. To replay with
instrumentation, append -instrumented to , e.g.,
--replay corpus-instrumented.
--reset Clear fuzzing data for one target, but leave corpus intact; to
reset all targets, use --reset-all
--resume Resume target's last fuzzing session
--run-until-crash Stop fuzzing once a crash is found
--slice If there are not sufficiently many cpus to fuzz all targets
simultaneously, fuzz them in intervals of [default:
1200]
--test Integration test containing fuzz target
--timeout Number of seconds to consider a hang when fuzzing or replaying
(equivalent to -- -t <TIMEOUT * 1000> when fuzzing)
--verbose Show build output when generating coverage, displaying, or
replaying
-h, --help Print help
-V, --version Print version
Try cargo afl fuzz --help to see additional fuzzer options.
При использовании опции `--display` отображается любой вывод, записанный целью в stderr. Это включает вывод из операторов `eprintln!`, а также из макросов отладки, таких как `dbg!`. Это может быть полезно для понимания того, что происходит в вашем коде при обработке конкретных входных данных.
Опции `--display` и `--replay` можно передавать вместе, что позволяет одновременно просматривать и воспроизводить записи корпуса одной командой, например:```
cargo test-fuzz foo --display corpus --replay corpus
Предупреждение: Эти утилиты исключены из семантического версионирования и могут быть удалены в будущих версиях test-fuzz.
dont_care!Макрос dont_care! можно использовать для реализации serde::Serialize/serde::Deserialize для типов, которые легко сконструировать и значения которых вам не нужно сохранять. Интуитивно, dont_care!($ty, $expr) означает:
$ty при сериализации.$ty с помощью $expr при десериализации.Если точнее, dont_care!($ty, $expr) разворачивается в следующее:```rust
impl serde::Serialize for $ty {
fn serialize(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
().serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for $ty { fn deserialize(deserializer: D) -> std::result::Result<Self, D::Error> where D: serde::Deserializer<'de>, { <()>::deserialize(deserializer).map(|_| $expr) } }
Если `$ty` является unit-структурой, то `$expr` можно опустить. То есть `dont_care!($ty)` эквивалентно `dont_care!($ty, $ty)`.
#### `leak!`
Макрос `leak!` может помочь сериализовать аргументы целевой функции, которые являются ссылками и типы которых реализуют трейт [`ToOwned`]. Его следует использовать вместе с опцией [`convert`].
В частности, вызов следующей формы объявляет тип `LeakedX` и реализует для него трейты `From` и `test_fuzz::Into`:```rust
leak!(X, LeakedX);
Затем можно использовать LeakedX с опцией convert следующим образом:```rust
#[test_fuzz::test_fuzz(convert = "&X, LeakedX")
Пример, где `X` является [`Path`], приведён в [conversion.rs] в этом репозитории.
В более общем случае, вызов вида `leak!($ty, $ident)` разворачивается в следующее:```rust
#[derive(Clone, std::fmt::Debug, serde::Deserialize, serde::Serialize)]
struct $ident(<$ty as ToOwned>::Owned);
impl From<&$ty> for $ident {
fn from(ty: &$ty) -> Self {
Self(ty.to_owned())
}
}
impl test_fuzz::Into<&$ty> for $ident {
fn into(self) -> &'static $ty {
Box::leak(Box::new(self.0))
}
}
serialize_refdeserialize_refserialize_ref и deserialize_ref работают аналогично leak!, но предназначены для использования с атрибутами полей Serde serialize_with и deserialize_with (соответственно).```rust
fn serialize_ref<S, T>(x: &&T, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
T: serde::Serialize,
{
::serialize(*x, serializer)
}
fn deserialize_ref<'de, D, T>(deserializer: D) -> Result<&'static T, D::Error> where D: serde::Deserializer<'de>, T: serde:🇩🇪:DeserializeOwned + std::fmt::Debug, { let x = ::deserialize(deserializer)?; Ok(Box::leak(Box::new(x))) }
#### `serialize_ref_mut` / `deserialize_ref_mut`
`serialize_ref_mut` и `deserialize_ref_mut` аналогичны `serialize_ref` и `deserialize_ref` (соответственно), за исключением того, что они работают с изменяемыми ссылками, а не с неизменяемыми.
## Возможности пакета `test-fuzz`
Возможности, описанные в этом разделе, применяются к пакету `test-fuzz` в целом. Включите их в спецификации зависимостей `test-fuzz`, как описано в [The Cargo Book]. Например, чтобы включить возможность `cast_checks`, используйте:```toml
test-fuzz = { version = "*", features = ["cast_checks"] }
Пакет test-fuzz в настоящее время поддерживает следующие возможности:
cast_checksИспользуйте cast_checks для автоматической проверки целевых функций на наличие недопустимых приведений типов.
Обратите внимание, что эта возможность включает cast_checks только для функций, аннотированных test_fuzz macro, а не для функций, которые они вызывают.
test-fuzz может сериализовать аргументы целей в нескольких форматах Serde. Ниже перечислены возможности, используемые для выбора формата.
cargo-test-fuzz может автоматически генерировать значения для типов, реализующих определённые трейты. Если все типы аргументов цели реализуют такие трейты, cargo-test-fuzz может автоматически генерировать файлы корпуса для цели.
Трейты, которые cargo-test-fuzz в настоящее время поддерживает, и значения, генерируемые для них, приведены ниже:
| Trait(s) | Value(s) |
|---|---|
Bounded | T::min_value(), T::max_value() |
Bounded + Add + One | T::min_value() + T::one() |
Bounded + Add + Div + Two | T::min_value() / T::two() + T::max_value() / T::two() |
Bounded + Add + Div + Two + One | T::min_value() / T::two() + T::max_value() / T::two() + T::one() |
Bounded + Sub + One | T::max_value() - T::one() |
Default | T::default() |
Обозначения
Add - core::ops::AddBounded - num_traits::bounds::BoundedDefault - std::default::DefaultDiv - core::ops::DivOne - num_traits::OneSub - core::ops::SubTwo - test_fuzz::runtime::traits::Two (по сути Add + One)TEST_FUZZ_LOGВо время раскрытия макроса:
TEST_FUZZ_LOG установлен в 1, выводите все инструментированные фаззинг-цели и определения модулей в стандартный вывод.TEST_FUZZ_LOG установлен в имя крейта, выводите инструментированные фаззинг-цели и определения модулей этого крейта в стандартный вывод.Это может быть полезно для отладки.
TEST_FUZZ_MANIFEST_PATHПри запуске цели извне каталога её пакета файл Cargo.toml пакета ищется по этому пути. Возможно, эту переменную окружения потребуется установить при использовании enable_in_production.
TEST_FUZZ_WRITEГенерирует файлы корпуса, когда тесты не запускаются, для тех целей, для которых установлен enable_in_production.
Аргументы цели должны реализовывать трейт Clone. Причина такого требования в том, что аргументы нужны в двух местах: во внутренней функции test-fuzz, которая записывает файлы корпуса, и в теле целевой функции. Чтобы разрешить этот конфликт, аргументы клонируются перед передачей во внутреннюю функцию, записывающую файлы корпуса.
В общем случае аргументы цели должны реализовывать трейты serde::Serialize и serde::Deserialize, например, с помощью deriving them. Мы говорим «в общем случае», потому что test-fuzz умеет обрабатывать определённые особые случаи, которые в обычных условиях не были бы сериализуемыми/десериализуемыми. Например, аргумент типа &str при сериализации преобразуется в String, а при десериализации — обратно в &str. См. также generic_args и impl_generic_args выше.
Фаззинг-харнессы, которые реализует test-fuzz, не инициализируют глобальные переменные. Хотя execute_with отчасти помогает, это не является полным решением. В общем случае фаззинг функции, которая полагается на глобальные переменные, требует специальных (ad-hoc) методов.
Эти опции несовместимы в следующем смысле. Если тип аргумента фаззинг-цели является параметром типа, convert будет пытаться сопоставить параметр типа, а не тип, которым этот параметр задан. Поддержка второго, по-видимому, потребовала бы имитации подстановки типов, которую выполнял бы компилятор. Однако в настоящее время это не реализовано.
#[cfg(test)] is not enabled для интеграционных тестов. Если ваша цель тестируется только интеграционными тестами, рассмотрите возможность использования enable_in_production и TEST_FUZZ_WRITE для генерации корпуса. (Однако обратите внимание на предупреждение, сопровождающее enable_in_production.)
Если вы знаете пакет, в котором находится ваша цель, передача -p <package> в cargo test/cargo test-fuzz может значительно сократить время сборки. Аналогично, если вы знаете, что ваша цель вызывается только из одного интеграционного теста, передача --test <name> может сократить время сборки.
Rust won't allow you to реализовать serde::Serialize для типов из других репозиториев. Но вы можете patch другие репозитории, чтобы сделать их типы сериализуемыми. Кроме того, cargo-clone может быть полезен для получения репозиториев зависимостей.
Serde attributes могут быть полезны при реализации serde::Serialize/serde::Deserialize для сложных типов.
Мы оставляем за собой право изменять формат корпусов, сбоев, зависаний и рабочих очередей и считать такие изменения не ломающими совместимость.
test-fuzz лицензирован и распространяется под лицензией AGPLv3 с исключением Macros and Inline Functions Exception. Простыми словами, использование test_fuzz macro, test_fuzz_impl macro или convenience functions and macros библиотеки test-fuzz в вашем программном обеспечении не требует, чтобы оно распространялось под лицензией AGPLv3.