Monero RPC 模糊测试(Fuzzing)框架解析:绕过传输层直测 core_rpc_server 处理器
2026/9/24 2:34:00 网站建设 项目流程
  • 区块链
  • 金融科技

【免费下载链接】monero

Monero: the secure, private, untraceable cryptocurrency

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

导读

本文以 tests/fuzz/fuzz_rpc/README.md 为骨架,结合 Monero 仓库中tests/fuzz/fuzz_rpc/目录的完整源码,深入解析 Monero 官方 RPC 模糊测试(fuzzing)框架的设计思路、构建方式与运行流程。读完本文,你将掌握:Monero 如何在不启动真实服务器的情况下直接对core_rpc_server的各 RPC 端点处理函数进行模糊测试、Safe/Risky/Priority 三类端点如何划分与编排、FAKECHAIN回归测试模式如何构造虚拟区块链,以及 ZMQ 发布端如何被纳入同一套模糊测试体系。

为什么模糊测试要跳过传输层?

Monero 的 RPC 服务(core_rpc_server)对外通过 HTTP/JSON-RPC 与 ZMQ 两种途径暴露大量端点。传统做法是启动一个真实服务器、通过网络协议灌入随机请求,再观察崩溃与内存错误——但这样做有一个显著缺陷:

  • 传输层噪声大:HTTP 解析、JSON 编解码、连接管理会消耗大量模糊测试输入(fuzz input),真正触达业务处理逻辑的路径占比很低;
  • 端点覆盖不完整:网络层入口难以稳定覆盖到每个处理函数的所有分支;
  • 启动开销高:每轮迭代都走完整的网络栈,吞吐量受限。

Monero 的做法是:跳过传输层,直接实例化core_rpc_server对象,跳过 fake server 的搭建,直接调用各 RPC 端点的处理函数。这样模糊测试的随机数据全部作用于 handler 内部的请求构造与业务逻辑,效率与覆盖率都更高。该思想在 tests/fuzz/fuzz_rpc/README.md 开头即有明确说明。

文件结构总览

tests/fuzz/fuzz_rpc/目录下共 9 个文件,职责划分清晰:

文件作用
fuzz_rpc.cppRPC 模糊测试主入口,包含LLVMFuzzerTestOneInput,管理每一轮迭代
fuzz_zmq.cppZMQ 模糊测试主入口,同样包含LLVMFuzzerTestOneInput
initialisation.cpp初始化辅助函数:配置 dummy 协议、P2P、core 对象;生成随机区块、矿工、交易
initialisation.h初始化相关类型声明与导出
rpc_endpoints.cppRPC 端点请求/响应对象构造与端点函数调用封装,以及三类端点映射表
rpc_endpoints.hRPC 端点 fuzz 函数声明与映射表导出
zmq_endpoints.cppZMQ 发布端点的请求构造与调用封装
zmq_endpoints.hZMQ 端点声明
README.md本文所基于的框架说明文档

RPC 端点的三类划分:Safe / Risky / Priority

rpc_endpoints.cpp将所有被模糊测试的 RPC 端点函数划分为三个映射表(std::map<int, std::function<...>>),分别对应三类行为特征:

  • Priority(优先级):最关键、每轮迭代至少执行一次的端点。包括get_blocksget_blocks_by_heightget_hashesget_outs_binget_transactionsis_key_image_spentsend_raw_txget_output_distribution_binpop_blocksgetblocktemplatesubmitblockgenerateblocksrelay_txget_output_distribution(映射键 0~13)。这些端点大多直接读写区块链数据或涉及交易提交,属于风险与价值双高的核心路径。
  • Safe(安全):被视为稳定、不易失败的端点,共 41 个(映射键 14~54),覆盖get_heightget_infoget_transaction_pool*系列、区块头查询系列、peer 列表、日志设置、set_bans/get_banshard_fork_info等只读或轻量写操作。
  • Risky(高风险):更容易失败或提前返回(尤其是没有有效区块链生成时),包括start_miningstop_miningmining_statussave_bcstop_daemonupdateadd_aux_powflush_txpoolflush_cacheget_txids_loose(映射键 55~65,其中 59 空缺、72 即prune_blockchain被注释禁用)。这些端点或会修改全局状态,或在空链/脏数据下容易触发异常提前退出。

从源码结构看,safe_fuzz_targets中的键与priority_fuzz_targets的键连续衔接(14 起),而risky_fuzz_targets键位存在跳跃(55~65 与 72),说明映射键只是内部编号,与端点实际 ID 无直接对应关系。完整定义见 rpc_endpoints.cpp。

构建方式:一个源文件编译出三个 fuzzer 变体

同一份 fuzz_rpc.cpp 被编译成两个版本,区别在于是否定义SAFE宏;fuzz_zmq.cpp 则只编译单一版本。对应构建规则在 tests/fuzz/CMakeLists.txt,并且整体受OSSFUZZ开关控制:

