☰
Grin 核心数据结构 Fuzz 测试实战:基于 cargo-fuzz 与 libFuzzer 的序列化健壮性验证指南
2026/10/14 17:28:40 网站建设 项目流程
  • 区块链
  • Web3

【免费下载链接】grin

Minimal implementation of the Mimblewimble protocol.

项目地址:https://gitcode.com/gh_mirrors/gr/grin
点击查看免费下载

本文以 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_v1Transactionv1
transaction_read_v2Transactionv2
block_read_v1UntrustedBlockv1
block_read_v2UntrustedBlockv2
compact_block_read_v1UntrustedCompactBlockv1
compact_block_read_v2UntrustedCompactBlockv2

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,其核心逻辑是:

  1. 对 6 个 fuzz 目标逐一调用generate();
  2. generate使用ser::serialize将默认对象序列化后写入corpus/<target>/pattern文件;
  3. 序列化对象与协议版本一一对应:v1 目标使用ser::ProtocolVersion(1),v2 目标使用ser::ProtocolVersion(2);
  4. 生成的对象来自 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/pattern

corpus目录默认位于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 覆盖,参照现有模式即可:

  1. 在 core/fuzz/fuzz_targets/ 下新建xxx_read_v1.rs(复制现有模板,替换类型与协议版本);
  2. 在 core/fuzz/Cargo.toml 的[[bin]]段声明同名 bin 目标;
  3. 在 core/fuzz/src/main.rs 的main()中追加一行generate(...)以生成种子语料;
  4. 执行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_VERSIONcore/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.

项目地址:https://gitcode.com/gh_mirrors/gr/grin
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询