Aptos Core 模糊测试(Fuzz Testing)实践指南:基于 testsuite/fuzzer 的完整工作流

原创2026-09-17 15:29:371,814 阅读
文章标签:区块链Web3后端

Aptos Core 模糊测试(Fuzz Testing)实践指南:基于 testsuite/fuzzer 的完整工作流

导读

本文是 Aptos Core 仓库中 testsuite/fuzzer 模糊测试套件的实战指南,覆盖从编写 Fuzz Target、生成种子语料(Corpus)、本地构建运行、覆盖率分析到崩溃调试的完整链路。读完本文,你将掌握 fuzz.sh 脚本的全部命令用法、如何基于 cargo-fuzz + libfuzzer_sys 为 Move VM / Aptos VM 编写结构感知的模糊测试目标,以及如何将种子语料托管到 Google OSS-Fuzz 基础设施实现每日持续模糊测试。


一、测试套件概览:Aptos Core 的持续模糊测试基础设施

Aptos Core 的模糊测试代码集中在仓库的 testsuite/fuzzer 目录下,该目录包含用于对 Aptos Core 进行模糊测试的工具与脚本。根据 README.md 的说明,这些 Fuzz Target 持续运行在 main 分支的每日版本上,依托 Google 的 OSS-Fuzz 基础设施。

目录核心结构如下:

路径 作用
fuzz.sh 主脚本,封装所有常见模糊测试操作
src/main.rs 基于 clap 的 "Fuzz CLI",提供语料生成等子命令
src/types.rs 定义 RunnableState、ExecVariant、Authenticator 等结构感知输入类型
src/utils.rs 语料生成等工具函数的实现
fuzz/fuzz_targets/ 各 Fuzz Target 源码(cargo-fuzz 约定目录)
fuzz/dictionaries/ 模糊测试字典,如 u256.dict
data/ 用于生成语料的 Move 原生函数调用项目(按 0x1/ 地址组织)
google-oss-fuzz/ 面向 OSS-Fuzz 的构建配置
Cargo.toml 顶层依赖声明(fuzzer 库)

其中 fuzz/Cargo.toml 是 cargo-fuzz 工作区清单,每个 Target 以 [[bin]] 形式注册,并可通过 required-features 声明所需特性(fuzz.sh 会据此自动追加 --features)。

现有 Fuzz Target 一览

从 fuzz/fuzz_targets/ 目录可以列出仓库中已实现的全部模糊测试目标:

  • Move VM / Aptos VM 相关(位于 fuzz_targets/move/):
    • aptosvm_authenticators.rs、aptosvm_publish.rs、aptosvm_publish_and_run.rs、aptosvm_publish_and_run_transactional.rs
    • bytecode_verifier_compiled_modules.rs
    • deserialize_script_module.rs、move_value_decorate.rs、move_value_deserialize.rs、value_deserialize.rs
    • type_tag_to_string.rs
  • 其他模块:signed_transaction_deserialize.rs、u256_diff_fuzz.rs、use_case_aware_shuffler.rs

其中 move/ 目录下的 Target 共享 utils/ 子模块(authenticator.rs、helpers.rs、vm.rs),这些工具函数承担模块拓扑排序、包发布、快速校验等公共逻辑。


二、fuzz.sh 主脚本:全部命令与用法

fuzz.sh 是执行常见模糊测试操作的主入口。脚本开头的环境配置说明了它的运行前提:

export RUSTFLAGS="${RUSTFLAGS} --cfg tokio_unstable -C target-cpu=native"
export EXTRAFLAGS="-Ztarget-applies-to-host -Zhost-config"
# Nightly version control
NIGHTLY_VERSION="nightly-2025-08-07"

即:需要 Rust Nightly 工具链(脚本固定版本号为 nightly-2025-08-07,会通过 rustup install 自动安装),并使用 tokio_unstable cfg 与原生 CPU 目标优化。执行任意命令前,脚本会自动检查并安装 cargo-fuzz(见 check_cargo_fuzz 函数)。

2.1 命令速查表

