bigywater.tech
Rust · 165

Fuzz 测试

单元测试只覆盖你想到的输入。Fuzz 测试让程序自己去找:生成输入、观察覆盖率、保留带来新分支的输入再变异。这篇用一个藏了 bug 的小函数,把 cargo fuzz 从建目录到找到崩溃、复现、对比随机测试的过程完整跑一遍。

11,480
fuzzer 命中越界 bug 所需的执行次数(固定种子)
0 / 2 亿
随机字节测试 2 亿次命中 bug 的次数
1.8×
--sanitizer none 对默认 ASan 的执行速度比

目录与目标

实测 · 本机执行输出
  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), exiting

随机测试对照

实测 · 本机执行输出
  200000000 次随机输入:通过前 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-

unsafe 的 bug

实测 · 本机执行输出
  被测函数:
    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 次/秒

分步演示

cargo fuzz
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。nightly 工具链用 rustup 装在隔离目录(不影响 brew 的 rustc):rustc 1.101.0-nightly (21b707e3f 2026-09-30)、miri 0.1.0 (21b707e3f9 2026-09-30)。cargo-fuzz 0.13.2(cargo install --locked,装在隔离目录),libfuzzer-sys 0.4;所有运行固定 -seed=1
    • 没有验证:Linux 上的表现、cargo fuzz coverage 生成覆盖率报告、结构化输入(arbitrary crate)、字典(-dict)、并行(-jobs/-workers)、长时间(小时级)运行能多发现什么、对更长崩溃输入的 tmin 缩减效果
    • 本篇的随机对照是自己写的 xorshift 随机字节生成器,不是任何现成的随机测试工具
    京ICP备2026067502号-1京公网安备11010502063487号