1.2 教程

使用 cargo-fuzz 模糊测试 rust-url

译文 · 基于 Rust Fuzz Book

教程

原文链接: https://rust-fuzz.github.io/book/cargo-fuzz/tutorial.html

在本教程中,我们将对 URL 解析 crate rust-url 进行模糊测试。目标是找到模糊器生成的某种输入,在传给 Url::parse 时会导致 panic 或崩溃。

首先克隆 rust-url 仓库并进入该目录:

1
2
git clone https://github.com/servo/rust-url.git
cd rust-url

虽然可以对 master 上的最新提交进行模糊测试,但我们将检出已知存在解析 bug 的特定修订版本:

1
git checkout bfa167b4e0253642b6766a7aa74a99df60a94048

初始化 cargo-fuzz:

1
cargo fuzz init

这将创建一个名为 fuzz_targets 的目录,其中包含一系列模糊测试目标。通常建议将 init 生成的文件纳入版本控制。每个模糊测试目标都是一个 Rust 程序:模糊器向其提供随机数据,用于测试某个 crate(此处为 rust-url)。cargo fuzz init 会自动为我们生成一个初始模糊测试目标。 使用 cargo fuzz list 查看所有现有模糊测试目标:

1
cargo fuzz list

该模糊测试目标的源代码默认位于 fuzz/fuzz_targets/<fuzz target name>.rs。打开该文件并编辑为如下内容:

1
2
3
4
5
6
7
8
9
#![no_main]
#[macro_use] extern crate libfuzzer_sys;
extern crate url;

fuzz_target!(|data: &[u8]| {
    if let Ok(s) = std::str::from_utf8(data) {
        let _ = url::Url::parse(s);
    }
});

libFuzzer 会反复用伪随机字节切片调用 fuzz_target!() 的主体,直到程序触发错误条件(段错误、panic 等)。编写 fuzz_target!() 主体以命中你需要测试的入口点。

由于生成的数据是字节切片,而 rust-url 在解析时期望 UTF-8 的 &str,因此需要将其转换为 &str。

开始模糊测试,运行:

1
cargo fuzz run <fuzz target name>

恭喜,你已经在进行模糊测试了!你看到的输出由模糊器 libFuzzer 生成。要了解输出含义,请参阅 libFuzzer 文档中的「output」一节。

若运行足够长时间,最终会发现一个崩溃。输出大致如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
...
#56232	NEW    cov: 2066 corp: 110/4713b exec/s: 11246 rss: 170Mb L: 42 MS: 1 EraseBytes-
#58397	NEW    cov: 2069 corp: 111/4755b exec/s: 11679 rss: 176Mb L: 42 MS: 1 EraseBytes-
#59235	NEW    cov: 2072 corp: 112/4843b exec/s: 11847 rss: 178Mb L: 88 MS: 4 InsertByte-ChangeBit-CopyPart-CopyPart-
#60882	NEW    cov: 2075 corp: 113/4953b exec/s: 12176 rss: 183Mb L: 110 MS: 1 InsertRepeatedBytes-
thread '<unnamed>' panicked at 'index out of bounds: the len is 1 but the index is 1', src/host.rs:105
note: Run with `RUST_BACKTRACE=1` for a backtrace.
==70997== ERROR: libFuzzer: deadly signal
    #0 0x1097c5500 in __sanitizer_print_stack_trace (libclang_rt.asan_osx_dynamic.dylib:x86_64+0x62500)
    #1 0x108383d1b in fuzzer::Fuzzer::CrashCallback() (fuzzer_script_1:x86_64+0x10002fd1b)
    #2 0x108383ccd in fuzzer::Fuzzer::StaticCrashSignalCallback() (fuzzer_script_1:x86_64+0x10002fccd)
    #3 0x1083d19c7 in fuzzer::CrashHandler(int, __siginfo*, void*) (fuzzer_script_1:x86_64+0x10007d9c7)
    ...
    #33 0x10838b393 in fuzzer::Fuzzer::Loop() (fuzzer_script_1:x86_64+0x100037393)
    #34 0x1083650ec in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (fuzzer_script_1:x86_64+0x1000110ec)
    #35 0x108396c3f in main (fuzzer_script_1:x86_64+0x100042c3f)
    #36 0x7fff91552234 in start (libdyld.dylib:x86_64+0x5234)

NOTE: libFuzzer has rudimentary signal handlers.
      Combine libFuzzer with AddressSanitizer or similar for better crash reports.
SUMMARY: libFuzzer: deadly signal
MS: 2 InsertByte-EraseBytes-; base unit: 3c4fc9770beb5a732d1b78f38cc8b62b20cb997c
0x68,0x74,0x74,0x70,0x3a,0x2f,0x2f,0x5b,0x3a,0x5d,0x3a,0x78,0xc5,0xa4,0x1,0x3a,0x7f,0x1,0x59,0xc5,0xa4,0xd,0x78,0x78,0x3a,0x78,0x69,0x3a,0x0,0x69,0x3a,0x5c,0xd,0x7e,0x78,0x40,0x0,0x25,0xa,0x0,0x29,0x20,
http://[:]:x\xc5\xa4\x01:\x7f\x01Y\xc5\xa4\x0dxx:xi:\x00i:\\\x0d~x@\x00%\x0a\x00)
artifact_prefix='/private/tmp/rust-url/fuzz/artifacts/fuzzer_script_1/'; Test unit written to /home/user/rust-url/fuzz/artifacts/fuzzer_script_1/crash-e9b1b5183e46a288c25a2a073262cdf35408f697
Base64: aHR0cDovL1s6XTp4xaQBOn8BWcWkDXh4OnhpOgBpOlwNfnhAACUKACkg

输出中以 http 开头的那一行是导致 rust-url panic 的输入。

最后修改 August 23, 2026: 更新 (499855b16)