命令 用途 语法
add 添加指定 Fuzz Target ./fuzz.sh add <fuzz_target_name>
block-builder 运行 Rust 工具辅助构建 fuzzers ./fuzz.sh block-builder <utility> [args]
block-builder-recursive 对目录下所有 Move.toml 批量执行 block-builder ./fuzz.sh block-builder-recursive <search_directory> <destination_directory>
build 构建指定或全部 Fuzz Target ./fuzz.sh build <fuzz_target|all> [target_dir]
build-oss-fuzz 专门为 OSS-Fuzz 环境构建 ./fuzz.sh build-oss-fuzz <target_dir>
clean-coverage 清理指定或全部目标的覆盖率产物 ./fuzz.sh clean-coverage <fuzz_target|all>
cmin 语料最小化(distillate corpora) ./fuzz.sh cmin <fuzz_target> [corpora_directory]
coverage 生成 HTML 格式覆盖率报告 ./fuzz.sh coverage <fuzz_target>
debug 使用 GDB 调试,传入 test_case 作为输入 ./fuzz.sh debug <fuzz_target> <test_case>
flamegraph 生成火焰图(可能需要额外的 OS 配置) ./fuzz.sh flamegraph <fuzz_target> <test_case>
list 列出所有现有 Fuzz Target ./fuzz.sh list
monitor-coverage 监控目标覆盖率,语料变化时自动重新生成 ./fuzz.sh monitor-coverage <fuzz_target>
run 运行指定 Fuzz Target,可选携带 testcase ./fuzz.sh run <fuzz_target> [testcase]
test 用预定义参数测试所有 Fuzz Target ./fuzz.sh test
tmin 最小化崩溃输入 ./fuzz.sh tmin <fuzz_target> <crashing_input>
samply 用 samply 采样剖析(脚本中额外提供) ./fuzz.sh samply <fuzz_target> <testcase>

2.2 关键命令的脚本实现细节

build(构建):默认输出目录为 ./target,通过 get_features_for_target 从 fuzz/Cargo.toml 中解析目标声明 required-features 并追加 --features,随后调用 cargo fuzz build --sanitizer none --verbose -O。其中 -O 表示开启优化以提升模糊测试吞吐。

run(运行):默认 fork 数量为 4(脚本注释提醒:超过 8 核时 fork 反而会降低性能),非 macOS 环境默认 -rss_limit_mb=4096、-ignore_crashes=0;macOS 上则使用 --sanitizer address。传入 testcase 时自动追加 -- -runs=1,即只执行一轮以便复现崩溃。

test(全量测试):遍历 list 输出的每个目标,以 -max_len=1024 -jobs=4 -workers=4 -runs=1000 运行,任一目标失败即中止。

coverage(覆盖率):需要 nightly 组件 llvm-tools-preview(README 中的提示 rustup +nightly-2024-04-06 component add llvm-tools-preview 即针对此场景;实际脚本会自动通过 cargo cov 检测并安装 cargo-binutils 与 rustfilt)。若已存在 coverage.profdata,脚本会交互式询问是 (c)lean 重新生成还是 (g) 直接用现有文件生成报告;最终用 cargo cov -- show 输出 HTML 到 fuzz/coverage/<target>/report,并通过 -Xdemangler=rustfilt 还原符号名。

build-oss-fuzz(OSS-Fuzz 专用构建):依次执行——应用 Patches 目录中的 git patch(针对部分依赖需编译 C/C++ 代码导致 OSS-Fuzz 构建失败的问题,脚本注释说明通过禁用 libFuzzer 下 C/C++ 编译绕过)、覆盖 CFLAGS/CXXFLAGS、限制 CARGO_BUILD_JOBS=4 防 OOM、逐个目标构建、把可执行文件拷贝到输出目录,最后从 CORPUS_ZIPS 数组下载种子语料 zip 并做"一料多用"(把 move_aptosvm_publish_and_run_seed_corpus.zip 同时复制为 move_aptosvm_publish_seed_corpus.zip 与 move_bytecode_verifier_compiled_modules_seed_corpus.zip)。

