Aptos Core 模糊测试(Fuzz Testing)实践指南:基于 testsuite/fuzzer 的完整工作流
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.rsbytecode_verifier_compiled_modules.rsdeserialize_script_module.rs、move_value_decorate.rs、move_value_deserialize.rs、value_deserialize.rstype_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 为例,它的完整流程展示了实战中的关键技巧:
- 快速过滤(
filter_modules):拒绝类型参数索引超过MAX_TYPE_PARAMETER_VALUE(64/4*16)的脚本,避免触发已知的 invariant violation 误报; - 快速校验:先用生产级 verifier 配置(
aptos_prod_verifier_config+LATEST_GAS_FEATURE_VERSION)对依赖模块与脚本做快速验证,不合规输入直接Corpus::Reject; - 去重检查:依赖模块按
self_id()去重,重复即拒绝; - 拓扑分组发布:通过
group_modules_by_address_topo按地址对模块拓扑排序,使用FakeExecutor::from_genesis_with_module_cache_manager构建创世环境,再按包逐一publish_group; - 构造并签名交易:按
ExecVariant分支构造Script或EntryFunction负载,按Authenticator分支做 Ed25519 / MultiAgent / FeePayer 签名; - 执行并检查结果:执行单笔交易,若状态为
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。托管与接入步骤如下:
- 上传到公开托管:如选择 Google Drive,需确保通过共享链接公开访问;
- (仅 GDrive)改写 URL:把 URL 模板中的
FILEID替换为你的文件 ID:https://docs.google.com/uc?export=download&id=FILEID - 更新
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:
arbitrarycrate 文档,用于实现结构感知输入; - Native Functions:Aptos Move 标准库原生函数文档,对应 data/ 目录下各原生函数语料项目的编写规范。
若希望为 Aptos Core 贡献新的 Fuzz Target 或改进 fuzz.sh,README 明确表示欢迎贡献;修改时请保持与现有 Target 一致的风格——优先复用 src/types.rs 的 RunnableState 等结构感知类型与 fuzz/fuzz_targets/move/utils/ 公共工具,并为新语料补充 CORPUS_ZIPS 条目。