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.2 | 2020年11月13日 |
#349 在 测试
每月113次下载
120KB
1.5K SLoC
test-fuzz
test-fuzz
是一个 Cargo 子命令和一组 Rust 宏,用于自动化与 afl.rs
相关的模糊测试任务,包括
- 生成模糊测试语料库
- 实现模糊测试框架
test-fuzz
使用 Rust 的测试设施来完成这些任务(部分)。这种与 Rust 测试设施的紧密集成是命名 test
-fuzz
的原因。
内容
安装
使用以下命令安装 cargo-test-fuzz
和 afl.rs
cargo install cargo-test-fuzz cargo-afl
概述
Fuzzing with 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
的非标准特质的实现将类型 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_args
的parameters
。同样,由cargo test-fuzz --display impl-generic-args
产生的泛型参数可能可以用作impl_generic_args
的parameters
。
但是请注意,即使目标在测试期间以某些参数被调用,这并不意味着当使用这些参数时,目标的参数是可序列化/反序列化的。`--display generic-args`/`--display impl-generic-args`的结果只是建议性的。
重命名= "名称"
在将模块添加到封装作用域时,将目标视为具有name
的名称。`test_fuzz`宏的展开向封装作用域添加了一个模块定义。默认情况下,模块的名称如下
- 如果目标不出现在
impl
块中,模块的名称为target_fuzz
,其中target
是目标的名称。 - 如果目标出现在
impl
块中,模块的名称为path_target_fuzz
,其中path
是impl
'sSelf
类型的路径,将其转换为蛇形并使用_
连接。
但是,使用此选项会导致模块被命名为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
命令与模糊目标进行交互,并操作它们的语料库、崩溃、挂起和工作队列。示例调用包括
-
列出模糊目标
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")
一个示例,其中 X
是 Path
,出现在此存储库中的 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_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
包。如需启用,请按照 《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的instrumented模糊目标和模块定义写入标准输出。
这可以用于调试。
TEST_FUZZ_MANIFEST_PATH
当在包目录外运行目标时,在此位置查找包的Cargo.toml
文件。当使用enable_in_production
时,可能需要设置此环境变量。
TEST_FUZZ_WRITE
当为设置了enable_in_production
的目标运行测试时,生成语料库文件。
限制
可克隆参数
目标参数必须实现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 许可证所涵盖。
依赖项
~8–22MB
~297K SLoC