monitor-coverage(覆盖率监控):先做一次初始覆盖率生成,随后每 10 秒检测语料目录的最新文件修改时间,发现变化即重新生成覆盖率报告——适合长时间模糊测试时观察覆盖率增长。

debug 与 flamegraph(调试与剖析):debug 通过 rust-gdb 以 -- -runs=1 附加崩溃输入;flamegraph 通过 cargo flamegraph 输出 ${target}_${timestamp}.svg;samply 则用 samply record 采样,二进制超过一天未重建会自动重新构建。


三、编写 Fuzz Target:从零到可运行

3.1 初始化 Fuzz Target

使用 add 子命令一键初始化。脚本实现(add 函数)会在 fuzz/fuzz_targets/ 下创建目标文件,并向 fuzz/Cargo.toml 追加如下 [[bin]] 注册块:

./fuzz.sh add fuzz_target_name
[[bin]]
name = "fuzz_target_name"
path = "fuzz_targets/fuzz_target_name.rs"
test = false
doc = false

3.2 最小 Fuzz Target 模板

基于 cargo-fuzz + libfuzzer_sys 的基础结构(即 README 中给出的标准模板):

#![no_main]
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
    // Code to handle the fuzzer input and test the desired functionality
});

fuzz_target! 宏由模糊引擎(libFuzzer)驱动,为闭包提供一段随机字节 &[u8]。

3.3 结构感知输入:Arbitrary 与 Dearbitrary

字节流难以直接表达"模块 + 交易 + 签名者"这类复杂输入,Aptos Core 的做法是让 Fuzz Target 接收结构感知(struct-aware)数据,通过 arbitrary crate 的 Arbitrary trait 实现。最简便的方式是用 derive_arbitrary 特性派生:

#![no_main]
use arbitrary::Arbitrary;
use libfuzzer_sys::fuzz_target;

#[derive(Arbitrary)]
struct ComplexData {}

fuzz_target!(|data: ComplexData| {
    // Code to handle the fuzzer input and test the desired functionality
});

注意:ComplexData 结构体中用到的所有类型都必须实现(或派生)Arbitrary。

Aptos Core 在 src/types.rs 中定义了核心结构感知类型(均派生 Arbitrary 与 Dearbitrary):

  • RunnableState:一次可运行的模糊输入,包含 dep_modules: Vec<CompiledModule>(依赖模块)、exec_variant: ExecVariant(执行变体)、tx_auth_type: Authenticator(交易认证方式);
  • ExecVariant:二选一——Script(编译后脚本 + 类型实参 + 参数)或 CallFunction(模块 ID + 函数定义索引 + 类型实参 + 参数);
  • Authenticator:三种交易签名方式——Ed25519、MultiAgent(多签,脚本中限制次要签名者数量 ≤ 10,因数量过大会显著拖慢签名过程)、FeePayer(费用支付者);
  • UserAccount / FundAmount:控制账户是否初始化及资金量(Zero=0、Poor=1000、Rich=10^15)。

Dearbitrary 是逆向转换 trait,用于把生成的结构感知输入序列化回字节,供种子语料落盘。

3.4 实现模糊逻辑:以 aptosvm_publish_and_run 为例

fuzz_target! 宏接收来自 fuzzer 的数据后,需要实现"把模糊输入转换为目标函数/模块可处理格式"的逻辑。以 aptosvm_publish_and_run.rs 为例,它的完整流程展示了实战中的关键技巧:

  1. 快速过滤(filter_modules):拒绝类型参数索引超过 MAX_TYPE_PARAMETER_VALUE(64/4*16)的脚本,避免触发已知的 invariant violation 误报;
  2. 快速校验:先用生产级 verifier 配置(aptos_prod_verifier_config + LATEST_GAS_FEATURE_VERSION)对依赖模块与脚本做快速验证,不合规输入直接 Corpus::Reject;
  3. 去重检查:依赖模块按 self_id() 去重,重复即拒绝;
  4. 拓扑分组发布:通过 group_modules_by_address_topo 按地址对模块拓扑排序,使用 FakeExecutor::from_genesis_with_module_cache_manager 构建创世环境,再按包逐一 publish_group;
  5. 构造并签名交易:按 ExecVariant 分支构造 Script 或 EntryFunction 负载,按 Authenticator 分支做 Ed25519 / MultiAgent / FeePayer 签名;
  6. 执行并检查结果:执行单笔交易,若状态为 InvariantViolation 或 Unknown 则 panic!(这正是模糊测试要捕获的不变量破坏);随后校验 gas 使用——EXECUTION_TIME_GAS_RATIO=100,当"执行耗时(ms)/gas 消耗"超过该比值时视为意外 gas 使用并 panic(覆盖率模式下跳过此检查以避免误报)。

