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><span>立即学习</span>“<a href="https://pan.quark.cn/s/6e7abc4abb9f" style="text-decoration: underline !important; color: blue; font-weight: bolder;" rel="nofollow" target="_blank">C++免费学习笔记(深入)</a>”;</p><div class="aritcle_card flexRow">
<div class="artcardd flexRow">
<a class="aritcle_card_img" href="/xiazai/gongju/2823" title="C++14"><img
src="https://img.php.cn/upload/manual/001/431/639/6ac8b33c327c4749.png" alt="C++14" ></a>
<div class="aritcle_card_info flexColumn">
<a href="/xiazai/gongju/2823" title="C++14">C++14</a>
<p>C++14 对 C++11 的修正与增强版本,适合旧系统维护和较老工具链兼容。</p>
</div>
<a href="/xiazai/gongju/2823" title="C++14" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a>
</div>
</div><p>// 示例:50% 概率插入一个合法 JSON key("k":)
if (((FuzzerMutateData*)seed)->Rand() % 2 == 0 && size + 4 <= max_size) {
memmove(data + 4, data, size);
data[0] = '"'; data[1] = 'k'; data[2] = '"'; data[3] = ':';
return size + 4;
}</p><p>// 示例:20% 概率把某个数字改成 "9223372036854775807"(int64_max)
if (((FuzzerMutateData<em>)seed)->Rand() % 5 == 0) {
for (size_t i = 0; i + 10 < size; ++i) {
if (data[i] >= '0' && data[i] <= '9') {
const char</em> maxint = "9223372036854775807";
size_t len = strlen(maxint);
if (i + len <= max_size) {
memcpy(data + i, maxint, len);
return std::min(size - (i+10) + len, max_size);
}
}
}
}</p><p>return size; // 默认不修改
}</p>注意:上面示例中的 strlen 和 memcpy 是允许的(async-signal-safe),但 std::min 必须替换成 C 风格三元表达式或手动比较,因为 <algorithm></algorithm> 可能触发 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 替代手写循环——边界更可控。


















