SurrealDB 模糊测试实战指南:基于 cargo-fuzz 的 Harness 构建、编译与并行执行
【免费下载链接】surrealdbA scalable, distributed, collaborative, document-graph database, for the realtime web项目地址: https://gitcode.com/GitHub_Trending/su/surrealdb
导读
本文以 SurrealDB 仓库中 fuzz/README.md 为骨架,系统讲解如何为 SurrealDB 搭建基于 libFuzzer 与 cargo-fuzz 的模糊测试环境:从 nightly 编译器安装、cargo-fuzz 工具链部署,到四个官方 fuzz harness 的构建与运行。读完本文,你将掌握fuzz_executor、fuzz_sql_parser、fuzz_structured_executor、fuzz_format四个目标各自的设计意图与底层调用链,并能通过字典文件与-fork并行参数最大化本地 CPU 利用率,从而在自己复现崩溃、回归验证或扩展 Harness 时直接照搬这套工作流。
为什么 SurrealDB 需要模糊测试
SurrealDB 的核心是一个 SQL 方言(SurrealQL)解析器加上一套分布式文档-图数据库执行引擎。任何用户输入(HTTP 查询、WebSocket 请求、导入文件)最终都会进入两条关键路径:
- 语法解析:
surrealdb_core::syn::parse将字符串解析为 AST,位于 surrealdb/core/src/syn/mod.rs; - 执行引擎:解析后的 AST 交给
Datastore与Session执行(如 surrealdb/core/src/dbs/iterator.rs 中的process)。
这两条路径一旦被畸形输入触发 panic、无限递归或栈溢出,就会造成服务拒绝。为此仓库维护了一套由 cargo-fuzz 管理的模糊测试 Harness,全部位于 fuzz 目录,通过覆盖率反馈驱动的 libFuzzer 在运行时自动发现并复现崩溃输入。
四个官方 Fuzz Harness 全景
仓库在 fuzz/Cargo.toml 中声明了四个二进制目标,分别覆盖解析与执行两个层面:
| Harness 二进制 | 源码 | 目标对象 | 核心动作 |
|---|---|---|---|
fuzz_sql_parser | fuzz/fuzz_targets/fuzz_sql_parser.rs | &str原始字符串 | 直接调用syn::parse,验证"不要崩溃" |
fuzz_executor | fuzz/fuzz_targets/fuzz_executor.rs | &str原始字符串 | 分号切分命令后在内存 Datastore 中逐条execute |
fuzz_structured_executor | fuzz/fuzz_targets/fuzz_structured_executor.rs | Ast(arbitrary 结构化输入) | 将任意生成的 AST 直接送入Datastore::process |
fuzz_format | fuzz/fuzz_targets/fuzz_format.rs | Ast(arbitrary 结构化输入) | 格式化 → 重新解析的往返一致性校验 |
其中fuzz_executor与fuzz_sql_parser使用字节/字符串级输入,能探测分词、解析、语法层面的问题;而fuzz_structured_executor与fuzz_format依赖libfuzzer-sys的arbitrary-derive特性(见 fuzz/Cargo.toml),直接从任意字节流生成Ast结构体,绕开字符串词法层,专攻深层语义、类型推导与执行路径。
环境准备:nightly 编译器与 cargo-fuzz
为什么必须用 nightly
模糊测试的高效性依赖运行时代码覆盖率反馈(coverage feedback)来引导变异方向。在撰写本文所依据的 README 时,当前 stable 版 rustc 尚无法对 harness 进行覆盖率插桩,因此必须借助 nightly 中的前沿特性。仓库根目录提供了工具链锁定文件:
- rust-toolchain.toml 与 rust-toolchain.nightly,可用 rustup 按需安装对应 nightly 工具链:
rustup toolchain install nightly安装后所有 fuzz 相关命令都需要显式指定 nightly,即cargo +nightly ...。
安装 cargo-fuzz
cargo-fuzz 的完整安装选项可参考 cargo-fuzz 官方书(fuzz/README.md 中给出的文档地址),最简安装只需一条命令:
cargo +nightly install cargo-fuzz该命令会把cargo fuzz子命令安装到本地 cargo bin 目录,之后即可用cargo +nightly fuzz驱动整个构建与运行流程。
构建 Fuzzer:优化与调试两种模式
标准构建(最大优化)
fuzz/README.md 给出的标准构建命令以fuzz_executor为例:
cargo +nightly fuzz build --fuzz-dir ./ fuzz_executor其中:
--fuzz-dir ./指以 fuzz 目录(即 README 所在目录)作为 fuzz 工作区;- 目标名
fuzz_executor对应 fuzz/Cargo.toml 中的[[bin]]声明; - 该命令默认携带调试信息并以
-O3最大优化编译,确保运行时吞吐最大化,适合长时间挂机跑覆盖率。
[profile.release] debug = 1(见 fuzz/Cargo.toml)保证了在 release 优化下依然保留行级调试信息,方便后续用符号化工具分析崩溃栈。
无优化构建(复现崩溃专用)
当你在排查一个已发现的崩溃时,全量优化会显著拖慢编译。README 明确指出:构建时追加-D可关闭优化,虽然模糊测试速度会慢约 10 倍,但对于复现某个固定崩溃输入而言依然绰绰有余:
cargo +nightly fuzz build -D --fuzz-dir ./ fuzz_executor建议的实践是:日常跑量用-O3构建,拿到崩溃样本后切到-D构建复现并加日志调试。
运行 Fuzzer:单线程到全核并行
列出可用 Harness
构建完成后,先用fuzz list确认仓库中所有可用的 fuzz 目标:
cargo +nightly fuzz list --fuzz-dir ./该命令会输出fuzz_sql_parser、fuzz_executor、fuzz_structured_executor、fuzz_format四个名称(与 fuzz/Cargo.toml 中的 bin 声明一一对应)。
默认单线程运行
libFuzzer 的默认模式是单线程。以fuzz_executor为例:
cargo +nightly fuzz run --fuzz-dir ./ fuzz_executor运行后会持续输出覆盖率、执行速度(exec/s)、新路径发现数量等统计,一旦发现崩溃会把最小化后的输入落盘到 fuzz 工作区的artifacts目录。
全核并行 + 字典文件
要充分利用本机算力,可以用 libFuzzer 的-fork=N参数启动 N 个独立进程并行 fuzz,并用-dict加载 SurrealQL 专用字典来提升变异效率(README 中的#FUZZ_TARGET#需替换为实际目标名,如fuzz_executor):
# -fork: 运行 N 个独立进程并行 fuzz,这里用 nproc 匹配本机处理器数量 # -dict: 启用该 fuzzer 专属字典文件 cargo +nightly fuzz run --fuzz-dir ./ \ fuzz_executor -- -fork=$(nproc) \ -dict=fuzz/fuzz_targets/fuzz_executor.dict注意:--之后的参数会原样透传给 libFuzzer。$(nproc)在 Linux 上返回逻辑核数,可自动做到"一台机器开满核"。
深入源码:四个 Harness 的实现原理
fuzz_sql_parser:最薄的"防崩溃"门卫
fuzz/fuzz_targets/fuzz_sql_parser.rs 全量实现只有几行:
#![no_main] use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &str| { // Don't crash. _ = surrealdb_core::syn::parse(data); });要点:
#![no_main]是 libFuzzer harness 的固定写法,由libfuzzer_sys提供真正的入口;- 输入是任意
&str,直接喂给 surrealdb/core/src/syn/mod.rs 的parse; parse内部会基于Capabilities::all()构建解析设置(见 settings_from_capabilities),并在 parse_with_settings 中对输入长度做u32::MAX上限校验、对对象与查询递归深度设限,从解析器层面防止超长输入与深递归导致的栈溢出。
它验证的契约是:任何字符串经parse只应返回Ok或Err,绝不 panic。
fuzz_executor:带会话的端到端执行
fuzz/fuzz_targets/fuzz_executor.rs 是真正的执行级 harness:
fuzz_target!(|commands: &str| { let commands: Vec<&str> = commands.split_inclusive(";").collect(); let blacklisted_command_strings = ["sleep", "SLEEP"]; use surrealdb_core::{dbs::Session, kvs::Datastore}; let max_commands = 500; if commands.len() > max_commands { return; } tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { let dbs = Datastore::new("memory").await.unwrap(); let ses = Session::owner().with_ns("test").with_db("test"); for command in commands.iter() { for blacklisted_string in blacklisted_command_strings.iter() { if command.contains(blacklisted_string) { return; } } let _ignore_the_result = dbs.execute(command, &ses, None).await; // TODO: 为查询包一层 tokio 超时,防止单个命令卡死整个 fuzz 进程 } }) });设计细节值得展开:
- 分号切分 + 上限保护:
split_inclusive(";")把一次模糊输入拆成多条语句,模拟真实的多语句请求;max_commands = 500限制语句条数,避免一个畸形输入构造出上万个查询拖垮 fuzz 进程; - 黑名单机制:
sleep/SLEEP被直接拒绝。字典 fuzz/fuzz_targets/fuzz_executor.dict 中同样注释了# Sleep is just going to slow the fuzzer down,二者配合防止 fuzz 输入调用sleep等函数让执行引擎挂起,白白浪费 CPU; - 内存 Datastore:
Datastore::new("memory")依赖 core crate 的kv-mem特性(见 fuzz/Cargo.toml),每次模糊输入都启动一个全新内存库,无磁盘污染、无跨输入状态干扰; - owner 会话:
Session::owner().with_ns("test").with_db("test")模拟拥有全部权限的超级用户会话,确保测试聚焦执行引擎本身而非权限检查; - 单线程 Tokio:
new_current_thread().enable_all()构建当前线程 runtime,block_on包裹全部执行,保证每次 fuzz 迭代串行且可预期; - 忽略结果:
_ignore_the_result = dbs.execute(...)表明目标是"执行不 panic",返回值本身不重要——panic 或超时才是要抓的 bug。
fuzz_structured_executor:直接生成 AST
fuzz/fuzz_targets/fuzz_structured_executor.rs 走的是结构化路线:
fuzz_target!(|query: Ast| { tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { let dbs = Datastore::new("memory").await.unwrap(); let ses = Session::owner().with_ns("test").with_db("test"); _ = black_box(dbs.process(query, &ses, None).await); }) });- 输入类型直接是
surrealdb_core::sql::Ast,由 libFuzzer 的arbitrary-derive从字节流生成合法的 AST 结构; - 跳过
syn::parse阶段,把随机 AST 直接交给dbs.process,从而探测**"结构上合法但语义上致命"**的查询(例如类型不匹配、深层嵌套表达式、非法图遍历),这比字符串变异更容易触达深层执行路径; std::hint::black_box防止编译器把结果视为无用值而整段优化掉,保证执行真实发生。
fuzz_format:格式化往返一致性
fuzz/fuzz_targets/fuzz_format.rs 是一种**性质测试(property test)**式 harness:
fuzz_target!(|query: Ast| { let format = query.to_sql(); let res = surrealdb_core::syn::parse_with_settings( &format.as_bytes(), ParserSettings { object_recursion_limit: 1_000_000, query_recursion_limit: 1_000_000, files_enabled: true, surrealism_enabled: true, ..ParserSettings::default() }, async |parser, stk| parser.parse_query(stk).await, ); if let Err(e) = res { panic!("Failed to parse format\n{e}\n\nSOURCE:\n{format}\nDEBUG:\n{:#?}", query); } });它的契约是:任意 AST →to_sql()序列化 → 重新 parse 必须成功。如果格式化输出连自己的解析器都过不了,说明存在 AST 序列化 bug(如丢括号、运算符优先级丢失)。这里把object_recursion_limit与query_recursion_limit放宽到 100 万、并启用files_enabled与surrealism_enabled,是为了覆盖那些在生产默认配置(settings_from_capabilities 中取自MAX_OBJECT_PARSING_DEPTH/MAX_QUERY_PARSING_DEPTH)下可能被深度限制挡住的合法 AST,确保 round-trip 测试不受解析深度阈值干扰。
字典文件:提升变异的"语法先验"
libFuzzer 的字典(.dict)为变异器提供高价值 token 种子,让随机字节更容易拼接出接近合法 SurrealQL 的片段。仓库维护了两个字典:
- fuzz/fuzz_targets/fuzz_executor.dict:覆盖 SurrealQL 全部关键字(
SELECT/DEFINE/RELATE/SCHEMAFULL等)、运算符(==、!=、??、::、&&、*~等)、以及array::、crypto::、geo::、math::、parse::、rand::、search::、set::、string::、time::、type::、vector::等命名空间下的全部函数签名(如string::distance::levenshtein(、vector::similarity::cosine(); - fuzz/fuzz_targets/fuzz_sql_parser.dict:为纯解析器 harness 准备的同类 token 集。
运行对应 harness 时用-dict=指定(如-dict=fuzz/fuzz_targets/fuzz_executor.dict),可以显著减少变异器在"拼出一个合法关键字"上浪费的迭代次数。注意字典中sleep(被注释掉,与 executor harness 的黑名单逻辑保持一致,避免 fuzz 进程被慢函数拖死。
依赖与特性:Cargo 配置详解
fuzz/Cargo.toml 是理解这套 fuzz 环境的关键:
| 配置项 | 值 | 说明 |
|---|---|---|
[package.metadata] cargo-fuzz = true | — | 标记该 crate 是 cargo-fuzz 项目,cargo fuzz build才会识别 |
libfuzzer-sys | 0.4.7+arbitrary-derive | 提供 fuzz 运行时与Ast结构化输入的 derive 支持 |
tokio | 1.44.2 | harness 内构建异步 runtime 执行查询 |
surrealdb-core | path = "../surrealdb/core",features["kv-mem", "arbitrary"] | 引入内存存储后端(kv-mem)与Ast/Value的 arbitrary 实现(arbitrary) |
surrealdb-types | path = "../surrealdb/types",features["arbitrary"] | 引入类型的 arbitrary 支持,fuzz_format中的ToSql来自该 crate |
[workspace] members = ["."] | — | 声明独立 workspace,防止干扰仓库根目录的主 workspace |
[profile.release] debug = 1 | — | release 构建保留行级调试信息,兼顾速度与崩溃定位 |
两个default-features = false意味着 fuzz crate 只拉取被显式声明的特性,避免引入额外存储后端,缩短编译时间并缩小 fuzz 二进制攻击面。
工作流总结与排障建议
综合 README 与源码,一套完整的 SurrealDB fuzz 工作流如下:
- 初始化:
rustup toolchain install nightly,再cargo +nightly install cargo-fuzz; - 构建:日常跑量用
cargo +nightly fuzz build --fuzz-dir ./ fuzz_executor(-O3);复现崩溃时改用-D关闭优化; - 确认目标:
cargo +nightly fuzz list --fuzz-dir ./核对四个 harness 名称; - 运行:单线程
cargo +nightly fuzz run --fuzz-dir ./ fuzz_executor;提效则追加-- -fork=$(nproc) -dict=fuzz/fuzz_targets/fuzz_executor.dict; - 处置崩溃:libFuzzer 会把触发 panic 的最小输入写入 artifacts 目录,再用
-D构建运行同一输入复现,配合[profile.release] debug = 1保留的调试信息定位栈帧。
常见注意点:
- 所有命令都必须带
+nightly,否则 stable 编译器无法插桩覆盖率; --之后的参数是 libFuzzer 的,不是 cargo-fuzz 的,-fork、-dict、-max_len等均需放在--后;- 若发现 fuzz 进程卡死,可参照 fuzz/fuzz_targets/fuzz_executor.rs 中的 TODO,为每条命令的执行 future 包一层
tokio::time::Timeout或tokio::select!; - 新增 harness 时,在 fuzz/Cargo.toml 追加
[[bin]]段,并在 fuzz/fuzz_targets 下新建#![no_main]的fuzz_target!源文件即可被fuzz list自动发现。
通过这套环境,你可以系统性地对 SurrealQL 的解析与执行引擎做持续攻击面测试——无论是为上游提交崩溃报告,还是在自己的分支上做变更前的回归验证,fuzz 目录都是一份开箱即用的基础设施。
【免费下载链接】surrealdbA scalable, distributed, collaborative, document-graph database, for the realtime web项目地址: https://gitcode.com/GitHub_Trending/su/surrealdb
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考