1.4 结构感知模糊测试

结构化输入与自定义变异器

译文 · 基于 Rust Fuzz Book

结构感知模糊测试

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

并非每个被测系统都希望接受原始字节缓冲区作为输入;它可能希望得到某种结构化数据的良构实例。例如,模糊测试目标可能测试的系统及其期望的输入如下:

被测系统结构化输入
颜色转换库RGB 颜色
编译器的类型检查器语法合法的源程序
编译器的优化器语义合法的源程序
分配器一系列 malloc、realloc 和 free 命令
HashMap 实现一系列 insert、get 和 remove 命令

结构感知模糊测试是指生成结构化类型的模糊输入,而非原始伪随机字节。这有助于模糊器避开会在早期被拒绝的无趣「浅层」输入(例如模糊测试编译器时的语法非法源文本),并聚焦于深入被测系统的有趣输入(例如成功解析并通过类型检查、进而练习编译器中端优化器与后端代码生成器的程序)。

libfuzzer-sys crate 提供两种结构感知模糊测试方法:

  1. 通过 fuzz_mutator! 宏 进行结构感知变异
  2. 通过 Arbitrary trait 进行结构感知生成

两种方法并不互斥,但若只实现一种,一项实验表明,基于 fuzz_mutator! 的变异随时间提供的覆盖率优于基于 arbitrary 的生成。

结构感知变异

fuzz_mutator! 宏 允许我们定义自定义变异器。模糊器将用它从语料库中的现有输入创建新输入。

编写自定义变异器时,mutatis crate 通常很有帮助。

本节包含两个基于变异的结构感知模糊测试示例:

  1. 模糊测试压缩数据

  2. 模糊测试数据结构实现

示例:压缩数据

考虑一个简单的模糊测试目标:以压缩数据为输入,解压后断言解压数据不以 “boom” 开头。libFuzzer(或任何其他模糊器)很难使该目标崩溃,因为其几乎所有变异都会破坏压缩格式。因此,我们使用自定义变异器:解压原始输入、变异解压后的数据,再重新压缩。这使 libFuzzer 能快速发现导致崩溃的输入。

 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#![no_main]

use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use libfuzzer_sys::{fuzz_mutator, fuzz_target};
use std::io::{Read, Write};

fuzz_target!(|data: &[u8]| {
    // 解压输入数据;若以 "boom" 开头则崩溃。
    if let Some(data) = decompress(data) {
        if data.starts_with(b"boom") {
            panic!("uh oh!");
        }
    }
});

fuzz_mutator!(
    |data: &mut [u8], size: usize, max_size: usize, _seed: u32| {
        // 解压输入数据。若失败则使用占位值。
        let mut decompressed = decompress(&data[..size]).unwrap_or_else(|| b"hi".to_vec());

        // 使用 `libFuzzer` 的默认变异器变异解压后的数据。通过 `resize`
        // 使 `decompressed` vec 的额外容量可用于插入类变异。
        let len = decompressed.len();
        let cap = decompressed.capacity();
        decompressed.resize(cap, 0);
        let new_decompressed_size = libfuzzer_sys::fuzzer_mutate(&mut decompressed, len, cap);

        // 重新压缩变异后的数据。
        let compressed = compress(&decompressed[..new_decompressed_size]);

        // 将重新压缩后的变异数据复制到 `data` 并返回新大小。
        let new_size = std::cmp::min(max_size, compressed.len());
        data[..new_size].copy_from_slice(&compressed[..new_size]);
        new_size
    }
);

fn decompress(compressed_data: &[u8]) -> Option<Vec<u8>> {
    let mut decoder = GzDecoder::new(compressed_data);
    let mut decompressed = Vec::new();
    if decoder.read_to_end(&mut decompressed).is_ok() {
        Some(decompressed)
    } else {
        None
    }
}

fn compress(data: &[u8]) -> Vec<u8> {
    let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
    encoder
        .write_all(data)
        .expect("writing into a vec is infallible");
    encoder.finish().expect("writing into a vec is infallible")
}

示例:模糊测试数据结构

假设我们正在实现持久、不可变的 map 数据结构。可将我们的 map 与已知正确的 map 实现(例如 std::collections::HashMap<K, V>)的结果进行比较,以测试正确性。这种技术称为差分模糊测试。

添加 mutatis 依赖

我们将使用 mutatis crate,它定义了编写自定义变异器的抽象与组合子,因此需要添加依赖:

1
2
3
4
# fuzz/Cargo.toml

[dependencies]
mutatis = { version = "0.5", features = ["derive"] }

定义 MapMethod 类型

这是一个 enum,为每个要练习的 PersistentImmutableMap<K, V> 方法提供一个变体。应 derive mutatis::Mutate 和 serde::{Serialize, Deserialize}。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// fuzz/fuzz_targets/map_differential.rs

