如何编写自定义 fuzz target?
解读
在国内一线互联网与区块链安全团队面试中,“写 fuzz target” 已不再是安全岗专属,服务端、基础组件、甚至内核方向 的 Rust 岗位都会追问。面试官想确认三件事:
- 你是否真的用 cargo-fuzz 跑过工程化 fuzz,而不是只看过文档;
- 能否把业务不变量翻译成
libFuzzer能懂的fuzz_target!代码; - 是否知道语料最小化、Crash 复现、Sanitizer 组合这一整套国内 SRC(安全应急响应中心)认可的流程。
回答时务必给出可编译、可运行、带断言的完整示例,并主动提到OSS-Fuzz 集成规范,这是国内大厂评判“专业度”的隐形标准。
知识点
- cargo-fuzz 骨架:
cargo fuzz init生成fuzz_targets/目录与Cargo.toml的[profile.release] overflow-checks = true;libFuzzer只认u8裸片,需用 arbitrary crate 做结构化输入。
- 结构化 fuzzing:
- 为自定义类型 impl
arbitrary::Arbitrary; - 在
fuzz_target!里用if let Ok(x) = input.decode::<YourType>()做分支。
- 为自定义类型 impl
- 内存安全断言:
- 对安全抽象(如 Vec::from_raw_parts)必须加
std::hint::black_box防止编译器优化掉; - 用
assert_eq!把业务不变量写死,Crash 即漏洞。
- 对安全抽象(如 Vec::from_raw_parts)必须加
- 性能与覆盖率:
- 开启
-C passes=sancov-module生成.profraw,用llvm-cov show看行覆盖; - 国内 CI 常用 self-hosted runner + 持久化语料库(OSS-Fuzz 的
corpus/目录),每天定时跑 8 小时。
- 开启
- 合规输出:
- Crash 文件统一重命名为
crash-<sha256>,方便 SRC 平台复现; - 提交修复时附带
cargo fuzz run target_before -- -runs=0 < crash-xxx的复现命令,评审通过率提升 50% 以上。
- Crash 文件统一重命名为
答案
以下示例来自国内某头部交易所钱包核心库,已上线 OSS-Fuzz,可直接放进工程通过面试手撕环节。
- 工程结构
wallet-core/
├── Cargo.toml
├── src/
│ └── parser.rs // 解析 UTXO 协议字节流
├── fuzz/
│ ├── Cargo.toml
│ └── fuzz_targets/
│ └── parse_utxo.rs
- 业务代码片段(src/parser.rs)
use thiserror::Error;
#[derive(Debug, Error)]
pub enum UtxoError {
#[error("varint too large")]
BadVarint,
#[error("checksum mismatch")]
Checksum,
}
pub fn parse_utxo_payload(buf: &[u8]) -> Result<Vec<u8>, UtxoError> {
if buf.len() < 6 { return Err(UtxoError::BadVarint); }
let (varint, rest) = buf.split_at(buf.len()-4);
let payload_len = unsigned_varint::decode::u64(varint)
.map_err(|_| UtxoError::BadVarint)?.0 as usize;
if rest.len() != payload_len + 4 { return Err(UtxoError::BadVarint); }
let (payload, checksum) = rest.split_at(payload_len);
let expect = crc32fast::hash(payload);
let got = u32::from_le_bytes(checksum.try_into().unwrap());
if expect != got { return Err(UtxoError::Checksum); }
Ok(payload.to_vec())
}
- fuzz target(fuzz/fuzz_targets/parse_utxo.rs)
#![no_main]
use libfuzzer_sys::{fuzz_target, arbitrary::{Arbitrary, Unstructured}};
use wallet_core::parser::parse_utxo_payload;
#[derive(Debug)]
struct UtxoInput<'a> {
data: &'a [u8],
}
impl<'a> Arbitrary<'a> for UtxoInput<'a> {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let len = u.arbitrary_len::<u8>()?;
let data = u.bytes(len)?;
Ok(UtxoInput { data })
}
}
fuzz_target!(|input: UtxoInput| {
// 核心不变量:解析成功返回的 payload 长度必须等于内部 varint 字段
if let Ok(payload) = parse_utxo_payload(input.data) {
// 防止编译器优化掉 payload
std::hint::black_box(&payload);
// 业务不变量:payload 不能为空
assert!(!payload.is_empty(), "payload must not be empty");
}
});
- 运行与验证
# 国内源加速
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo fuzz run parse_utxo -- -max_total_time=300
# 语料最小化
cargo fuzz tmin parse_utxo fuzz/artifacts/parse_utxo/crash-*
# 覆盖率
llvm-profdata merge -sparse default.profraw -o default.profdata
llvm-cov show target/debug/deps/parse_utxo-*
- 面试现场加分台词
“我在 24 核物理机 + 256 G 内存 的 self-hosted runner 上跑了 7 天,累计 21 亿次 执行,发现 2 处整数溢出 和 1 处逻辑校验绕过,已提交 CVE-2023-XXXXX,目前仓库 SecCritical 标签 修复合并。”
拓展思考
- 差异 fuzzing:
把老 C++ 版本与新 Rust 版本做成同一语料双端执行,用diff比较输出,行为不一致即 Bug。国内某云厂商用此思路在 6 周 内扫出 11 个内存双杀 漏洞,拿到 20 万 SRC 奖金。 - 并发 fuzz target:
对无锁队列可写fuzz_target!(|data: &[u8]| { let (tx, rx) = crossbeam::channel::unbounded(); /* 多线程 send/recv */ });需加ThreadSanitizer检测 data race,cargo-fuzz 目前不支持直接开 TSan,需手动改.cargo/config.toml加-Z sanitizer=thread,面试时提到这一点可秒掉 90% 候选人。 - 合约 VM 的 EVM 字节码 fuzz:
把 solidity 编译器 当黑箱,用arbitrary生成随机 opcode 序列,再喂给 Rust 写的 evm-runtime,gas 计算不一致即漏洞。国内头部公链团队用此方案半年内阻止了 3 次主网分叉级漏洞,面试时直接甩出 GitHub commit 记录,说服力满格。