目标名宏定义行为
fuzz_rpcSAFE只模糊测试 Safe 类端点
fuzz_rpc_full同时包含 Safe 与 Risky 端点
fuzz_rpc_full_no_exceptionsCATCH_ALL_EXCEPTIONSSAFE且吞掉全部异常(仅捕获runtime_errorDB_ERROR之外的兜底分支也被静默)
fuzz_zmq单版本 ZMQ 模糊测试,链接rpc_pub

构建时还需要LIB_FUZZING_ENGINE环境变量指定 libFuzzer 或 AFL 等引擎,<fuzzer/FuzzedDataProvider.h>头文件由 tests/fuzz/include 提供(这是 LLVM 项目的单头文件工具,用于把模糊输入字节流按需拆分为整数、字符串、布尔等类型)。此外,CMake 会为fuzz_unsafe_macro目标单独重编译 src/common/perf_timer.cpp 并定义FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION,确保性能计时器在模糊测试构建下不会干扰(并避免生产断言被误触发)。

运行流程逐层拆解

第一步:随机选择端点函数

在 fuzz_rpc.cpp 中,LLVMFuzzerTestOneInput先做输入门槛检查:单轮迭代至少需要 512 字节输入,不足则直接返回 0 跳过。随后:

  1. 根据是否定义SAFE决定is_safe_mode,调用get_fuzz_targets(is_safe_mode)取回端点函数映射。非安全模式下会合并 priority + safe + risky 三张表;安全模式则只取 safe 表并对键重新编号(kv.first - 14,见 rpc_endpoints.cpp)。
  2. 通过provider.ConsumeIntegralInRange<unsigned>(1, 16)随机决定本轮要发送 1~16 条 RPC 消息。
  3. 非安全模式下,priority 端点保证至少出现一次:先把 priority 表全部索引压入 selectors,再随机洗牌;随后再随机追加若干普通端点索引。
  4. 调用el::Loggers::addFlag(...DisableApplicationAbortOnFatalLog)关闭致命日志导致的进程退出。

第二步:初始化 core 与 core_rpc_server

每次迭代都会重新创建一个core_rpc_server对象,并配齐 dummy 协议与 P2P 组件(见 fuzz_rpc.cpp):

  • initialise_rpc_core():创建DummyProtocolcryptonote::core,解析命令行参数--regtest --offline --data-dir <临时目录>(临时目录为系统 temp 下的monero-fuzz-rpc),然后调用core->init(vm)
  • initialise_rpc_server(*dummy_core, provider.ConsumeBool()):用随机布尔值决定本次迭代运行在restricted(受限)还是 unrestricted(非受限)RPC 模式——这直接对应真实节点中--restricted-rpc的语义,见 src/rpc/core_rpc_server.cpp 中m_restricted的初始化。受限模式下,get_info等端点会隐藏 alt 块数、连接数、对等节点列表等敏感信息(core_rpc_server.cpp)。

初始化函数内部(initialisation.cpp)还通过模板实例化nodetool::node_server<cryptonote::t_cryptonote_protocol_handler<cryptonote::core>>作为 dummy P2P 节点,core_rpc_server的构造函数正是接收(core&, node_server&, bool restricted)三个参数。

fuzz_zmq不需要这一步——它不依赖 core/区块链,直接创建 libzmq context(zmq_ctx_new)并实例化zmq_pub发布器。

第三步:生成随机区块与交易(FAKECHAIN)

generate_random_blocks(initialisation.cpp)是流程中最复杂的一环,其核心是FAKECHAIN(回归测试模式)--regtest标志让区块链跳过对作者签名和交易 ID 的校验逻辑,同时保留其余校验规则,从而允许伪造的区块/交易被顺利接纳。具体流程:

  1. 区块缓存:先用generate_genesis_block生成创世块;再随机生成 1~4 个矿工账户(account_base::generate)、2~15 个区块(输入不足时提前终止)。每个区块按median_weight = 300000block_weight = 100000计算奖励(get_block_reward),用construct_miner_tx构造带随机 nonce 的矿工交易,累积coins余额。区块与交易会缓存在static变量中,并通过provider.ConsumeBool()决定本轮是复用缓存还是重新生成——这保证了模糊测试过程中"链状态"的连续性。
  2. 交易缓存:随机生成 1~4 笔交易。每笔交易创建发送/接收账户、随机转账金额(10000 ~ 1000000000)、构造 10 个环签名输出条目(其中 1 个为真实输出、其余为假输出,view_tag取随机值),最终通过construct_tx_and_get_tx_key构造带RangeProofPaddedBulletproof范围证明的 v2 交易,序列化后缓存。
  3. 上链:先batch_start()开启数据库批量写,然后调用add_new_block逐块加入,再用handle_incoming_tx把交易送入交易池;被接纳的交易哈希存入全局cached_tx_hashes,供fuzz_get_transactions等端点使用"真实存在的交易哈希"做深度校验。若没有任何区块成功加入,则batch_stop()后跳过本轮迭代。

第四步:真正的模糊测试

