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