4 个版本 (重大更改)
0.6.0 | 2021年2月20日 |
---|---|
0.5.0 | 2021年1月31日 |
0.4.0 | 2020年8月12日 |
0.3.0 | 2020年7月17日 |
#1245 在 开发工具
在 2 crates 中使用
12KB
172 代码行
模糊检查
Fuzzcheck 是一个模块化、结构感知且具有反馈驱动的 Rust 函数模糊测试引擎。
给定一个函数 test: (T) -> bool
,您可以使用 fuzzcheck 来找到类型为 T
的值,该值会失败测试或导致崩溃。
具有相同目标的其它crate有 quickcheck
和 proptest
。Fuzzcheck 比这些更强大,因为它根据从运行测试函数生成的反馈来指导测试用例的生成。这种反馈通常是代码覆盖率,但也可能不同。
另一个类似的crate是 cargo-fuzz
,通常与 arbitrary
配对。在这种情况下,fuzzcheck 有一个优势,就是更容易使用、更模块化,并且更基本的结构感知,因此可能更有效率。
工具 fuzzcheck-view
可用于可视化 fuzzcheck 生成的每个/所有测试用例的代码覆盖率。不过它仍然只是一个原型。
遵循 fuzzcheck.neocities.org 上的指南 以开始使用或阅读 docs.rs 上的文档。
设置
需要 Linux 或 macOS。 计划支持 Windows,但我需要帮助。
还需要 Rust nightly。您可以使用以下命令安装它:
rustup toolchain install nightly
虽然这不是严格必要的,但安装 cargo-fuzzcheck
可执行文件将使运行 fuzzcheck 更容易。
cargo install cargo-fuzzcheck
在您的 Cargo.toml
文件中,将 fuzzcheck
添加为开发依赖项
[dev-dependencies]
fuzzcheck = "0.9"
然后,我们需要一种序列化值的方法。默认情况下,fuzzcheck 使用 serde_json
来完成此目的(但可以进行更改)。这意味着我们的数据类型应该实现 serde 的特性。在 Cargo.toml
中添加
[dependencies]
serde = { version = "1.0", features = ["derive"] }
用法
以下是如何使用模糊测试的示例。注意
- 与fuzzcheck相关的所有代码都依赖于
#[cfg(test)]
,因为我们不希望在正常构建中携带fuzzcheck依赖。 - 因为fuzzcheck的宏需要,所以需要使用
#![cfg_attr(test, feature(no_coverage))]
。 - 使用
derive(fuzzcheck::DefaultMutator)
使自定义类型可模糊测试。 - 构建模式来配置模糊测试
// this nightly feature is required by fuzzcheck’s procedural macros
#![cfg_attr(test, feature(no_coverage))]
use serde::{Deserialize, Serialize};
// The DefaultMutator macro creates a mutator for a custom type
// The mutator is accessible via SampleStruct::<T, U>::default_mutator()
#[cfg_attr(test, derive(fuzzcheck::DefaultMutator))]
// the fuzzer needs to serialize and deserialize test cases,
// we use serde by default, but that can be changed
#[derive(Clone, Serialize, Deserialize)]
struct SampleStruct<T, U> {
x: T,
y: U,
}
#[cfg_attr(test, derive(fuzzcheck::DefaultMutator))]
#[derive(Clone, Serialize, Deserialize)]
enum SampleEnum {
A(u16),
B,
C { x: bool, y: bool },
}
fn should_not_crash(xs: &[SampleStruct<u8, SampleEnum>]) {
if xs.len() > 3
&& xs[0].x == 100
&& matches!(xs[0].y, SampleEnum::C { x: false, y: true })
&& xs[1].x == 55
&& matches!(xs[1].y, SampleEnum::C { x: true, y: false })
&& xs[2].x == 87
&& matches!(xs[2].y, SampleEnum::C { x: false, y: false })
&& xs[3].x == 24
&& matches!(xs[3].y, SampleEnum::C { x: true, y: true })
{
panic!()
}
}
// fuzz tests reside along your other tests and have the #[test] attribute
#[cfg(test)]
mod tests {
use super::*;
use fuzzcheck::{DefaultMutator, SerdeSerializer};
#[test]
fn test_function_shouldn_t_crash() {
let _ = fuzzcheck::fuzz_test(should_not_crash) // the test function to fuzz
.default_mutator() // the mutator to generate values of &[SampleStruct<u8, SampleEnum>]
.serde_serializer() // save the test cases to the file system using serde
.default_sensor_and_pool() // gather observations using the default sensor (i.e. recording code coverage)
.arguments_from_cargo_fuzzcheck() // take arguments from the cargo-fuzzcheck command line tool
.stop_after_first_test_failure(true) // stop the fuzzer as soon as a test failure is found
.launch();
}
}
现在我们可以使用cargo-fuzzcheck
,通过Rust nightly版本来启动测试。
rustup override set nightly
# the argument is the *exact* path to the test function
cargo fuzzcheck tests::test_function_shouldn_t_crash
这会启动一个循环,直到找到失败的测试为止。
当找到失败的测试时,会打印以下内容:
Failing test case found. Saving at "fuzz/tests::test_function_shouldn_t_crash/artifacts/59886edc1de2dcc1.json"
这是在我机器上大约50毫秒的模糊测试后显示的内容。
在这里,工件文件的路径是fuzz/tests::test_function_shouldn_t_crash/artifacts/59886edc1de2dcc1.json
。它包含了导致测试失败的JSON编码输入。
[
{
"x": 100,
"y": {
"C": {
"x": false,
"y": true
}
}
},
{
"x": 55,
"y": {
"C": {
"x": true,
"y": false
}
}
},
..
]
最小化失败的测试输入
Fuzzcheck还可以用于最小化一个导致测试失败的输入。如果失败是可恢复的(即它不是段错误/栈溢出),并且fuzzer没有被指令在第一次失败后停止,那么失败的测试用例将自动进行最小化。否则,您可以使用minify
命令。
假设您有一个包含您想要最小化输入的文件crash.json
。使用带有minify
命令和--input-file
选项的cargo fuzzcheck <精确的模糊测试名称>
启动。
cargo fuzzcheck "tests::test_function_shouldn_t_crash" --command minify --input-file "crash.json"
这将重复以“最小化”模式启动fuzzer,并将工件保存到artifacts/crash.minified
文件夹中。每个工件的名字将以其输入的复杂度为前缀。例如,crash.minified/800--fe958d4f003bd4f5.json
的复杂度是8.00
。
您可以在任何时候停止最小化fuzzer,并在crash.minified
文件夹中查找最简单的输入。
关于模糊测试引擎的先前工作
据我所知,进化式、覆盖率指导的模糊测试引擎是由美国模糊跳鼠(AFL)普及的。
Fuzzcheck也是进化式和覆盖率指导的。
后来,LLVM 发布了自己的模糊测试引擎,名为 libFuzzer,其灵感来源于 AFL,但使用 Clang 的 SanitizerCoverage 并在进程内运行(它与被模糊测试的程序位于同一个进程中)。
Fuzzcheck 也是在进程内运行的。它使用 rustc 的 -Z 代码覆盖率-instrument
选项,而不是 SanitizerCoverage 来进行代码覆盖率检测。
AFL 和 libFuzzer 通过操作位字符串(例如 1011101011
)来工作。然而,许多程序在结构化数据上运行,而位字符串级别的突变可能不会映射到结构化数据级别的有意义的突变。这个问题可以通过使用如 protobuf 之类的紧凑二进制编码并提供用于处理结构化数据的自定义突变函数来部分解决。这是一种进行“结构感知模糊测试”的方法(演讲,教程)。
处理结构化数据的另一种方法是使用生成器,就像 QuickCheck 的 Arbitrary
特性。然后,“将覆盖率引导的模糊测试提供的原始字节数据缓冲区输入视为一系列随机值,并围绕它实现一个‘随机’数生成器。”(@fitzgen 的引用博客文章)。工具 cargo-fuzz
最近实现了这种方法。
Fuzzcheck 也是结构感知的,但与之前尝试的结构感知模糊测试不同,它不使用如 protobuf 之类的中间二进制编码,也不使用类似 Quickcheck 的生成器。相反,它直接对进程内的类型化值进行突变。这在许多方面都更好。首先,它更快,因为不需要在每次迭代中编码和解码输入。其次,输入的复杂性由用户定义的函数给出,这比计算 protobuf 编码的字节数更准确。最后,并且最重要的是,突变比在 protobuf 或 Arbitrary
的基于字节数据缓冲区的 RNG 上进行的突变更快、更有意义。我特别喜欢 fuzzcheck 的一个细节,并且只能通过突变类型值来实现,那就是每个突变都是 就地 进行的,并且是可逆的。这意味着生成新的测试用例非常快,通常甚至可以做到零分配。
当我为 Swift 开发 Fuzzcheck 时,一些研究人员为 Coq 开发了 Fuzzchick(论文)。它是一个作为 Quickchick 扩展实现的覆盖率引导的基于属性的测试工具。据我所知,它是除 fuzzcheck 之外唯一具有相同哲学的工具。名称 fuzzcheck
和 Fuzzchick
的相似性是一个巧合。