SurrealDB 模糊测试实战指南:基于 cargo-fuzz 的 Harness 构建、编译与并行执行
2026/9/11 13:51:44 网站建设 项目流程

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_executorfuzz_sql_parserfuzz_structured_executorfuzz_format四个目标各自的设计意图与底层调用链,并能通过字典文件与-fork并行参数最大化本地 CPU 利用率,从而在自己复现崩溃、回归验证或扩展 Harness 时直接照搬这套工作流。

为什么 SurrealDB 需要模糊测试

SurrealDB 的核心是一个 SQL 方言(SurrealQL)解析器加上一套分布式文档-图数据库执行引擎。任何用户输入(HTTP 查询、WebSocket 请求、导入文件)最终都会进入两条关键路径:

  • 语法解析surrealdb_core::syn::parse将字符串解析为 AST,位于 surrealdb/core/src/syn/mod.rs;
  • 执行引擎:解析后的 AST 交给DatastoreSession执行(如 surrealdb/core/src/dbs/iterator.rs 中的process)。

这两条路径一旦被畸形输入触发 panic、无限递归或栈溢出,就会造成服务拒绝。为此仓库维护了一套由 cargo-fuzz 管理的模糊测试 Harness,全部位于 fuzz 目录,通过覆盖率反馈驱动的 libFuzzer 在运行时自动发现并复现崩溃输入。

四个官方 Fuzz Harness 全景

仓库在 fuzz/Cargo.toml 中声明了四个二进制目标,分别覆盖解析与执行两个层面:

Harness 二进制源码目标对象核心动作
fuzz_sql_parserfuzz/fuzz_targets/fuzz_sql_parser.rs&str原始字符串直接调用syn::parse,验证"不要崩溃"
fuzz_executorfuzz/fuzz_targets/fuzz_executor.rs&str原始字符串分号切分命令后在内存 Datastore 中逐条execute
fuzz_structured_executorfuzz/fuzz_targets/fuzz_structured_executor.rsAst(arbitrary 结构化输入)将任意生成的 AST 直接送入Datastore::process
fuzz_formatfuzz/fuzz_targets/fuzz_format.rsAst(arbitrary 结构化输入)格式化 → 重新解析的往返一致性校验

其中fuzz_executorfuzz_sql_parser使用字节/字符串级输入,能探测分词、解析、语法层面的问题;而fuzz_structured_executorfuzz_format依赖libfuzzer-sysarbitrary-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_parserfuzz_executorfuzz_structured_executorfuzz_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只应返回OkErr,绝不 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;
  • 内存 DatastoreDatastore::new("memory")依赖 core crate 的kv-mem特性(见 fuzz/Cargo.toml),每次模糊输入都启动一个全新内存库,无磁盘污染、无跨输入状态干扰;
  • owner 会话Session::owner().with_ns("test").with_db("test")模拟拥有全部权限的超级用户会话,确保测试聚焦执行引擎本身而非权限检查;
  • 单线程 Tokionew_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_limitquery_recursion_limit放宽到 100 万、并启用files_enabledsurrealism_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-sys0.4.7+arbitrary-derive提供 fuzz 运行时与Ast结构化输入的 derive 支持
tokio1.44.2harness 内构建异步 runtime 执行查询
surrealdb-corepath = "../surrealdb/core",features["kv-mem", "arbitrary"]引入内存存储后端(kv-mem)与Ast/Value的 arbitrary 实现(arbitrary
surrealdb-typespath = "../surrealdb/types",features["arbitrary"]引入类型的 arbitrary 支持,fuzz_format中的ToSql来自该 crate
[workspace] members = ["."]声明独立 workspace,防止干扰仓库根目录的主 workspace
[profile.release] debug = 1release 构建保留行级调试信息,兼顾速度与崩溃定位

两个default-features = false意味着 fuzz crate 只拉取被显式声明的特性,避免引入额外存储后端,缩短编译时间并缩小 fuzz 二进制攻击面。

工作流总结与排障建议

综合 README 与源码,一套完整的 SurrealDB fuzz 工作流如下:

  1. 初始化rustup toolchain install nightly,再cargo +nightly install cargo-fuzz
  2. 构建:日常跑量用cargo +nightly fuzz build --fuzz-dir ./ fuzz_executor-O3);复现崩溃时改用-D关闭优化;
  3. 确认目标cargo +nightly fuzz list --fuzz-dir ./核对四个 harness 名称;
  4. 运行:单线程cargo +nightly fuzz run --fuzz-dir ./ fuzz_executor;提效则追加-- -fork=$(nproc) -dict=fuzz/fuzz_targets/fuzz_executor.dict
  5. 处置崩溃: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::Timeouttokio::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),仅供参考

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

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

立即咨询