代码中的 DEBUG 环境变量控制:设置后仅打印告警而不 panic,便于本地观察潜在问题。

3.5 编写 Fuzz Target 的最佳实践

README 明确给出的四条准则:

  • 聚焦目标功能(Focus on Target Functionality):选择对应用功能与安全至关重要的函数或模块;
  • 处理多样化输入(Handle Diverse Inputs):确保 harness 能处理广泛的输入格式与尺寸;
  • 错误处理(Error Handling):实现健壮的错误处理,拦截崩溃或非预期行为;
  • 性能优化(Performance Optimization):为吞吐优化,以支撑更多迭代与更深层的模糊测试。

从源码看,Aptos 团队还将"不变量检查"内置在 harness 中(如 invariant violation 即 panic),并把"意外 gas 消耗"也当作需要捕获的异常行为,这些都是值得借鉴的工程实践。


四、OSS-Fuzz 种子语料:托管与接入

4.1 语料包命名与托管

某些 fuzzer 在提供良好初始语料时效果更好。README 要求把语料打包为 zip 并按如下格式命名:

[fuzzer_name]_seed_corpus.zip

例如 move_aptosvm_publish_and_run_seed_corpus.zip。托管与接入步骤如下:

  1. 上传到公开托管:如选择 Google Drive,需确保通过共享链接公开访问;
  2. (仅 GDrive)改写 URL:把 URL 模板中的 FILEID 替换为你的文件 ID:
    https://docs.google.com/uc?export=download&id=FILEID
    
  3. 更新 fuzz.sh:把修改后的 URL 插入 fuzz.sh 中的 CORPUS_ZIPS 数组。

当前仓库 CORPUS_ZIPS 数组已内置两个语料源(托管在 Google Cloud Storage 上):

CORPUS_ZIPS=(
    "https://storage.googleapis.com/aptos-core-corpora/move_aptosvm_publish_and_run_seed_corpus.zip"
    "https://storage.googleapis.com/aptos-core-corpora/move_aptosvm_publish_and_run_transactional_seed_corpus.zip"
)

在 OSS-Fuzz 环境构建时,fuzz.sh 会把语料压缩包正确放置到 fuzzer 二进制旁,OSS-Fuzz 会按名称选取对应压缩包,用其内容喂养 fuzzer。

注意:README 在"Generate Corpora"一节特别提醒:更推荐把语料上传到 GCP Bucket 或 S3 等公开云端,避免使用 GDrive(因下载链接格式与稳定性问题)。


五、生成种子语料:block-builder 工具链

一些 fuzzer 依赖初始语料的质量。语料生成通过 ./fuzz.sh block-builder 调用 src/main.rs 中定义的 "Fuzz CLI" 完成。该 CLI(作者标注为 Security Team @ Aptos Labs)提供以下子命令:

子命令 作用
compile_federated_jwk <module_path> 从源码编译模块,导出序列化元数据与代码,供 fuzz target 静态初始化使用
generate_runnable_state <csv_path> <destination_path> 从 CSV(第三列为 base64 编码模块)生成可运行状态
generate_runnable_state_from_project <project_path> <destination_path> 从 Move 项目生成可运行状态
generate_runnable_states_recursive <base_dir> <destination_path> 递归处理目录下所有 Move.toml 项目
generate_transactional_runnable_states_recursive <base_dir> <destination_path> 递归处理目录下所有 .move 文件,生成事务性可运行状态
generate_runnable_states_from_all_tests <destination_path> 从所有测试源码(e2e、transactional、compiler v2 测试)生成可运行状态

