test-fuzz 是一个 Cargo 子命令,也是一组 Rust 宏,用于自动化与使用 afl.rs 进行模糊测试相关的某些任务,包括:
test-fuzz(部分地)借助 Rust 的测试机制完成这些任务。例如,为了生成模糊测试语料库,test-fuzz 会在 cargo test 调用期间每次调用目标函数时记录其参数。类似地,test-fuzz 将模糊测试 harness 实现为 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
## Overview
使用 `test-fuzz` 进行模糊测试本质上分为三个步骤:\*
1. **识别模糊测试目标**:
- 在目标 crate 的 `Cargo.toml` 文件中添加以下 `dependencies`:
```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`(例如 trait 约束)。这可能是必要的,例如,当目标参数类型是关联类型时。有关示例,请参见此仓库中的 [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"`
在序列化目标的参数时,使用 `Y` 对 `From<X>` 的实现将类型 `X` 的值转换为类型 `Y`,或者使用 `Y` 对非标准 trait `test_fuzz::FromRef<X>` 的实现将类型 `&X` 的值转换为类型 `Y`。在反序列化时,使用 `Y` 对非标准 trait `test_fuzz::Into<X>` 的实现将这些值转换回类型 `X`。
也就是说,使用 `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 相同。使用非标准 trait 的原因是为了避免标准 trait 的覆盖实现可能引发的冲突。
enable_in_production当不在运行测试时,如果设置了环境变量 TEST_FUZZ_WRITE,则生成 corpus 文件。默认行为是仅在运行测试时生成 corpus 文件,无论是否设置了 TEST_FUZZ_WRITE。当从包目录之外运行目标时,请将 TEST_FUZZ_MANIFEST_PATH 设置为该包的 Cargo.toml 文件的路径。
警告:设置 enable_in_production 可能会引入拒绝服务攻击向量。例如,为某个使用不同参数被多次调用的函数设置此选项,可能会占满磁盘空间。对 TEST_FUZZ_WRITE 的检查旨在对此提供一定防御。尽管如此,请在使用前仔细考虑此选项。
execute_with = "function"不直接调用目标,而是:
FnOnce() -> R 的闭包,其中 R 是目标的返回类型,这样调用该闭包就等于调用目标;function。以这种方式调用目标,可以让 function 为调用设置环境。例如,这对于对 Substrate externalities 进行模糊测试很有用。
no_auto_generate不要尝试为目标 auto-generate corpus files。
only_generic_args在运行测试时记录目标的泛型实参,但不生成 corpus 文件,也不实现模糊测试 harness。当目标是泛型函数但还不清楚应使用哪些类型参数进行模糊测试时,这很有用。
预期的工作流程是:启用 only_generic_args,然后依次运行 cargo test 和 cargo test-fuzz --display generic-args。得到的某个泛型实参可能可以作为 generic_args 的 parameters 使用。类似地,从 cargo test-fuzz --display impl-generic-args 得到的泛型实参可能可以用作 impl_generic_args 的 parameters。
但请注意,仅仅因为目标在测试期间使用某些参数被调用,并不意味着当使用这些参数时目标的参数是可序列化/可反序列化的。--display generic-args/--display impl-generic-args 的结果仅供参考。
rename = "name"在向封闭作用域添加模块时,将目标视为其名称为 name。test_fuzz 宏的展开会在封闭作用域中添加一个模块定义。默认情况下,该模块的命名如下:
impl 块中,则模块命名为 target_fuzz__,其中 target 是目标的名称。impl 块中,则模块命名为 path_target_fuzz__,其中 path 是 impl 的 Self 类型路径的最后一段。然而,使用此选项会使模块改名为 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 字段属性] 应用于函数参数。这为处理困难类型提供了另一种工具。
下面是一个示例。由于 `Context` 包含一个 `Mutex`,因此无法为 `Context` 派生 `serde::Serialize` 和 `serde::Deserialize` trait。但 `Context` 实现了 `Default`。因此,对 `Context` 参数应用 `#[serde(skip)]` 会导致它在序列化时被跳过,并在反序列化时采用其默认值。```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 [转换] on the argument.
test_fuzz_impl 宏无论何时在 impl 块中使用 test_fuzz 宏,
都必须用 test_fuzz_impl 宏置于该 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 命令用于与模糊测试目标进行交互,以及操作其语料库、崩溃、挂起和工作队列。示例调用包括:
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 的值。$expr 初始化类型为 $ty 的值。更具体地说,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` 是单元结构体,则可以省略 `$expr`。也就是说,`dont_care!($ty)` 等价于 `dont_care!($ty, $ty)`。
#### `leak!`
`leak!` 宏可以帮助序列化那些是引用且其类型实现了 [`ToOwned`] trait 的目标参数。它旨在与 [`convert`] 选项一起使用。
具体来说,以下形式的调用会声明一个类型 `LeakedX`,并为其实现 `From` 和 `test_fuzz::Into` trait:```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_ref / deserialize_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` 包。按照 [The Cargo Book] 中的描述,在 `test-fuzz` 的依赖规格中启用它们。例如,要启用 `cast_checks` 特性,请使用:```toml
test-fuzz = { version = "*", features = ["cast_checks"] }
The test-fuzz package currently supports the following features:
cast_checksUse cast_checks to automatically check target functions for invalid casts.
Note that this feature enables cast_checks only for functions annotated with the test_fuzz macro, not for the functions they call.
test-fuzz 可以使用多种 Serde 格式序列化目标参数。以下特性用于选择格式。
cargo-test-fuzz 可以为实现了某些 trait 的类型自动生成值。如果目标的所有参数类型都实现了这些 trait,cargo-test-fuzz 就可以为该目标自动生成语料库文件。
cargo-test-fuzz 当前支持的 trait 以及为它们生成的值如下:
图例
Add - core::ops::AddBounded - num_traits::bounds::BoundedDefault - std::default::DefaultDiv - core::ops::DivOne - num_traits::OneSub - core::ops::SubTEST_FUZZ_LOG在宏展开期间:
TEST_FUZZ_LOG 被设置为 1,则将所有已插桩的模糊测试目标和模块定义写入标准输出。TEST_FUZZ_LOG 被设置为某个 crate 名称,则将该 crate 中已插桩的模糊测试目标和模块定义写入标准输出。这在调试时可能很有用。
TEST_FUZZ_MANIFEST_PATH当从包目录之外运行目标时,在此位置查找该包的 Cargo.toml 文件。使用 enable_in_production 时,可能需要设置此环境变量。
TEST_FUZZ_WRITE对于设置了 enable_in_production 的目标,在不运行测试时为其生成语料库文件。
目标的参数必须实现 Clone trait。这一要求的原因是,参数在两个地方都需要使用:在 test-fuzz 内部一个写入语料库文件的函数中,以及在目标函数的函数体中。为了解决这一冲突,参数会在传递给前者之前被克隆。
通常情况下,目标的参数必须实现 serde::Serialize 和 serde::Deserialize trait,例如通过派生它们。我们说“通常情况下”,是因为 test-fuzz 知道如何处理某些通常无法序列化/反序列化的特殊情况。例如,类型为 &str 的参数在序列化时会转换为 String,在反序列化时再转换回 &str。另请参阅上文中的 generic_args 和 impl_generic_args。
test-fuzz 实现的模糊测试框架(harness)不会初始化全局变量。虽然 execute_with 提供了一些补救措施,但它并不是完整的解决方案。一般来说,对依赖全局变量的函数进行模糊测试需要采用临时(ad-hoc)方法。
convert 与 generic_args / impl_generic_args这些选项在以下意义上互不兼容:如果模糊测试目标的参数类型是类型参数,convert 会尝试匹配类型参数本身,而不是该参数所设置的具体类型。要支持后者,似乎需要模拟编译器所执行的那种类型替换。然而,目前尚未实现这一点。
#[cfg(test)] 在集成测试中不会被启用。如果您的目标仅由集成测试测试,请考虑使用 enable_in_production 和 TEST_FUZZ_WRITE 生成语料库。(不过,请注意 enable_in_production 附带的警告。)
如果您知道目标所在的包,将 -p <package> 传给 cargo test/cargo test-fuzz 可以显著减少构建时间。同样,如果您知道目标只由一个集成测试调用,传入 --test <name> 也可以减少构建时间。
Rust 不允许您为其他仓库中的类型实现 serde::Serialize。但您也许可以对其他仓库应用补丁,使其类型可序列化。此外, 对于获取依赖项的仓库也很有用。
我们保留更改语料库、崩溃、挂起和工作队列格式的权利,并有权将这些更改视为非破坏性更改。
test-fuzz 依据 AGPLv3 许可证获得授权并进行分发,且附带宏与内联函数例外。简而言之,在您的软件中使用 test_fuzz 宏、test_fuzz_impl 宏 或 test-fuzz 的便捷函数和宏,并不要求其受 AGPLv3 许可证约束。
| Trait(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() |
Two - test_fuzz::runtime::traits::Two(本质上是 Add + One)Serde 属性 有助于为复杂的类型实现 serde::Serialize/serde::Deserialize。