Caloogy 文章
English

模糊测试:让机器替你找崩溃

日期
2026 年 6 月 9 日
系列
方法论
阅读
4 分钟
主题
TestingFuzzingAflLibfuzzer

一句话版本

人写测试用例,只会想到自己能想到的情况。模糊测试(fuzzing)让机器自动生成海量的、奇奇怪怪的输入喂给程序,看它会不会崩溃、卡死、或者违反某个规则。 现代模糊测试是覆盖率引导的:它会观察每个输入让程序走了哪些代码路径,保留能走到新路径的输入并在此基础上继续变异,像一个不知疲倦、越来越聪明的测试员。谷歌的 OSS-Fuzz 已经用这种方法在开源项目里发现了上万个安全漏洞。

全文分三层:

  1. 通俗理解:让一万只猴子乱按键盘,但只奖励发现新房间的猴子
  2. 实际用处:AFL、OSS-Fuzz、Heartbleed、智能合约的不变量测试、AI 辅助
  3. 原理与工程要点:覆盖率引导、怎么写测试入口、检测器、什么样的代码适合模糊测试

第一层:通俗理解

1.1 一万只猴子,只奖励发现新房间的

想象一栋迷宫般的大楼,有些房间里藏着炸弹(bug)。你派一万只猴子进去乱跑:

这样,探索的范围会越来越深,最终走到人类测试员想不到的角落里。

1.2 它擅长找什么

类型 例子
崩溃 空指针、数组越界、除以零
内存安全问题 缓冲区溢出、释放后使用(配合检测器)
卡死 无限循环、超长耗时
违反规则 解析后再序列化结果不一致;"余额永远不为负"被打破

特别擅长测试解析不可信输入的代码:文件格式、网络协议、压缩、图片解码、正则引擎。


第二层:实际用处


第三层:原理与工程要点

3.1 覆盖率引导的循环

走到了新路径

崩溃 / 违反断言

没有新东西

语料库
(有趣的输入)

挑一个输入并变异
翻转比特、插入、拼接……

运行程序,记录覆盖率

保存为发现

编译时插入轻量的"探针",记录程序经过了哪些分支。一个输入只要触发了新的分支组合,就被视为"有趣",加入语料库。

3.2 写一个测试入口

模糊测试需要一个"入口函数":接收一段任意字节,喂给被测代码。以 libFuzzer 风格为例:

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    parse_my_format(data, size);   // 被测函数:不能崩溃,不能违反断言
    return 0;
}

好的入口:

还可以加入性质检查,比如"解析后再序列化,应该得到相同的内容"(往返测试),或者"两种实现的输出必须一致"(差分测试,见《多源交叉验证》)。

3.3 检测器(sanitizer)

很多内存错误不会立即导致崩溃,而是悄悄破坏数据。编译时开启检测器,能让这些问题在发生的那一刻就被抓住:

检测器 找什么
AddressSanitizer 越界读写、释放后使用
UndefinedBehaviorSanitizer 整数溢出、非法移位等未定义行为
MemorySanitizer 使用未初始化的内存

3.4 种子语料

从一些合法的样本输入开始(比如几个正常的图片文件、几条正常的协议消息),比从空白开始效率高得多。语料库要持续保存,作为回归测试的一部分。

3.5 和其他测试的关系

测试 回答的问题
单元测试 我想到的情况对不对?
模糊测试 我没想到的情况会不会出事?
基于性质的测试 某条规则在大量随机输入下是否都成立?(模糊测试的近亲)
形式化验证 某条规则在所有输入下是否都成立?

模糊测试最适合持续运行:接入 CI,每次代码变更都跑一段时间,或者像 OSS-Fuzz 那样全天候运行。


总结

层次 要点
直觉 海量随机输入,只奖励发现新路径的那些
擅长 解析不可信输入的代码;崩溃、内存错误、卡死、性质违反
关键 覆盖率引导 + 检测器 + 好的种子语料
实践 写快而确定的入口;加入往返和差分检查;持续运行