5.1 Aptos-VM Publish & Run 语料

README 给出的 CSV 方式用法:

./fuzz.sh block-builder generate_runnable_state /tmp/modules.csv /tmp/Modules

CSV 文件结构为:

  • 第 1 列:模块名(Module name)
  • 第 2 列:模块地址(Module address)
  • 第 3 列:模块字节码的 Base64 编码(Base64-encoded bytecode)

任何合法的 Move 项目都可用来生成测试用例(README 提示:函数调用的参数可能需要额外关注,属 TODO 事项),这对测试新功能或提升覆盖率完整性很有帮助。对于原生函数(native functions),请遵循 data/ 目录中的结构——仓库 data/0x1/ 下按原生函数模块组织了几十个微型项目(如 aptos_hash/keccak256、ed25519/signature_verify_strict_internal、bls12381/*、ristretto255/*、string/internal_check_utf8 等),每个项目含 Move.toml 与 sources/call_native.move,即一个只调用单个原生函数的 entry 函数。

5.2 从项目生成

./fuzz.sh block-builder generate_runnable_state_from_project data/0x1/string/generic fuzz/corpus/move_aptosvm_publish_and_run

第一个参数是项目路径,第二个是目标目录。

关键约束:需要创建一个 entry 函数,可以接受 signer 或无参数;泛型 T 函数不允许作为 entry。

验证建议:在调用 fuzzer 时附加 DEBUG=1,并使用新生成的测试用例作为第二个参数,验证测试用例按预期运行。

5.3 批量构建

./fuzz.sh block-builder generate_runnable_states_recursive data/0x1/ fuzz/corpus/move_aptosvm_publish_and_run

该命令递归编译指定目录下的所有模块(底层由 fuzz.sh 的 block-builder-recursive 委托给 Rust 命令 generate_runnable_states_recursive,传入 base_dir 与 destination_path,并在目标目录不存在时自动 mkdir -p)。

5.4 语料发布内部步骤(internal)

README 给出了完整的内部发布流程,适用于任何想公开的种子语料(记得把公开链接加到 fuzz.sh 开头的 CORPUS_ZIPS):

# 1. 登录 GCP
gcloud auth login
# 2. 从存储桶下载现有语料
gcloud storage cp gs://aptos-core-corpora/move_aptosvm_publish_and_run_seed_corpus.zip move_aptosvm_publish_and_run_seed_corpus.zip
# 3. 解压
unzip move_aptosvm_publish_and_run_seed_corpus.zip -d move_aptosvm_publish_and_run_seed_corpus
# 4. 递归重新生成可运行状态
./fuzz.sh block-builder generate_runnable_states_recursive data/0x1/ move_aptosvm_publish_and_run_seed_corpus
# 5. 通常应运行 cmin 最小化;但此处假定手动创建的输入已足够好且服务特定目的
# 6. 重新打包
zip -r move_aptosvm_publish_and_run_seed_corpus.zip move_aptosvm_publish_and_run_seed_corpus
# 7. 上传回存储桶
gsutil storage cp move_aptosvm_publish_and_run_seed_corpus.zip gs://aptos-core-corpora/move_aptosvm_publish_and_run_seed_corpus.zip
# 8. 恢复 ACL(公开 URL 保持不变)
gsutil storage acl ch -u AllUsers:R gs://aptos-core-corpora/move_aptosvm_publish_and_run_seed_corpus.zip

六、崩溃调试与性能剖析

6.1 常规手段:符号化栈回溯

因为构建出的二进制是符号化的,可以直接使用 fuzzer 产生的栈回溯。但对于 INVARIANT_VIOLATIONS(不变量违反),栈回溯是不准确的。要获得正确的栈回溯,使用:

DEBUG_VM_STATUS=<status_reported_by_the_fuzzer> ./fuzz.sh run <fuzzer_target> <test_case>

该命令是选择性触发的:只有指定的、逗号分隔的 status 才会触发 PartialVMError 中的 panic。这与 bytecode_verifier_compiled_modules.rs 中的处理思路一致——该 Target 在 check_for_invariant_violation_vmerror 中会忽略已知误报(消息以 "too many type parameters/arguments in the program" 开头的错误),其余 InvariantViolation 直接 panic。

6.2 高级手段:GDB、Flamegraph 与 Samply

fuzz.sh 集成了多种调试与剖析工具:

  • GDB 调试:./fuzz.sh debug <fuzz_target> <test_case> 使用 rust-gdb 以 -- -runs=1 模式挂载崩溃输入,并导出 LSAN_OPTIONS=verbosity=1:log_threads=1 与 RUST_BACKTRACE=1 辅助定位内存问题;
  • Flamegraph:./fuzz.sh flamegraph <fuzz_target> <test_case> 通过 cargo flamegraph(--root 模式)输出火焰图 SVG,用于定位热点(README 提醒可能需要额外的 OS 配置,如 perf 权限);
  • Samply:./fuzz.sh samply <fuzz_target> <test_case> 使用 samply record 采样剖析,二进制缺失或超过一天会自动重建。

6.3 崩溃输入最小化

./fuzz.sh tmin <fuzz_target> <crashing_input>

底层调用 cargo fuzz tmin(--sanitizer address -O),把崩溃输入精简到最小可复现集合,便于人工分析根因。

6.4 语料最小化

./fuzz.sh cmin <fuzz_target> [corpora_directory]

默认语料目录为 ./fuzz/corpus/<fuzz_target>,用 cargo fuzz cmin 蒸馏语料,剔除不增加覆盖率覆盖的冗余输入。


七、本地快速体验:完整演练

综合上述内容,一套完整的本地工作流如下:

# 1. 列出所有目标(会自动安装 cargo-fuzz)
./fuzz.sh list

# 2. 构建单个目标(或 all 构建全部;可用第二参数指定输出目录)
./fuzz.sh build move_aptosvm_publish_and_run

# 3. 运行目标(可指定语料目录;默认 fork=4、rss_limit_mb=4096)
./fuzz.sh run move_aptosvm_publish_and_run

# 4. 用预定义参数快速验证所有目标(-max_len=1024 -jobs=4 -workers=4 -runs=1000)
./fuzz.sh test

# 5. 用 DEBUG=1 + 单个 testcase 复现/验证某个输入
DEBUG=1 ./fuzz.sh run move_aptosvm_publish_and_run /path/to/testcase

# 6. 生成覆盖率报告(HTML,输出到 fuzz/coverage/<target>/report)
./fuzz.sh coverage move_aptosvm_publish_and_run

# 7. 崩溃输入最小化
./fuzz.sh tmin move_aptosvm_publish_and_run /path/to/crash_input

运行前提:本仓库为 Rust 工作区,需要 Rust Nightly(nightly-2025-08-07,脚本自动安装)、cargo-fuzz(自动安装),覆盖率场景还需 nightly 的 llvm-tools-preview 组件与 cargo-binutils/rustfilt。由于 RUSTFLAGS 包含 -C target-cpu=native,建议在与目标环境一致的机器上构建运行。


八、参考与延伸阅读

README 末尾列出的参考资料:

  • Rust Fuzz Book:cargo-fuzz / libFuzzer 的权威使用手册;
  • Google OSS-Fuzz:Google 的持续模糊测试服务文档,Aptos Core 的 Fuzz Target 每日构建即运行于此;
  • Arbitrary:arbitrary crate 文档,用于实现结构感知输入;
  • Native Functions:Aptos Move 标准库原生函数文档,对应 data/ 目录下各原生函数语料项目的编写规范。

若希望为 Aptos Core 贡献新的 Fuzz Target 或改进 fuzz.sh,README 明确表示欢迎贡献;修改时请保持与现有 Target 一致的风格——优先复用 src/types.rs 的 RunnableState 等结构感知类型与 fuzz/fuzz_targets/move/utils/ 公共工具,并为新语料补充 CORPUS_ZIPS 条目。

登录后查看全文
aptos-core