51 个版本 (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.6 | 2020年11月19日 |
#86 在 编码
每月下载量 34,896
用于 72 个 包(4 个直接使用)
56KB
557 行
test-fuzz
test-fuzz
是一个Cargo子命令和一组Rust宏,用于自动化与 afl.rs
相关的模糊测试任务,包括
- 生成模糊测试语料库
- 实现模糊测试框架
test-fuzz
通过Rust的测试功能完成这些任务(部分)。例如,要生成模糊测试语料库,test-fuzz
在每次调用时记录目标参数,这发生在 cargo test
调用期间。同样,test-fuzz
将模糊测试框架作为在 cargo-test
生成的二进制文件中的附加测试实现。这种与Rust测试功能的紧密集成是命名 test
-fuzz
的原因。
内容
安装
使用以下命令安装 cargo-test-fuzz
和 afl.rs
cargo install cargo-test-fuzz cargo-afl
概述
使用 test-fuzz
进行模糊测试基本上是三个步骤:*
-
确定模糊测试目标:
- 将以下
dependencies
添加到目标crate的Cargo.toml
文件中serde = "*" test-fuzz = "*"
- 在目标函数之前使用
test_fuzz
宏#[test_fuzz::test_fuzz] fn foo(...) { ... }
- 将以下
-
通过运行
cargo test
生成语料库cargo test
-
模糊测试目标 通过运行
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
的非标准特质 test_fuzz::FromRef<X>
将类型 Y
转换为。反序列化时,使用类型 Y
的非标准特质 test_fuzz::Into<X>
将这些值转换回类型 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_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 {}
test_fuzz_impl
宏
每当在 impl
块中使用 test_fuzz
宏时,必须使用 test_fuzz_impl
宏来先于 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
命令用于与模糊测试目标交互,并操作它们的语料库、崩溃、挂起和工作队列。示例调用包括
-
列出模糊测试目标
cargo test-fuzz --list
-
显示目标
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] [-- <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
,并为它实现了 From
和 test_fuzz::Into
特性
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<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_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
来自动检查目标函数中的无效类型转换。
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::默认() |
键
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
当不运行测试这些目标的测试时,生成语料库文件。
限制
可克隆的参数
目标参数必须实现 Clone
特性。此要求的原因是参数需要在两个地方使用:在写入语料库文件的 test-fuzz
内部函数中,以及在目标函数体中。为了解决这个冲突,在传递给前者之前参数会被克隆。
可序列化/反序列化的参数
通常,目标的参数必须实现 serde::Serialize
和 serde::Deserialize
特性,例如通过 派生它们。我们说“通常”,因为 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 <package>
传递给cargo test
/cargo test-fuzz
可以显著减少构建时间。同样,如果您知道目标仅从一个集成测试中被调用,传递--test <name>
也可以减少构建时间。 -
Rust不允许您为其他存储库的类型实现
serde::Serialize
。但您可能能够修补其他存储库以使其类型可序列化。此外,cargo-clone
可以用于获取依赖项的存储库。 -
Serde属性在实现
serde::Serialize
/serde::Deserialize
对于难以处理的数据类型时非常有用。
许可证
test-fuzz
根据AGPLv3许可协议许可并分发,带有宏和内联函数异常。简单来说,在您的软件中使用test_fuzz
宏、test_fuzz_impl
宏或test-fuzz
的便捷函数和宏不需要它受AGPLv3许可证的约束。
依赖项
~2.7–5MB
~84K SLoC