use mutatis::Mutate;
use serde::{Serialize, Deserialize};

/// `PersistentImmutableMap<K, V>` 方法调用命令。
#[derive(Debug, Mutate, Serialize, Deserialize)]
enum MapMethod<K, V> {
    /// 向 map 插入新条目。
    Insert { key: K, value: V },
    /// 从 map 获取该键的值。
    Get { key: K },
    /// 从 map 移除该键的条目。
    Remove { key: K },
}

定义 fuzz_mutator!

使用 serde_json 反序列化输入类型、变异后再序列化。(也可使用 bincode 或 postcard 等替代 serde_json。)

 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
27
28
29
// fuzz/fuzz_targets/map_differential.rs

// ...

libfuzzer_sys::fuzz_mutator!(|data: &mut [u8], size: usize, max_size: usize, seed: u32| {
    // 尝试反序列化 `Vec<MapMethod<String, u32>>`;失败时回退为空 vec。
    let mut methods: Vec<MapMethods<String, u32>> = serde_json::from_slice(&data[..size])
        .ok()
        .unwrap_or_default();

    // 创建新的变异会话。
    let mut session = mutatis::Session::new()
        // 使用给定的变异种子。
        .seed(seed.into())
        // 仅当最大结果大小小于输入大小时执行收缩类变异。
        .shrink(max_size < size);

    // 变异 methods。
    if session.mutate(&mut methods).is_ok() {
        // 将 methods 序列化回 JSON。
        let mut data = &mut data[..max_size];
        if serde_json::to_writer(&mut data, &methods).is_ok() {
            return data.len();
        }
    }

    // 回退到默认 libfuzzer 变异器
    fuzzer_mutate(data, size, max_size)
});

定义模糊测试目标

模糊测试目标反序列化 map 方法,创建我们的数据结构实例与用于对比的实现,然后在两个实例上调用这些方法并断言结果相同。

 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
27
28
29
30
31
32
33
34
35
36
37
38
39
// fuzz/fuzz_targets/map_differential.rs

// ...

libfuzzer_sys::fuzz_target!(|data| {
    // 反序列化 map 方法,否则忽略该输入。
    let Ok(methods) = serde_json::from_slice::<Vec<MapMethods<String, u32>>>(data) else {
        return libfuzzer_sys::Corpus::Reject;
    };

    // 创建我们的持久不可变 map 类型实例,以及 `std` 的 hash map 实例。
    let mut ours = PersistentImmutableMap::new();
    let mut theirs = std::collections::HashMap::new();

    // 解释 `MapMethod` 命令,并断言我们的 map 与 `std` map 得到相同结果。
    for method in methods {
        match method {
            MapMethod::Insert { key, value } => {
                let their_old_entry = theirs.insert(key.clone(), value);
                let (new_ours, our_old_entry) = ours.insert(key, value);
                assert_eq!(their_old_entry, our_old_entry);
                ours = new_ours;
            }
            MapMethod::Get { key } => {
                let their_value = theirs.get(&key);
                let our_value = ours.get(&key);
                assert_eq!(their_value, our_value);
            }
            MapMethod::Remove { key } => {
                let their_old_entry = theirs.remove(&key);
                let (new_ours, our_old_entry) = ours.remove(&key);
                assert_eq!(their_old_entry, our_old_entry);
                ours = new_ours;
            }
        }
    }

    libfuzzer_sys::Corpus::Keep
});

结构感知生成

fuzz_target! 宏允许我们定义接受任意输入类型的模糊测试目标,而不仅是 &[u8],只要输入类型实现了 Arbitrary trait。

1
2
3
libfuzzer_sys::fuzz_target!(|input: AnyTypeThatImplementsArbitrary| {
    // 在此使用 `input`...
})

arbitrary crate 为 std 中几乎所有类型实现了 Arbitrary, 包括 Vec、HashMap 等集合,以及 String、PathBuf 等类型。

为方便起见,libfuzzer-sys crate 将 arbitrary crate 重新导出为 libfuzzer_sys::arbitrary。可通过以下方式启用 #[derive(Arbitrary)]:

  • 启用 arbitary crate 的 "derive" 特性,或
  • (等价地)启用 libfuzzer-sys crate 的 "arbitrary-derive" 特性。

更多细节请参阅 arbitrary crate 文档。

本节包含两个基于生成的结构感知模糊测试示例:

  1. 模糊测试颜色转换

  2. 模糊测试分配器 API 调用

示例:模糊测试颜色转换

假设我们正在开发可将 RGB 颜色转换为 HSL 再转回的颜色转换库。

启用 Derive Arbitrary

我们不想手写 Arbitrary 实现,因此希望启用 arbitrary crate 的 "derive" Cargo 特性。这样可用 #[derive(Arbitrary)] 自动获得 Arbitrary 实现。

