48 个版本 (30 个稳定版)

新功能 6.0.0 2024年8月21日
5.2.2 2024年7月12日
5.2.0 2024年5月10日
5.0.0 2024年1月22日
0.1.0-alpha.22020年11月13日

#349测试

Download history 194/week @ 2024-05-03 130/week @ 2024-05-10 10/week @ 2024-05-17 3/week @ 2024-05-24 5/week @ 2024-05-31 4/week @ 2024-06-07 1/week @ 2024-06-28 104/week @ 2024-07-05 125/week @ 2024-07-12 3/week @ 2024-07-19 113/week @ 2024-08-16

每月113次下载

AGPL-3.0 WITH mif-exception

120KB
1.5K SLoC

test-fuzz

test-fuzz 是一个 Cargo 子命令和一组 Rust 宏,用于自动化与 afl.rs 相关的模糊测试任务,包括

  • 生成模糊测试语料库
  • 实现模糊测试框架

test-fuzz 使用 Rust 的测试设施来完成这些任务(部分)。这种与 Rust 测试设施的紧密集成是命名 test-fuzz 的原因。

内容

  1. 安装
  2. 概述
  3. 组件
  4. test-fuzz 软件包功能
  5. 自动生成的语料库文件
  6. 环境变量
  7. 限制
  8. 技巧和窍门
  9. 许可协议

安装

使用以下命令安装 cargo-test-fuzzafl.rs

cargo install cargo-test-fuzz cargo-afl

概述

Fuzzing with 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(例如,特质边界)。这可能有必要,例如,如果目标的参数类型是关联类型。有关示例,请参阅本存储库中的 associated_type.rs

generic_args= "parameters"

在模糊测试时,将 parameters 用作目标的类型参数。例如

#[test_fuzz(generic_args = "String")]
fn foo<T: Clone + Debug + Serialize>(x: &T) {
    ...
}

注意:目标的所有参数必须可序列化,以便对其类型参数的每个实例进行序列化。但是,当目标使用 parameters 实例化时,其参数必须可反序列化。

impl_generic_args= "parameters"

在模糊测试时,将 parameters 用作目标的 Self 类型参数。例如

#[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 的实现将类型 X 的值转换为类型 Y,或者使用类型 &X 的非标准特质的实现将类型 X 转换为类型 Y。在反序列化时,使用类型 Y 的非标准特质的实现将值转换回类型 X

即,使用 convert = "X, Y" 必须伴随某些实现。如果 X 实现 Clone,那么 Y 可以实现以下内容

impl From<X> for Y {
    fn from(x: X) -> Self {
        ...
    }
}

如果 X 没有实现 Clone,那么 Y 必须实现以下内容

impl test_fuzz::FromRef<X> for Y {
    fn from_ref(x: &X) -> Self {
        ...
    }
}

此外,Y 必须实现以下内容(无论 X 是否实现 Clone

impl test_fuzz::Into<X> for Y {
    fn into(self) -> X {
        ...
    }
}

test_fuzz::Into 的定义与 std::convert::Into 相同。使用非标准特质的理由是为了避免标准特质的泛型实现可能引起的冲突。

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 外部性

no_auto_generate

不要尝试为该目标自动生成语料库文件。

only_generic_args

在运行测试时记录目标泛型参数,但不生成语料库文件,也不实现模糊测试框架。当目标是泛型函数,但不确定应该使用哪些类型参数进行模糊测试时,这可能很有用。

预期的流程是:启用only_generic_args,然后运行cargo test,然后运行cargo test-fuzz --display generic-args。结果中的一个泛型参数可能可以用作generic_argsparameters。同样,由cargo test-fuzz --display impl-generic-args产生的泛型参数可能可以用作impl_generic_argsparameters

但是请注意,即使目标在测试期间以某些参数被调用,这并不意味着当使用这些参数时,目标的参数是可序列化/反序列化的。`--display generic-args`/`--display impl-generic-args`的结果只是建议性的。

重命名= "名称"

在将模块添加到封装作用域时,将目标视为具有name的名称。`test_fuzz`宏的展开向封装作用域添加了一个模块定义。默认情况下,模块的名称如下

  • 如果目标不出现在impl块中,模块的名称为target_fuzz,其中target是目标的名称。
  • 如果目标出现在impl块中,模块的名称为path_target_fuzz,其中pathimpl's 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 {}

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] [-- <ARGS>...]

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
      --cpus <N>                  Fuzz using at most <N> cpus; default is all but one
      --display <OBJECT>          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
                                  <OBJECT>, 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 <SECONDS>
      --features <FEATURES>       Space or comma separated list of features to activate
      --list                      List fuzz targets
      --manifest-path <PATH>      Path to Cargo.toml
      --max-total-time <SECONDS>  Fuzz at most <SECONDS> of time (equivalent to -- -V <SECONDS>)
      --no-default-features       Do not activate the `default` feature
      --no-run                    Compile, but don't fuzz
      --no-ui                     Disable user interface
  -p, --package <PACKAGE>         Package containing fuzz target
      --persistent                Enable persistent mode fuzzing
      --pretty                    Pretty-print debug output when displaying/replaying
      --replay <OBJECT>           Replay corpus, crashes, hangs, or work queue. By default, an
                                  uninstrumented fuzz target is used. To replay with instrumentation
                                  append `-instrumented` to <OBJECT>, 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 <SECONDS>           If there are not sufficiently many cpus to fuzz all targets
                                  simultaneously, fuzz them in intervals of <SECONDS> [default:
                                  1200]
      --test <NAME>               Integration test containing fuzz target
      --timeout <TIMEOUT>         Number of seconds to consider a hang when fuzzing or replaying
                                  (equivalent to -- -t <TIMEOUT * 1000> when fuzzing)
      --verbose                   Show build output when displaying/replaying
  -h, --help                      Print help
  -V, --version                   Print version

