- 区块链
- Web3
【免费下载链接】grin
Minimal implementation of the Mimblewimble protocol.
本文以 Grin(Mimblewimble 协议的极简实现)仓库中的 core/fuzz 模块 为核心,系统讲解如何借助 Rust nightly 工具链与cargo-fuzz(libFuzzer)对区块链核心数据结构的二进制序列化/反序列化逻辑进行模糊测试。文中覆盖环境搭建、语料(corpus)生成、6 个 fuzz 目标的运行方式,并结合 core/src/ser.rs、core/src/global.rs 等源码剖析其底层原理,读完你将能够在本仓库内独立搭建、运行并扩展针对Transaction、Block、CompactBlock的反序列化 fuzz 测试。
一、为什么需要对 Grin 的核心数据结构做 Fuzz
区块链节点天然运行在"不可信网络"环境中:来自任意对等节点的区块、交易、紧凑区块数据,都会首先经过二进制反序列化(deserialization)流程进入内存。Grin 的序列化层是自定义实现(见 core/src/ser.rs 顶部注释:"Serialization and deserialization layer specialized for binary encoding"),并非直接复用现成框架,因此任何解析逻辑缺陷——例如越界读取、栈溢出、panic、非预期内存分配——都可能被恶意构造的网络报文触发,进而导致节点崩溃甚至被攻击。
正因如此,仓库专门维护了core/fuzz目录,将Transaction、Block、CompactBlock三种核心类型的反序列化入口暴露给 libFuzzer,用"无限次随机输入 + 覆盖率引导"的方式持续寻找解析层缺陷。这与 core/src/core/block.rs 中为"不可信通道"专门设计的UntrustedBlockHeader/UntrustedBlock类型形成呼应:源码注释明确写道"it supposed to be used when we can't trust the channel (eg network)"——fuzz 测试正是验证这些入口安全性的第一道防线。
二、环境准备:安装 cargo-fuzz(需要 Rust nightly)
原文档明确指出,运行本模块 fuzz 测试必须使用 Rust nightly 工具链,且依赖cargo-fuzz(其底层即 LLVM 的 libFuzzer 与libfuzzer-syscrate)。安装命令如下:
cargo install cargo-fuzz几点实操提示:
- 若当前默认工具链为 stable,请先通过
rustup toolchain install nightly安装 nightly,并在core/fuzz目录下用cargo +nightly fuzz形式调用(或在rust-toolchain文件中固定 nightly 版本)。 cargo-fuzz会链接到本机可用的clang/ LLVM 运行时,请确保系统装有较新版本的 clang(Linux 下通常可通过apt install clang或llvm获得)。- 仓库中 core/fuzz/Cargo.toml 显示 fuzz 工程依赖
libfuzzer-sys = "0.4.0"、grin_core(路径..)与grin_keychain(路径../../keychain),其中grin_core是序列化逻辑的实际载体,grin_keychain提供盲因子、密钥标识等反序列化所需的 keychain 类型。 - 该 crate 的
publish = false,且通过[package.metadata] cargo-fuzz = true标记为 fuzz 工程,正常发布流程不会携带它。
三、项目结构全景:6 个 Fuzz 目标 + 1 个语料生成器
core/fuzz目录结构如下:
core/fuzz/ ├── Cargo.toml # 声明所有 bin 目标与依赖 ├── README.md # 官方使用说明(本文主体) ├── src/ │ └── main.rs # gen-corpus 可执行文件:生成种子语料 └── fuzz_targets/ ├── transaction_read_v1.rs ├── transaction_read_v2.rs ├── block_read_v1.rs ├── block_read_v2.rs ├── compact_block_read_v1.rs └── compact_block_read_v2.rs从 core/fuzz/Cargo.toml 可以看到,工程以[[bin]]方式声明了 7 个可执行目标,其中 6 个为 fuzz 目标(对应 6 个fuzz_targets/*.rs文件),1 个为语料生成器gen-corpus(对应src/main.rs)。目标命名规律清晰:
| Fuzz 目标 | 反序列化类型 | 协议版本 |
|---|---|---|
transaction_read_v1 | Transaction | v1 |
transaction_read_v2 | Transaction | v2 |
block_read_v1 | UntrustedBlock | v1 |
block_read_v2 | UntrustedBlock | v2 |
compact_block_read_v1 | UntrustedCompactBlock | v1 |
compact_block_read_v2 | UntrustedCompactBlock | v2 |
v1/v2 分别对应两代序列化协议格式(详见下文"协议版本"章节),每种类型都同时覆盖两个版本,以验证旧格式兼容路径与新格式解析路径。
查看全部目标的完整列表有两种方式:
cargo fuzz list或者直接阅读 core/fuzz/Cargo.toml 中的[[bin]]段——原文档也明确推荐了这两种途径。
四、语料生成:gen-corpus 的原理与执行
libFuzzer 具备覆盖率引导能力,即使从空语料开始也会自行生成随机输入;但随机字节命中"结构合法"的样本概率极低,因此**用真实对象的序列化结果作为种子语料(seed corpus)**可以大幅提升解析代码路径的覆盖效率。原文档指出这一步是可选(optional)的,但强烈推荐。
生成命令如下(注意需在core/fuzz目录内执行):
cd fuzz cargo run --bin gen-corpus该命令的实际实现位于 core/fuzz/src/main.rs,其核心逻辑是:
- 对 6 个 fuzz 目标逐一调用
generate(); generate使用ser::serialize将默认对象序列化后写入corpus/<target>/pattern文件;- 序列化对象与协议版本一一对应:v1 目标使用
ser::ProtocolVersion(1),v2 目标使用ser::ProtocolVersion(2); - 生成的对象来自 core/src/core/transaction.rs、core/src/core/block.rs、core/src/core/compact_block.rs 三处的
Transaction::default()、Block::default()、CompactBlock::from(Block::default())。
源码细节(core/fuzz/src/main.rs):generate函数会先检查corpus/<target>目录与pattern文件是否已存在,只有不存在时才创建并写入,避免重复生成覆盖已有种子;若目录创建失败或序列化失败,则返回ser::Error::IOErr并打印失败信息。
生成的目录结构大致为:
core/fuzz/corpus/ ├── transaction_read_v1/pattern ├── transaction_read_v2/pattern ├── block_read_v1/pattern ├── block_read_v2/pattern ├── compact_block_read_v1/pattern └── compact_block_read_v2/patterncorpus目录默认位于core/fuzz/下(即运行gen-corpus时的当前工作目录)。libFuzzer 在运行时会把语料目录中的每个文件当作一个初始输入,并在运行过程中持续把"发现新覆盖"的输入回写进该目录。
五、运行 Fuzz 测试:命令与注意事项
Fuzz 测试本质上是一个无限循环:libFuzzer 会不断变异语料、喂给目标函数并观察崩溃与覆盖率。原文档强调:"run it for some period of time then stop if no failures are found"——即按时间段运行,若无失败即可手动停止。
运行前务必确认当前所在目录是core,否则可能得到误导性错误(misleading errors):
cargo fuzz run transaction_read cargo fuzz run block_read cargo fuzz run compact_block_read实操要点:
- 上述命令理论上应在
core/fuzz目录下执行(cargo-fuzz 要求在工作区/工程目录内运行),原文档提示"在core目录"是为了规避路径解析上的误导性报错,实际以能正确解析core/fuzz/Cargo.toml为准。 - 每个目标可配合 cargo-fuzz 的常用参数使用,例如
-- -runs=100000限制运行次数、-timeout=5设置单次执行超时、-max_len=1024限制输入长度等(这些是 libFuzzer 的通用参数,通过--透传)。 - 一旦发现崩溃(crash),libFuzzer 会把触发崩溃的输入保存为
crash-*文件并生成最小化用例,可用于回归测试与缺陷定位。
六、Fuzz 目标源码剖析:反序列化入口长什么样
6 个 fuzz 目标的结构高度一致,仅反序列化的类型与协议版本不同。以 core/fuzz/fuzz_targets/block_read_v1.rs 为例:
#![no_main] use libfuzzer_sys::fuzz_target; extern crate grin_core; use grin_core::core::UntrustedBlock; use grin_core::global; use grin_core::ser::{self, DeserializationMode}; fuzz_target!(|data: &[u8]| { global::set_local_chain_type(global::ChainTypes::AutomatedTesting); let mut d = data.clone(); let _t: Result<UntrustedBlock, ser::Error> = ser::deserialize(&mut d, ser::ProtocolVersion(1), DeserializationMode::Full); });要点逐条拆解:
#![no_main]:fuzz 目标的入口由libfuzzer_sys的fuzz_target!宏接管,普通 main 函数不参与链接。global::set_local_chain_type(global::ChainTypes::AutomatedTesting):每一轮输入都先把当前线程的链类型设为AutomatedTesting(CI 自动化测试链)。这在 core/src/global.rs 中实现,通过thread_local存储覆盖全局链类型;ChainTypes枚举包含AutomatedTesting / UserTesting / Testnet / Mainnet四种(core/src/global.rs),其中AutomatedTesting对应 CI 测试场景,反序列化过程中涉及链类型相关校验(如区块版本、难度参数)时都会读取该值。ser::deserialize(&mut d, ProtocolVersion, DeserializationMode::Full):核心反序列化调用。注意这里传入的是DeserializationMode::Full,含义是"完整反序列化以充分重建对象"(core/src/ser.rs);另一可选模式SkipPow用于区块头场景跳过 PoW 读取。对于 fuzz 而言Full模式覆盖的解析路径最全,最能暴露深层缺陷。UntrustedBlock而非Block:这是刻意为之的设计。与 core/src/core/block.rs 对照可见,UntrustedBlock在Readable::read中会先经UntrustedBlockHeader::read做轻量验证(轻量校验头部合法性)再解析 body——模拟真实网络节点接收不可信区块的完整路径。同理,compact_block_read_v1.rs使用UntrustedCompactBlock(core/src/core/compact_block.rs),交易目标则直接使用Transaction(core/src/core/transaction.rs)。- 返回
Result<_, ser::Error>被丢弃:fuzz 关注的是"反序列化过程是否 panic/崩溃",而不是返回的Err——解析失败是正常且预期的事情。libFuzzer 只对崩溃(panic、abort、sanitizer 报错)感兴趣,这也解释了为什么代码用_t忽略结果。 let mut d = data.clone():deserialize接受Read实现并会消费输入字节,因此对 libFuzzer 提供的&[u8]做一次克隆,避免所有权与借用问题。
其余 5 个目标仅在两处不同:反序列化类型(Transaction/UntrustedBlock/UntrustedCompactBlock)与协议版本(ProtocolVersion(1)或ProtocolVersion(2)),逻辑完全同构。
七、协议版本(ProtocolVersion)与序列化层的底层支撑
fuzz 目标中反复出现的ser::ProtocolVersion(1)与ser::ProtocolVersion(2),其定义在 core/src/ser.rs:
ProtocolVersion(pub u32)是一个 u32 包装类型,同时用于p2p 层(节点间消息序列化)与本地 DB / MMR 存储层(区块与交易数据的持久化格式)。ProtocolVersion::local()返回节点当前默认的 p2p 协议版本,其值在 core/src/global.rs 中定义为PROTOCOL_VERSION = ProtocolVersion(1_000),节点通过 Hand/Shake 与对端协商兼容版本。ProtocolVersion::local_db()返回ProtocolVersion(1),是本地数据库使用的序列化版本——这与 fuzz 目标中的v1对应;v2则代表随版本演进产生的第二代序列化格式。为两代格式分别建立 fuzz 目标,可以确保旧数据(历史区块、旧 DB 文件)与新材料都能被安全解析。
序列化/反序列化的两个核心函数同样位于 core/src/ser.rs:
pub fn deserialize<T: Readable, R: Read>( source: &mut R, version: ProtocolVersion, mode: DeserializationMode, ) -> Result<T, Error> pub fn serialize<W: Writeable>( sink: &mut dyn Write, version: ProtocolVersion, thing: &W, ) -> Result<(), Error>gen-corpus走serialize路径生成种子,fuzz 目标走deserialize路径消费输入,二者通过version参数严格对齐。此外,反序列化层内置了读取大小保护:ser.rs中定义MAX_READ_SIZE: usize = 100_000(core/src/ser.rs),超大读取请求会返回Error::TooLargeReadErr——这是 fuzz 测试同样需要覆盖的防御路径之一。
值得一提的错误类型(core/src/ser.rs):Error枚举涵盖了IOErr、UnexpectedData、CorruptedData、CountError、TooLargeReadErr、SortError、DuplicateError、InvalidBlockVersion、UnsupportedProtocolVersion等,fuzz 过程中产生的任何Err都意味着解析器对该输入给出了明确拒绝——这与 panic 有着本质区别。
八、如何解读运行结果与扩展新的 Fuzz 目标
运行结果的正确解读
cargo-fuzz 运行时终端会持续输出执行统计(总执行数、覆盖率、每秒执行数等)。需要关注的信号:
NEW:libFuzzer 发现了新的代码覆盖,输入被保存进 corpus——说明语料在有效增长,这是"运行良好"的标志;REDUCE:正在精简触发新覆盖的输入;ERROR/SUMMARY:检测到崩溃,libFuzzer 会打印触发输入的文件路径(crash-*文件)与栈回溯,可用cargo fuzz run <target> crash-<hash>复现。
按原文档建议,fuzz 是无限测试,无失败时手动停止(Ctrl+C)即可;发现的 crash 文件应保留并用于缺陷修复后的回归验证。
扩展新目标的步骤
若需为新的核心数据结构(例如某个Readable类型)增加 fuzz 覆盖,参照现有模式即可:
- 在 core/fuzz/fuzz_targets/ 下新建
xxx_read_v1.rs(复制现有模板,替换类型与协议版本); - 在 core/fuzz/Cargo.toml 的
[[bin]]段声明同名 bin 目标; - 在 core/fuzz/src/main.rs 的
main()中追加一行generate(...)以生成种子语料; - 执行
cargo fuzz run xxx_read开始测试。
顺带一提,仓库在 p2p/fuzz 目录维护了面向 P2P 消息编解码的同类 fuzz 工程(read_hand、read_msg_header、read_tx_hashset_archive等目标),与core/fuzz形成互补——核心数据格式与网络消息格式两条防线同时覆盖。
九、相关资源索引
| 用途 | 仓库路径 |
|---|---|
| 官方 fuzz 使用说明(本文依据) | core/fuzz/README.md |
| fuzz 工程清单与 bin 声明 | core/fuzz/Cargo.toml |
| 语料生成器实现 | core/fuzz/src/main.rs |
| 6 个 fuzz 目标源码 | core/fuzz/fuzz_targets/ |
| 序列化/反序列化层与协议版本定义 | core/src/ser.rs |
| 全局链类型与 PROTOCOL_VERSION | core/src/global.rs |
| Block / UntrustedBlock 实现 | core/src/core/block.rs |
| CompactBlock / UntrustedCompactBlock 实现 | core/src/core/compact_block.rs |
| Transaction / TransactionBody 实现 | core/src/core/transaction.rs |
| P2P 消息层 fuzz 工程(横向参考) | p2p/fuzz |
总结
core/fuzz是 Grin 针对"不可信输入反序列化"这一关键攻击面构建的自动化防御体系:通过gen-corpus生成高质量种子语料,借助 cargo-fuzz / libFuzzer 的覆盖率引导,以 6 个同构 fuzz 目标持续覆盖Transaction、UntrustedBlock、UntrustedCompactBlock在 v1/v2 两代协议格式下的完整解析路径。掌握本文所述的环境搭建、语料生成、目标运行与结果解读方法后,你既能复现这套测试流程,也能依照同样的模式为仓库新增任意核心数据结构的 fuzz 覆盖,为序列化层的健壮性提供持续保障。
- 区块链
- Web3
【免费下载链接】grin
Minimal implementation of the Mimblewimble protocol.
相关推荐
OSS-Fuzz Rust 模糊测试实战指南:基于 cargo-fuzz 编写、构建与校验 fuzz target
OSS Fuzz Rust 模糊测试实战指南:基于 cargo fuzz 编写、构建与校验 fuzz target 本篇指南以 OSS Fuzz 仓库中 inf
测试应用安全质量保障RocksDB Fuzz 测试实战:基于 libFuzzer 与 protobuf 的结构感知模糊测试指南
RocksDB Fuzz 测试实战:基于 libFuzzer 与 protobuf 的结构感知模糊测试指南 导读 本文以 RocksDB 仓库 fuzz/REA
数据库KV存储嵌入式数据库存储Rome 模糊测试指南:基于 cargo-fuzz 与 libFuzzer 的 JS/TS/JSON 解析与格式化鲁棒性验证
Rome 模糊测试指南:基于 cargo fuzz 与 libFuzzer 的 JS/TS/JSON 解析与格式化鲁棒性验证 Rome(本仓库,Unified
开发工具Lint格式化前端构建编译器代码质量编辑器
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考