由于我们将为 Rgb 类型 derive Arbitrary,而该类型位于主颜色转换 crate 中,因此将其添加到主 Cargo.toml。

1
2
3
4
# Cargo.toml

[dependencies]
arbitrary = { version = "1", optional = true, features = ["derive"] }

为我们的 Rgb 类型 Derive Arbitrary

在主 crate 中,当启用 "arbitrary" Cargo 特性时,我们 derive Arbitrary trait:

1
2
3
4
5
6
7
8
9
// src/lib.rs

#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Rgb {
    pub r: u8,
    pub g: u8,
    pub b: u8,
}

为模糊测试目标启用主项目的 "arbitrary" Cargo 特性

由于我们在主颜色转换 crate 中将 arbitrary 设为可选依赖,需要为模糊测试目标启用该特性。

1
2
3
4
# fuzz/Cargo.toml

[dependencies]
my_color_conversion_library = { path = "..", features = ["arbitrary"] }

添加模糊测试目标

需要向项目添加新的模糊测试目标:

1
$ cargo fuzz add rgb_to_hsl_and_back

实现模糊测试目标

最后,可实现接受任意 RGB 颜色的模糊测试目标:转换为 HSL,再转回 RGB 并断言与原始颜色相同!由于我们为 Rgb 类型实现了 Arbitrary,模糊测试目标可直接接受 Rgb 实例:

1
2
3
4
5
6
7
8
9
// fuzz/fuzz_targets/rgb_to_hsl_and_back.rs

libfuzzer_sys::fuzz_target!(|color: Rgb| {
    let hsl = color.to_hsl();
    let rgb = hsl.to_rgb();

    // 对所有 RGB -> HSL -> RGB 转换,这应为真!
    assert_eq!(color, rgb);
});

示例:模糊测试分配器 API 调用

例如,假设我们在模糊测试自己的 malloc 和 free 实现。我们希望构造一系列合法的分配与释放 API 调用。此外,希望该序列由模糊器引导,以便利用其对代码覆盖率的洞察,在模糊测试期间最大化我们练习的代码量。

添加模糊测试目标

首先,向项目添加新的模糊测试目标:

1
$ cargo fuzz add fuzz_malloc_free

启用 Derive Arbitrary

与上述颜色转换示例类似,我们不想手写 Arbitrary 实现,而是希望 derive 它。

1
2
3
4
# fuzz/Cargo.toml

[dependencies]
libfuzzer-sys = { version = "0.4.0", features = ["arbitrary-derive"] }

定义 AllocatorMethod 类型并 Derive Arbitrary

接下来,定义表示 malloc、realloc 或 free 的 enum:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// fuzz_targets/fuzz_malloc_free.rs

use libfuzzer_sys::arbitrary::Arbitrary;

#[derive(Arbitrary, Debug)]
enum AllocatorMethod {
    Malloc {
        // 要分配的内存大小。
        size: usize,
    },
    Free {
        // 释放我们已进行的第 index 次分配。
        index: usize
    },
    Realloc {
        // 我们将对第 index 次分配进行 realloc。
        index: usize,
        // 分配的新大小。
        new_size: usize,
    },
}

编写接受 AllocatorMethod 序列的模糊测试目标

最后,编写接受 AllocatorMethod 向量的模糊测试目标,通过执行相应的 malloc、realloc 和 free 调用来解释它们。这可行是因为当 T 实现 Arbitrary 时,Vec<T> 也实现 Arbitrary。

 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// fuzz/fuzz_targets/fuzz_malloc_free.rs

libfuzzer_sys::fuzz_target!(|methods: Vec<AllocatorMethod>| {
    let mut allocs = vec![];

    // 解释模糊器提供的方法并执行相应的分配器 API 调用。
    for method in methods {
        match method {
            AllocatorMethod::Malloc { size } => {
                let ptr = my_allocator::malloc(size);
                allocs.push(ptr);
            }
            AllocatorMethod::Free { index } => {
                match allocs.get(index) {
                    Some(ptr) if !ptr.is_null() => {
                        my_allocator::free(ptr);
                        allocs[index] = std::ptr::null();
                    }
                    _ => {}
                }
            }
            AllocatorMethod::Realloc { index, size } => {
                match allocs.get(index) {
                    Some(ptr) if !ptr.is_null() => {
                        let new_ptr = my_allocator::realloc(ptr, size);
                        allocs[index] = new_ptr;
                    }
                    _ => {}
                }
            }
        }
    }

    // 释放所有剩余分配。
    for ptr in allocs {
        if !ptr.is_null() => {
            my_allocator::free(ptr);
        }
    }
});
最后修改 August 23, 2026: 更新 (499855b16)