#test-cases #fuzzing #property-testing #fuzzer #properties #test

nightly fuzzcheck

为 Rust 函数提供的模块化、结构感知和反馈驱动的模糊测试引擎

18 个版本 (11 个破坏性更新)

0.12.1 2022 年 7 月 9 日
0.11.0 2022 年 2 月 26 日
0.10.1 2021 年 12 月 5 日
0.9.0 2021 年 11 月 19 日
0.3.0 2020 年 7 月 17 日

#330 in 测试

Download history 16/week @ 2024-03-12 10/week @ 2024-03-19 14/week @ 2024-03-26 43/week @ 2024-04-02 16/week @ 2024-04-09 4/week @ 2024-04-16 15/week @ 2024-04-23 9/week @ 2024-04-30 7/week @ 2024-05-07 20/week @ 2024-05-14 18/week @ 2024-05-21 34/week @ 2024-05-28 10/week @ 2024-06-04 22/week @ 2024-06-11 13/week @ 2024-06-18 11/week @ 2024-06-25

63 每月下载量
3 crates 中使用

MIT 许可证

655KB
16K SLoC

Fuzzcheck

CI Docs MIT licensed crates.io

Fuzzcheck 是一个模块化、结构感知和反馈驱动的 Rust 函数模糊测试引擎。

给定一个函数 test: (T) -> bool,您可以使用 fuzzcheck 找到一个类型为 T 的值,该值会导致测试失败或崩溃。

该工具 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-dependency

[dev-dependencies]
fuzzcheck = "0.12"

然后,我们需要一种序列化值的方法。默认情况下,fuzzcheck 使用 serde_json 来实现此目的(但可以更改)。这意味着我们的数据类型应该实现 serde 的 traits。在 Cargo.toml 中添加以下内容:

[dependencies]
serde = { version = "1.0", features = ["derive"] }

使用方法

以下是使用模糊测试的示例。注意

  1. 与fuzzcheck相关的所有代码都基于#[cfg(test)]条件编译,因为我们不希望在常规构建中携带fuzzcheck依赖
  2. 模糊测试的宏需要#![cfg_attr(test, feature(no_coverage))]
  3. 使用derive(fuzzcheck::DefaultMutator)可以使自定义类型可模糊测试
#![cfg_attr(fuzzing, feature(no_coverage))]
use serde::{Deserialize, Serialize};

#[cfg_attr(fuzzing, derive(fuzzcheck::DefaultMutator))]
#[derive(Clone, Serialize, Deserialize)]
struct SampleStruct<T, U> {
    x: T,
    y: U,
}

#[cfg_attr(fuzzing, 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(all(fuzzing, test))]
mod tests {
    #[test]
    fn test_function_shouldn_t_crash() {
        let result = fuzzcheck::fuzz_test(super::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();
        assert!(!result.found_test_failure);
    }
}

现在我们可以使用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

这会启动一个循环,直到找到失败的测试为止。在我的机器上大约进行了50ms的模糊测试后,打印出以下行

Failing test case found. Saving at "fuzz/tests::test_function_shouldn_t_crash/artifacts/59886edc1de2dcc1.json"

文件59886edc1de2dcc1.json包含导致测试失败的JSON编码输入。

[
  {
    "x": 100,
    "y": {
      "C": {
        "x": false,
        "y": true
      }
    }
  },
  {
    "x": 55,
    "y": {
      "C": {
        "x": true,
        "y": false
      }
    }
  },
  ..
]

最小化失败的测试输入

Fuzzcheck还可以用来最小化一个失败的测试的大输入。如果失败是可以恢复的(即它不是段错误/堆栈溢出),并且模糊器没有被指示在第一次失败后停止,那么失败的测试案例将自动最小化。否则,您可以使用minify命令。

假设您有一个包含您想要最小化的输入的文件crash.json。使用minify命令和--input-file选项启动带有cargo fuzzcheck <模糊测试的确切名称>

cargo fuzzcheck "tests::test_function_shouldn_t_crash" --command minify --input-file "crash.json"

这将反复以“最小化”模式启动模糊器,并将工件保存在artifacts/crash.minified文件夹中。每个工件的名字将以输入的复杂性为前缀。例如,crash.minified/800--fe958d4f003bd4f5.json的复杂性为8.00

您可以在任何时候停止最小化模糊器,并在crash.minified文件夹中查找最简单的输入。

替代方案

其他具有相同目标的crate有quickcheckproptest。Fuzzcheck比这些更强大,因为它可以根据测试函数运行时产生的反馈来引导测试用例的生成。这种反馈通常是代码覆盖率,但也可能不同。

另一个类似的crate是cargo-fuzz,通常与arbitrary一起使用。在这种情况下,fuzzcheck的优势在于使用更简单、更模块化,并且更基本的结构感知,因此可能更有效。

关于模糊引擎的先前工作

据我所知,进化式、覆盖率引导的模糊测试引擎被美国模糊跳鼠(AFL)普及。
Fuzzcheck也是进化式和覆盖率引导的。

后来,LLVM发布了它自己的模糊测试引擎,即libFuzzer,它基于与AFL相同的理念,但使用了Clang的SanitizerCoverage,并且是进程内的(它存在于被模糊测试的程序相同的进程中)。
Fuzzcheck也是进程内的。它使用rustc的-Z instrument-coverage选项而不是SanitizerCoverage来进行代码覆盖率检测。

AFL和libFuzzer通过操作位串(例如1011101011)来工作。然而,许多程序在结构化数据上工作,并且位串级别的变异可能不会映射到结构化数据级别的有意义的变异。这个问题可以通过使用像protobuf这样的紧凑二进制编码以及为libFuzzer提供在结构化数据本身上工作的自定义变异函数来部分解决。这是一种进行“结构感知模糊测试”的方法(演讲教程)。

处理结构化数据的另一种方法是使用生成器,就像QuickCheck的Arbitrary特质一样。然后“将覆盖率引导模糊测试提供的原始字节缓冲区输入视为一系列随机值,并在其周围实现一个‘随机’数生成器。”(@fitzgen的引用博客)。工具cargo-fuzz最近实现了这种方法。

Fuzzcheck也是结构感知的,但与之前结构感知模糊测试的尝试不同,它不使用像protobuf这样的中间二进制编码,也不使用类似于Quickcheck的生成器。相反,它直接在进程内变异有类型值。这在很多方面都更好。首先,它更快,因为不需要在每次迭代中对输入进行编码和解码。其次,输入的复杂度由用户定义的函数给出,这比计算protobuf编码的字节数更准确。最后,最重要的是,变异比在protobuf或Arbitrary的基于字节的随机数生成器上做的变异更快、更有意义。fuzzcheck的一个我特别喜欢的细节,这也是因为它变异有类型值才可能实现的,是每个变异都是原地进行的并且是可逆的。这意味着生成新的测试用例非常快,通常甚至可以不进行任何分配。

在我为Swift开发Fuzzcheck的时候,一些研究人员开发了Coq的Fuzzchick(《论文》)。它是一个作为Quickchick扩展实现的覆盖率引导属性测试工具。据我所知,它是唯一与fuzzcheck有相同哲学的其他工具。名字fuzzcheckFuzzchick之间的相似性是一个巧合。

LibAFL是另一个用Rust编写的模块化模糊测试器。它相对较新。

依赖项

~5–6.5MB
~127K SLoC