单元测试只覆盖你想到的输入。Fuzz 测试让程序自己去找:生成输入、观察覆盖率、保留带来新分支的输入再变异。这篇用一个藏了 bug 的小函数,把 cargo fuzz 从建目录到找到崩溃、复现、对比随机测试的过程完整跑一遍。
cargo fuzz init:退出码 0
生成/使用的文件:
Cargo.toml
fuzz/.gitignore
fuzz/Cargo.toml
fuzz/fuzz_targets/parse.rs
fuzz/fuzz_targets/parse_fixed.rs
fuzz/fuzz_targets/payload.rs
src/lib.rs
被测代码 src/lib.rs(parse 里藏了一个 bug:长度恰好是 6 时 data[6] 越界):
1 | pub fn parse(data: &[u8]) -> Option<u32> {
2 | if data.len() < 4 || &data[..4] != b"FUZZ" { return None; }
3 | if data.len() > 4 && data[4] == b'!' {
4 | if data.len() > 5 && data[5] == b'?' {
5 | return Some(data[6] as u32);
6 | }
7 | }
8 | Some(0)
9 | }
fuzz/fuzz_targets/parse.rs:
1 | #![no_main]
2 |
3 | use libfuzzer_sys::fuzz_target;
4 |
5 | fuzz_target!(|data: &[u8]| {
6 | let _ = fz::parse(data);
7 | });Fuzz 常与 sanitizer 一起使用,见Sanitizer;cargo-fuzz 使用 nightly 工具链。
退出码 1;含编译与运行共 8.9 秒
INFO: Seed: 1
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
覆盖率相关日志(#执行次数 事件 cov=覆盖的边 ft=特征数 corp=语料数/字节 exec/s):
#2 INITED cov: 17 ft: 17 corp: 1/1b exec/s: 0 rss: 36Mb
#12 NEW cov: 20 ft: 20 corp: 2/5b lim: 4 exec/s: 0 rss: 36Mb L: 4/4 MS: 5 CrossOver-InsertByte-CopyPart-ChangeBit-CrossOver-
#192 NEW cov: 21 ft: 21 corp: 3/9b lim: 4 exec/s: 0 rss: 36Mb L: 4/4 MS: 5 ShuffleBytes-CopyPart-EraseBytes-CopyPart-CMP- DE: "FUZZ"-
#484 NEW cov: 23 ft: 23 corp: 4/14b lim: 6 exec/s: 0 rss: 36Mb L: 5/5 MS: 2 CopyPart-CrossOver-
#696 NEW cov: 25 ft: 25 corp: 5/22b lim: 8 exec/s: 0 rss: 36Mb L: 8/8 MS: 2 CopyPart-InsertRepeatedBytes-
#865 NEW cov: 26 ft: 26 corp: 6/30b lim: 8 exec/s: 0 rss: 36Mb L: 8/8 MS: 4 InsertByte-InsertByte-EraseBytes-PersAutoDict- DE: "FUZZ"-
#8791 NEW cov: 27 ft: 27 corp: 7/35b lim: 86 exec/s: 0 rss: 37Mb L: 5/8 MS: 1 InsertByte-
#8957 NEW cov: 29 ft: 29 corp: 8/44b lim: 86 exec/s: 0 rss: 37Mb L: 9/9 MS: 1 PersAutoDict- DE: "FUZZ"-
#8973 NEW cov: 30 ft: 30 corp: 9/51b lim: 86 exec/s: 0 rss: 37Mb L: 7/9 MS: 1 EraseBytes-
#10974 REDUCE cov: 30 ft: 30 corp: 9/50b lim: 104 exec/s: 0 rss: 37Mb L: 6/9 MS: 1 EraseBytes-
崩溃信息:
thread '<unnamed>' (8140672) panicked at <实验目录>/proj/u165/src/lib.rs:5:25:
index out of bounds: the len is 6 but the index is 6
SUMMARY: libFuzzer: deadly signal
MS: 1 ChangeByte-; base unit: 999522c4ba612a9e595443ae18ec05d1864d98dd
0x46,0x55,0x5a,0x5a,0x21,0x3f,
Base64: RlVaWiE/
最后一条统计: #10974 REDUCE cov: 30 ft: 30 corp: 9/50b lim: 104 exec/s: 0 rss: 37Mb L: 6/9 MS: 1 EraseBytes-
同一个种子重放(二分 -runs):第 11480 次执行时触发崩溃(-runs=11479 不崩,-runs=11480 崩);另两次重复结果一致:True 崩溃输入 6 字节:[70, 85, 90, 90, 33, 63] = b'FUZZ!?'
重放(cargo fuzz run parse <崩溃文件>):退出码 1
thread '<unnamed>' (8141284) panicked at <实验目录>/proj/u165/src/lib.rs:5:25:
index out of bounds: the len is 6 but the index is 6
SUMMARY: libFuzzer: deadly signal
Error: Fuzz target exited with exit status: 77
cargo fuzz fmt:
`std::fmt::Debug`:
[70, 85, 90, 90, 33, 63]
语料文件 8 个
4 字节 b'\n\n\x03\x08'
4 字节 b'FUZZ'
5 字节 b'FUZZ\x03'
5 字节 b'FUZZ!'
6 字节 b'FUZZ!Z'
8 字节 b'FUZZ?FU\x03'
8 字节 b'FZZ\x00\x00\x00\x00Z'
9 字节 b'FUZZ!FUZZ' cargo fuzz tmin 退出码 0
CRASH_MIN: minimizing crash input: 'fuzz/artifacts/parse/crash-…' (6 bytes)
INFO: Done MinimizeCrashInputInternalStep, no crashes found
CRASH_MIN: failed to minimize beyond fuzz/artifacts/parse/crash-… (6 bytes), exiting200000000 次随机输入:通过前 4 字节 FUZZ 的 0 次,命中 bug 的 0 次,耗时 4.0 秒
退出码 0
Done 41765493 runs in 31 second(s)
覆盖率增长事件 11 次;最后一次:#11757 NEW cov: 33 ft: 33 corp: 12/73b lim: 104 exec/s: 0 rss: 37Mb L: 7/10 MS: 1 EraseBytes- 被测函数:
pub fn payload(data: &[u8]) -> u8 {
if data.len() < 2 { return 0; }
let n = data[0] as usize;
unsafe { *data.get_unchecked(1 + n) }
}
默认(带 debug assertions 的 release 构建):退出码 1
thread '<unnamed>' (8142840) panicked at <实验目录>/proj/u165/src/lib.rs:22:20:
unsafe precondition(s) violated: slice::get_unchecked requires that the index is within the slice
==38939== ERROR: libFuzzer: deadly signal
#1 fuzzer::PrintStackTrace()+0x30
#2 fuzzer::Fuzzer::CrashCallback()+0x54
#6 _RNvNtNtNtCsgneyKrj87uf_3std3sys3pal4unix14abort_internal+0x8
#7 _RNvNtCsgneyKrj87uf_3std7process5abort+0x8
#8 _RNCNvCsV36ZhzaJIF_13libfuzzer_sys10initialize0B3_+0xb8
#9 _RNvNtCsgneyKrj87uf_3std9panicking15panic_with_hook+0x26c
#10 _RNCNvNtCsgneyKrj87uf_3std9panicking13panic_handler0B5_+0x6c
#11 _RINvNtNtCsgneyKrj87uf_3std3sys9backtrace26___rust_end_short_backtraceNCNvNtB6_9panicking13panic_handler0zEB6_+0x8
#12 _RNvCs8kYXCe6bXIp_7___rustc17rust_begin_unwind+0x1c
#13 _RNvNtCshIDsKH5exNL_4core9panicking18panic_nounwind_fmt+0x24
#14 _RNvCsjT72xSJpIW2_2fz7payload+0x114
cargo fuzz run -O(关掉 debug assertions,仍带 ASan):退出码 1
==39390==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000d7b at pc 0x000102f3d338 bp 0x00016cec1310 sp 0x00016cec1308
READ of size 1 at 0x602000000d7b thread T0
#0 payload lib.rs:22
#1 _RNvNvCsjoLmVVAucbG_7payload1__19___libfuzzer_sys_run payload.rs:6
#2 rust_fuzzer_test_input lib.rs:276
#3 _RINvNvNtCsgneyKrj87uf_3std9panicking12catch_unwind7do_callNCNvCsV36ZhzaJIF_13libfuzzer_sys15test_input_wrap0lEBY_+0xac
#4 __rust_try+0x18
#5 LLVMFuzzerTestOneInput+0x16c
#6 fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long)+0x158
#7 fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool, bool*)+0x48
#8 fuzzer::Fuzzer::MutateAndTestOne()+0x22c
#9 fuzzer::Fuzzer::Loop(std::__1::vector<fuzzer::SizedFile, std::__1::allocator<fuzzer::SizedFile>>&)+0x354
#10 fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long))+0x1a3c
#11 main+0x24
cargo fuzz run -O --sanitizer none:退出码 0
Done 82776333 runs in 31 second(s)对比用 Miri 检查未定义行为:Miri 检查未定义行为。
ASan(默认) 执行 27,790,311 次,平均 1,323,348 次/秒 --sanitizer none 执行 50,995,363 次,平均 2,428,350 次/秒