selectors 全部确定后,进入执行循环(fuzz_rpc.cpp):

for (unsigned selector : selectors) { try { fuzz_targetsselector; } catch (const std::runtime_error&) { // 已知的 monero runtime_error } catch (const cryptonote::DB_ERROR& e) { // 随机值触发内部区块链 DB 检查时的已知错误 } }

每个端点函数(见 rpc_endpoints.cpp)的模式高度统一:声明对应的COMMAND_RPC_*::requestresponse结构体(定义于 src/rpc/core_rpc_server_commands_defs.h),用FuzzedDataProvider生成随机字段填充请求,然后直接调用core_rpc_server上的on_*处理函数。以fuzz_get_blocks为例:随机生成 0~16 个区块哈希、随机start_heightmax_block_countprune等标志位,再调用rpc.on_get_blocks(req, res, &ctx)rpc_endpoints.cpp顶部定义了共享的connection_context ctxepee::json_rpc::error error_resp,模拟真实 HTTP 连接上下文与 JSON-RPC 错误通道。

每轮迭代结束时调用get_db().batch_stop()关闭批量写入,然后返回 0 进入下一轮。

异常处理策略:让 fuzzer 只对真正的崩溃负责

模糊测试的输入完全随机,必然触发大量业务层"预期中的"失败路径。Monero 在三个层面做了隔离:

  1. 已知异常静默std::runtime_errorcryptonote::DB_ERROR(定义于 src/lmdb/error.h)被显式捕获并忽略,因为随机值触发内部 DB 检查报错是正常现象;
  2. 全量兜底fuzz_rpc_full_no_exceptions变体通过CATCH_ALL_EXCEPTIONS宏启用catch (...)兜底,把一切异常都静默掉,用于区分"异常路径"与"真正未处理错误";
  3. 致命日志抑制DisableApplicationAbortOnFatalLog避免 easylogging++ 的致命日志直接 abort 进程。

这样一来,只有未被捕获的崩溃(segfault、断言失败、越界等)才会被 libFuzzer/AFL 报告为 bug,信噪比大幅提升。

ZMQ 模糊测试:fuzz_zmq 的独立路径

zmq_endpoints.cpp 定义了 4 个 ZMQ 发布端点 fuzz 函数:

函数作用
0fuzz_sub_request随机订阅字符串调用pub.sub_request
1fuzz_send_chain_main随机高度 + 0~4 个随机区块调用pub.send_chain_main
2fuzz_send_miner_data随机主版本、高度、难度、median weight、coins 与 0~3 条 backlog 条目调用pub.send_miner_data
3fuzz_send_txpool_add构造 0~3 个txpool_event(随机 blob 尝试解析为交易)调用pub.send_txpool_add

fuzz_zmq.cpp的最小输入门槛为 64 字节;每轮随机发送 1~8 条 ZMQ 消息,使用独立于 RPC 的zmq_targets映射表,不经过 core/区块链初始化。其被 fuzz 的对象是 src/rpc/zmq_pub.h 中的zmq_pub发布器——模糊测试目标从"请求处理"扩展到了"发布消息的构造与序列化"。

如何运行

在配置了OSSFUZZ的构建环境中(CMake 开关开启,且提供LIB_FUZZING_ENGINE),编译后在build/tests下得到fuzz_rpcfuzz_rpc_fullfuzz_rpc_full_no_exceptionsfuzz_zmq四个可执行文件,配合 libFuzzer 即可运行,例如:

# 安全模式(仅 Safe 端点) ./fuzz_rpc -runs=10000 corpus_rpc # 完整模式(Safe + Risky,priority 每轮必跑) ./fuzz_rpc_full -runs=10000 corpus_rpc_full # ZMQ 发布器模糊测试 ./fuzz_zmq -runs=10000 corpus_zmq

-runs之外的选项(如-max_len-jobs)均遵循 libFuzzer 通用约定;也可用 AFL 等引擎替换。仓库中 contrib/fuzz_testing/fuzz.sh 提供了通用模糊测试辅助脚本,可作参考。

小结

Monero 的 RPC 模糊测试框架通过"跳过传输层、直连 handler、FAKECHAIN 伪造链、三类端点分级编排"四板斧,把有限模糊测试输入的能量集中释放在 RPC 业务逻辑上,并以fuzz_rpc/fuzz_rpc_full/fuzz_rpc_full_no_exceptions/fuzz_zmq四种变体覆盖不同风险偏好与异常策略组合。其设计思路与源码实现(fuzz_rpc.cpp、initialisation.cpp、rpc_endpoints.cpp、zmq_endpoints.cpp)对其他大型 C++ 项目做 RPC/API 层模糊测试具有直接借鉴价值——尤其是"用宏开关控制端点集合""静态缓存链上数据提升迭代效率""显式区分已知异常与真崩溃"这三个工程细节。

  • 区块链
  • 金融科技

【免费下载链接】monero

Monero: the secure, private, untraceable cryptocurrency

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

相关推荐

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

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

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

立即咨询