LLVMFuzzerCustomMutator 是可选的语义感知变异接口,用于在默认变异无效时介入;需严格遵守无堆分配、无异常、仅用 async-signal-safe 函数、通过 Rand() 获取随机数、输出长度≤max_size 等约束。

libFuzzer 的 LLVMFuzzerCustomMutator 是什么角色
它不是必须实现的接口,而是当你发现默认变异(bit flips、byte swaps、dictionary insertion 等)对目标代码无效时,才需要介入的“最后手段”。libFuzzer 在每次调用 LLVMFuzzerCustomMutator 时,会传入原始输入缓冲区、长度、最大允许长度和一个随机数生成器句柄;你返回新长度,libFuzzer 负责后续执行与崩溃判定。
关键点:这个函数不能分配堆内存(libFuzzer 不管理你的 malloc/free)、不能抛异常、不能调用非 async-signal-safe 函数(如 printf、malloc、std::string::append),否则会导致进程被 kill 或行为不可控。
- 必须用
uint8_t*直接操作内存,避免std::vector或std::string - 所有随机数必须通过传入的
mutate_data->Rand()获取,不能用rand()或std::random_device - 输出长度不能超过
max_size,否则 libFuzzer 会截断并静默失败
如何编写一个安全可用的自定义 mutator 示例
假设你在 fuzz 一个解析 JSON 字符串的函数,发现默认变异很难生成合法但边缘的 JSON(比如嵌套过深、键名含控制字符、数值溢出)。你可以写一个轻量级 mutator,在已有输入上做语义感知调整:
extern "C" size_t LLVMFuzzerCustomMutator(uint8_t *data, size_t size, size_t max_size, unsigned int seed) {
// 使用 seed 初始化本地 rng(仅用于确定性调试,实际应只用 mutate_data->Rand())
// 但注意:libFuzzer 不传 seed 给你 —— 这里只是示意;真实代码中删掉这行,直接用 Rand()
auto &mutate_data = *reinterpret_cast<fuzzermutatedata>(seed); // ❌ 错误!libFuzzer 不传 FuzzerMutateData*
<p>// ✅ 正确方式:libFuzzer v14+ 传的是 FuzzerMutateData<em>,但你不能依赖其内部布局
// 实际应这样(参考 llvm-project/llvm/lib/Fuzzer/FuzzerInterface.h):
// struct FuzzerMutateData { uint32_t (</em>Rand)(); };
// 所以你只能调用 ((FuzzerMutateData*)seed)->Rand()</p>
<p>if (size == 0 || max_size == 0) return size;</p>
<p>// 示例:50% 概率插入一个合法 JSON key("k":)
if (((FuzzerMutateData*)seed)->Rand() % 2 == 0 && size + 4 </p>
<p>// 示例:20% 概率把某个数字改成 "9223372036854775807"(int64_max)
if (((FuzzerMutateData<em>)seed)->Rand() % 5 == 0) {
for (size_t i = 0; i + 10 = '0' && data[i] maxint = "9223372036854775807";
size_t len = strlen(maxint);
if (i + len <p>return size; // 默认不修改
}</p></em></p></fuzzermutatedata>
注意:上面示例中的 strlen 和 memcpy 是允许的(async-signal-safe),但 std::min 必须替换成 C 风格三元表达式或手动比较,因为 可能触发 STL 内部锁或异常。
为什么 LLVMFuzzerCustomCrossOver 往往比自定义 mutator 更值得优先尝试
交叉变异(crossover)复用已有语料中的子结构,天然更贴近语法/协议边界。对 parser、decoder、config loader 类目标,它常比手工写 mutator 更快产出有效输入。
-
LLVMFuzzerCustomCrossOver接收两个输入指针和长度,返回新长度;同样禁止堆分配和非 async-signal-safe 调用 - 简单策略:随机选一个分割点,拼接
data1[0..pos]+data2[pos..],再做一次小范围翻转 - libFuzzer 默认已启用 crossover,但只有在 corpus 达到一定规模(默认 1000 个输入)后才高频触发 —— 如果你刚启动 fuzz,先等它跑一阵再评估是否需自定义
调试自定义 mutator 时最常踩的三个坑
libFuzzer 不会告诉你 mutator 崩溃的具体原因,只会打印 ERROR: libFuzzer: deadly signal 或静默跳过变异 —— 这让你很难定位问题。
- 误用
std::string或std::vector:它们的构造/析构可能调用malloc或抛std::bad_alloc,触发信号终止 - 越界读写:比如用
data[size]当作结束符判断,但size可能为 0;或 memcpy 长度算错导致覆盖max_size边界 - 忽略
max_size限制:返回值 >max_size时 libFuzzer 会截断,但你的逻辑可能已破坏数据一致性(比如插入字符串后没更新后续偏移),导致后续解析崩溃而非你的 mutator 崩溃
建议在 mutator 开头加一句:if (size > max_size) return size;,并始终用 memmove/memcpy 替代手写循环——边界更可控。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