Try `cargo afl fuzz --help` to see additional fuzzer options.

便利函数和宏

警告: 这些实用工具不包括在语义版本控制中,并且可能在 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<S>(&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<D>(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 特性的引用类型的目标参数。它旨在与 convert 选项一起使用。

具体来说,以下形式的调用声明了一个类型 LeakedX,并为其实现了 Fromtest_fuzz::Into 特性

leak!(X, LeakedX);

然后可以像以下这样使用 LeakedXconvert 选项

#[test_fuzz::test_fuzz(convert = "&X, LeakedX")

一个示例,其中 XPath,出现在此存储库中的 conversion.rs

更普遍地,以下形式的调用 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_refdeserialize_ref 函数类似于 leak!,但它们旨在与 Serde 的 serialize_withdeserialize_with 字段属性(分别)一起使用。

fn serialize_ref<S, T>(x: &&T, serializer: S) -> Result<S::Ok, S::Error>
where
    S: serde::Serializer,
    T: serde::Serialize,
{
    <T as 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 = <T as serde::de::Deserialize>::deserialize(deserializer)?;
    Ok(Box::leak(Box::new(x)))
}

serialize_ref_mut / deserialize_ref_mut

serialize_ref_mutdeserialize_ref_mutserialize_refdeserialize_ref(分别)类似,区别在于它们在可变引用上操作,而不是在不可变引用上。

test-fuzz 软件包功能

本节中的功能适用于整个 test-fuzz 包。如需启用,请按照 《Cargo Book》 中的说明在 test-fuzz 的依赖规范中启用。例如,要启用 cast_checks 功能,请使用

test-fuzz = { version = "*", features = ["cast_checks"] }

test-fuzz 包当前支持以下功能

cast_checks

使用 cast_checks 自动检查目标函数中的无效转换。

Serde 格式

test-fuzz 可以使用多种 Serde 格式序列化目标参数。以下用于选择格式的功能。

自动生成的语料库文件

cargo-test-fuzz 可以自动为实现了某些特征的类型生成值。如果目标的所有参数类型都实现了这些特征,cargo-test-fuzz 可以自动为目标生成语料库文件。

cargo-test-fuzz 目前支持的特征及其生成的值如下

特征
有界 T::min_value()T::max_value()
有界++一个 T::min_value() + T::一个()
有界+++两个 T::min_value() / T::两个() + T::max_value() / T::两个()
有界+++两个+一个 T::min_value() / T::两个() + T::max_value() / T::两个() + T::一个()
有界++一个 T::max_value() - T::一个()
默认 T::默认()

环境变量

TEST_FUZZ_LOG

在宏展开期间

  • 如果 TEST_FUZZ_LOG 设置为 1,则将所有仪器化的模糊目标模块定义写入标准输出。
  • 如果将 TEST_FUZZ_LOG 设置为crate名称,则将那个crate的instrumented模糊目标和模块定义写入标准输出。

这可以用于调试。

TEST_FUZZ_MANIFEST_PATH

当在包目录外运行目标时,在此位置查找包的Cargo.toml文件。当使用enable_in_production时,可能需要设置此环境变量。

TEST_FUZZ_WRITE

当为设置了enable_in_production的目标运行测试时,生成语料库文件。

限制

可克隆参数

目标参数必须实现Clone特质。这个要求的原因是参数需要在两个地方使用:在一个用于写入语料库文件的test-fuzz内部函数中,以及在目标函数的主体中。为了解决这个冲突,在传递给前者之前,参数会被克隆。

可序列化/反序列化参数

通常,目标的参数必须实现serde::Serializeserde::Deserialize特质,例如通过推导它们。我们之所以说“通常”,是因为test-fuzz知道如何处理某些特殊案例,这些案例通常不可序列化/反序列化。例如,当序列化时,类型为&str的参数会被转换为String,在反序列化时再转换回&str。也请参阅generic_argsimpl_generic_args上面的内容。

全局变量

test-fuzz实现的模糊测试工具不会初始化全局变量。虽然execute_with提供了一些补救措施,但这并不是一个完整的解决方案。通常,模糊测试依赖于全局变量的函数需要特定的方法。

convertgeneric_args / impl_generic_args

这些选项在以下意义上是不兼容的。如果模糊目标参数类型是一个类型参数,convert将尝试匹配类型参数,而不是参数设置的类型。支持后者似乎需要像编译器执行的那样模拟类型替换。然而,这目前尚未实现。

技巧和窍门

  • #[cfg(test)] 未启用集成测试。如果您的目标仅通过集成测试进行测试,那么请考虑使用enable_in_productionTEST_FUZZ_WRITE来生成语料库。(注意enable_in_production的伴随警告。)

  • 如果您知道目标所在的包,可以通过将 -p <package> 传递给 cargo test/cargo test-fuzz 显著减少构建时间。同样,如果您知道目标仅从一个集成测试中调用,通过传递 --test <name> 也可以减少构建时间。

  • Rust 不允许您 为其他存储库的类型实现 serde::Serialize。但您可能能够 修补 其他存储库以使它们的类型可序列化。此外,cargo-clone 可以用于获取依赖项的存储库。

  • Serde 属性 可以在实现 serde::Serialize/serde::Deserialize 对于困难类型非常有帮助。

许可协议

test-fuzz 根据 AGPLv3 许可证进行许可和分发,带有 宏和内联函数异常。简单来说,在您的软件中使用 test_fuzztest_fuzz_impltest-fuzz便利函数和宏,不需要它被 AGPLv3 许可证所涵盖。

依赖项

~8–22MB
~297K SLoC