trailofbits/test-fuzz

GitHub: trailofbits/test-fuzz

一款通过复用 Rust 测试设施自动生成语料库和测试具的 AFL 模糊测试工具。

Stars: 198 | Forks: 25

# test-fuzz `test-fuzz` 是一个 Cargo 子命令和 Rust 宏的集合,用于自动化某些与 [`afl.rs`] 模糊测试相关的任务,包括: - 生成模糊测试语料库 - 实现模糊测试测试具 (harness) `test-fuzz` 部分利用 Rust 的测试设施来完成这些任务。例如,为了生成模糊测试语料库,`test-fuzz` 会在 `cargo test` 调用期间记录目标函数每次被调用时的参数。同样,`test-fuzz` 将模糊测试测试具实现为 `cargo-test` 生成的二进制文件中的一个额外测试。这种与 Rust 测试设施的紧密集成正是 **`test`**`-fuzz` 名称的由来。 **目录** 1. [安装] 2. [概述] 3. [组件] - [`test_fuzz` 宏] - [`test_fuzz_impl` 宏] - [`cargo test-fuzz` 命令] - [便捷函数和宏] 4. [`test-fuzz` 包特性] 5. [自动生成的语料库文件] 6. [环境变量] 7. [限制] 8. [技巧与窍门] 9. [语义版本控制策略] 10. [许可证] ## 安装 使用以下命令安装 `cargo-test-fuzz` 和 [`afl.rs`]: ``` cargo install cargo-test-fuzz cargo-afl ``` ## 概述 使用 `test-fuzz` 进行模糊测试本质上是三个步骤:\* 1. **确定模糊测试目标**: - 将以下 `dependencies` 添加到目标 crate 的 `Cargo.toml` 文件中: serde = "*" test-fuzz = "*" - 在目标函数前添加 [`test_fuzz`] 宏: #[test_fuzz::test_fuzz] fn foo(...) { ... } 2. **生成语料库**,通过运行 `cargo test`: cargo test 3. **对目标进行模糊测试**,通过运行 [`cargo test-fuzz`]: cargo test-fuzz foo \* 重启后可能需要一个额外的预备步骤: ``` 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` 用作目标的类型参数。示例: ``` #[test_fuzz(generic_args = "String")] fn foo(x: &T) { ... } ``` 注意:对于类型参数的**每一个**实例化,目标的参数都必须是可序列化的。但仅当目标使用 `parameters` 实例化时,才要求目标的参数是可反序列化的。 ##### `impl_generic_args = "parameters"` 在模糊测试时将 `parameters` 用作目标的 `Self` 类型参数。示例: ``` #[test_fuzz_impl] impl for Foo { #[test_fuzz(impl_generic_args = "String")] fn bar(&self, x: &T) { ... } } ``` 注意:对于 `Self` 类型参数的**每一个**实例化,目标的参数都必须是可序列化的。但仅当目标的 `Self` 使用 `parameters` 实例化时,才要求目标的参数是可反序列化的。 ##### `convert = "X, Y"` 序列化目标参数时,使用 `Y` 对 `From` 的实现将类型 `X` 的值转换为类型 `Y`,或使用 `Y` 对非标准 trait `test_fuzz::FromRef` 的实现将类型 `&X` 的值转换为类型 `Y`。反序列化时,使用 `Y` 对非标准 trait `test_fuzz::Into` 的实现将这些值转换回类型 `X`。 也就是说,使用 `convert = "X, Y"` 必须伴随某些实现。如果 `X` 实现了 [`Clone`],那么 `Y` 可以实现以下内容: ``` impl From for Y { fn from(x: X) -> Self { ... } } ``` 如果 `X` 没有实现 [`Clone`],那么 `Y` 必须实现以下内容: ``` impl test_fuzz::FromRef for Y { fn from_ref(x: &X) -> Self { ... } } ``` 此外,`Y` 必须实现以下内容(无论 `X` 是否实现 [`Clone`]): ``` impl test_fuzz::Into for Y { fn into(self) -> X { ... } } ``` `test_fuzz::Into` 的定义与 [`std::convert::Into`] 相同。使用非标准 trait 的原因是为了避免因标准 trait 的覆盖实现而可能产生的冲突。 ##### `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 externalities]。 ##### `no_auto_generate` 不要尝试为目标[自动生成语料库文件]。 ##### `only_generic_args` 在运行测试时记录目标的泛型参数,但不生成语料库文件且不实现模糊测试测试具。当目标是泛型函数但不清楚应使用哪些类型参数进行模糊测试时,这会很有用。 预期的工作流程是:启用 `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__`。示例: ``` #[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` 无法派生 `serde::Serialize` 和 `serde::Deserialize` trait,因为它包含一个 `Mutex`。但是,`Context` 实现了 `Default`。因此,将 `#[serde(skip)]` 应用于 `Context` 参数会导致其在序列化时被跳过,并在反序列化时采用其默认值。 ``` 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); } ``` 请注意,当 Serde 字段属性应用于参数时,`test_fuzz` 宏不会对该参数执行其他[转换]。 ### `test_fuzz_impl` 宏 每当在 `impl` 块中使用 [`test_fuzz`] 宏时, 该 `impl` 前必须加上 `test_fuzz_impl` 宏。示例: ``` #[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` 命令用于与模糊测试目标交互,并操作其语料库、崩溃、挂起和工作队列。示例调用包括: 1. 列出模糊测试目标 cargo test-fuzz --list 2. 显示目标 `foo` 的语料库 cargo test-fuzz foo --display corpus 3. 对目标 `foo` 进行模糊测试 cargo test-fuzz foo 4. 重放为目标 `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 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)` 展开为以下内容: ``` impl serde::Serialize for $ty { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { ().serialize(serializer) } } impl<'de> serde::Deserialize<'de> for $ty { fn deserialize(deserializer: D) -> std::result::Result 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: ``` leak!(X, LeakedX); ``` 然后可以按如下方式将 `LeakedX` 与 `convert` 选项一起使用: ``` #[test_fuzz::test_fuzz(convert = "&X, LeakedX") ``` 本仓库中的 [conversion.rs] 中有一个 `X` 为 [`Path`] 的示例。 更一般地,形式为 `leak!($ty, $ident)` 的调用展开为以下内容: ``` #[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_ref` `serialize_ref` 和 `deserialize_ref` 功能类似于 `leak!`,但它们旨在与 Serde 的 [`serialize_with`] 和 [`deserialize_with`] 字段属性(分别)一起使用。 ``` fn serialize_ref(x: &&T, serializer: S) -> Result 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::de::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` 特性,请使用: ``` test-fuzz = { version = "*", features = ["cast_checks"] } ``` `test-fuzz` 包目前支持以下特性: ### `cast_checks` 使用 [`cast_checks`] 自动检查目标函数是否存在无效转换。 请注意,此特性仅为带有 [`test_fuzz` 宏] 注解的函数启用 `cast_checks`,而不是为它们调用的函数启用。 ### Serde 格式 `test-fuzz` 可以以多种 Serde 格式序列化目标参数。以下是用于选择格式的特性。 - `serde_postcard` - [Postcard](默认) - `serde_bincode` - [Bincode] ## 自动生成的语料库文件 `cargo-test-fuzz` 可以为实现了某些 trait 的类型自动生成。如果目标的所有参数类型都实现了此类 trait,`cargo-test-fuzz` 可以为目标自动生成语料库文件。 `cargo-test-fuzz` 当前支持的 trait 及其为它们生成的值如下: | 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::Add`] - `Bounded` - [`num_traits::bounds::Bounded`] - `Default` - [`std::default::Default`] - `Div` - [`core::ops::Div`] - `One` - [`num_traits::One`] - `Sub` - [`core::ops::Sub`] - `Two` - `test_fuzz::runtime::traits::Two`(本质上是 `Add + One`) ## 环境变量 ### `TEST_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` 实现的模糊测试测试具不会初始化全局变量。虽然 [`execute_with`] 提供了一些补救措施,但它不是一个完整的解决方案。通常,模糊测试依赖于全局变量的函数需要专门的方法。 ### [`convert`] 和 [`generic_args`] / [`impl_generic_args`] 这些选项在以下意义上是不兼容的。如果模糊测试目标的参数类型是类型参数,[`convert`] 将尝试匹配类型参数,而不是参数所设置的类型。支持后者似乎需要模拟编译器执行的类型替换。但是,目前尚未实现。 ## 技巧与窍门 - 集成测试[未启用] `#[cfg(test)]`。如果您的目标仅由集成测试测试,则考虑使用 [`enable_in_production`] 和 [`TEST_FUZZ_WRITE`] 来生成语料库。(但是,请注意伴随 [`enable_in_production`] 的警告。) - 如果您知道目标所在的包,将 `-p ` 传递给 `cargo test`/[`cargo test-fuzz`] 可以显著减少构建时间。类似地,如果您知道您的目标仅从一个集成测试中调用,传递 `--test ` 可以减少构建时间。 - Rust [不允许您]为其他仓库的类型实现 `serde::Serialize`。但是您可以[补丁] 其他仓库以使其类型可序列化。此外,[`cargo-clone`] 可用于获取依赖项的仓库。 - [Serde 属性] 有助于为困难的类型实现 `serde::Serialize`/`serde::Deserialize`。 ## 语义版本控制策略 我们保留更改语料库、崩溃、挂起和工作队列格式的权利,并将此类更改视为非破坏性更改。 ## 许可证 `test-fuzz` 根据 AGPLv3 许可证授权和分发,并包含[宏和内联函数例外]。通俗地说,在您的软件中使用 [`test_fuzz` 宏]、[`test_fuzz_impl` 宏] 或 `test-fuzz` 的[便捷函数和宏]不需要其受 AGPLv3 许可证涵盖。
标签:afl.rs, Cargo子命令, Fuzzing, Rust, Rust安全, SOC Prime, 代码安全审计, 单元测试, 可视化界面, 宏, 开发工具, 测试辅助, 网络流量审计, 语料库生成, 软件测试工具, 通知系统, 通知系统