Databend SQL AST 解析器 AFL 模糊测试实战指南:从 cargo-afl 安装到 fuzz_parse_sql 持续运行
【免费下载链接】databendData Agent Ready Warehouse : One for Analytics, Search, AI, Python Sandbox. — rebuilt from scratch. Unified architecture on your S3.项目地址: https://gitcode.com/GitHub_Trending/da/databend
本文以 Databend 仓库中的 src/query/ast/fuzz/README.md 为核心指南,系统讲解如何使用cargo-afl(American Fuzzy Lop 的 Rust 绑定)对 Databend 的 SQL AST 解析器进行模糊测试(Fuzzing)。你将掌握从工具安装、fuzz target 构建、种子语料准备到持续运行的完整流程,并深入理解 Databend 解析器的底层调用链(tokenize → parse)以及仓库内另外两套模糊测试方案之间的分工,从而能直接在自己的环境下复现并扩展这套解析器安全测试方案。
一、为什么需要对 SQL 解析器做模糊测试
SQL 解析器是数据库前端最容易暴露安全隐患的组件之一:它直接接收来自客户端的任意文本输入,需要经过词法分析(Tokenizer)与语法分析(Parser)两个阶段,任何未覆盖到的边界情况都可能引发 panic、死循环或非预期行为。Databend 的解析器由databend-common-astcrate 提供,位于 src/query/ast,其核心入口定义在 src/query/ast/src/parser/parser.rs,是整个查询引擎的第一道关卡。
模糊测试的思路很直接:持续向解析器投喂随机变异或生成的 SQL 文本,一旦触发崩溃(crash)或超时(hang),说明存在可被利用或需要修复的缺陷。cargo-afl正是这一思路在 Rust 生态中的标准实践,Databend 官方将其作为解析器的模糊测试基础设施,并沉淀在 src/query/ast/fuzz 目录中。
二、环境准备:安装 cargo-afl
原文档给出的安装方式是一条命令:
cargo install cargo-afl说明如下:
cargo-afl是 AFL 模糊器(原版由 Google 的 Michal Zalewski 编写)针对 Rust 的工具链封装,安装后会提供cargo afl build与cargo afl fuzz两个子命令;- 该命令依赖 Rust 工具链可正常访问 crates.io,仓库根目录的 rust-toolchain.toml 定义了 Databend 构建所用的工具链版本,建议在安装前先确认本地 Rust 版本与之匹配;
- AFL 依赖编译器插桩(instrumentation)来实现覆盖率引导,安装完成后可用
cargo afl --help验证是否成功。
三、认识 fuzz target:fuzz_parse_sql
3.1 目录结构与 Cargo 配置
src/query/ast/fuzz 目录包含四个组成部分:
- Cargo.toml:独立的 fuzz crate 清单;
- fuzz_targets/fuzz_parse_sql.rs:本次模糊测试的目标程序;
- in:初始种子语料(seed corpus)目录;
- README.md:使用指南(即本文依据)。
值得注意的一点是 Cargo.toml 中的这段注释:
# cargo can't build fuzz targets with afl # split `fuzz` into separate workspace can help resolve this. # add an empty `[workspace]` table to the package's manifest. [workspace]由于cargo-afl无法直接在 Databend 这样的大型工作区中构建 fuzz target,Databend 将 fuzz 包拆分成独立的 workspace,并在其清单中放置了一个空的[workspace]表来切断与父工作区的关联。Cargo.toml还通过[[bin]]声明了二进制目标:
[[bin]] name = "fuzz_parse_sql" path = "fuzz_targets/fuzz_parse_sql.rs" doctest = false test = true依赖上仅引入了databend-common-ast(复用仓库工作区版本)与afl = "0.12"两个 crate,保持 fuzz 目标足够轻量。
3.2 fuzz target 源码逐行解读
fuzz_targets/fuzz_parse_sql.rs 的核心逻辑非常精简:
#[macro_use] extern crate afl; use databend_common_ast::parser::parse_expr; use databend_common_ast::parser::tokenize_sql; use databend_common_ast::parser::Dialect; fn main() { loop { fuzz!(|text: String| { let tokens = tokenize_sql(&text).unwrap(); let _ = parse_expr(&tokens, Dialect::PostgreSQL); }); } }这段代码体现了三个关键设计:
loop { fuzz!(...) }模式:AFL 的 Rust 绑定通过fuzz!宏注入测试数据并反复回调闭包,外层loop保证每次输入处理完成后立即进入下一轮迭代,最大化吞吐;- 测试管线为「词法 → 表达式解析」:输入文本先经
tokenize_sql切分成 token 序列,再交给parse_expr以Dialect::PostgreSQL模式解析为表达式(Expr)。注意这里使用.unwrap(),意味着一旦 tokenize 阶段遇到无法处理的输入会直接 panic——这正是 fuzz 期望捕获的崩溃信号; - 聚焦表达式而非完整语句:
fuzz_parse_sql调用的是parse_expr而非parse_sql,说明该 target 专门针对表达式(expr)解析路径做压力测试,覆盖算术、比较、函数调用、字面量等高频子语法。
3.3 底层解析调用链印证
在 src/query/ast/src/parser/parser.rs 中可以找到被 fuzz 的两个函数的真实实现:
pub fn tokenize_sql(sql: &str) -> Result<Vec<Token<'_>>> { let mut tokens = Vec::with_capacity((sql.len() / 4).clamp(4, 256)); for token in Tokenizer::new(sql) { tokens.push(token?); } Ok(tokens) } /// Parse udf function into Expr pub fn parse_expr(tokens: &[Token], dialect: Dialect) -> Result<Expr> { run_parser(tokens, dialect, ParseMode::Default, false, expr) }可以看到:
tokenize_sql内部基于Tokenizer逐 token 推进,任何词法错误都会通过?提前返回Err;parse_expr最终落到run_parser(...)与expr解析器,也就是 fuzz 闭包中真正接受考验的组合子。
而 src/query/ast/src/parser/input.rs 定义了 fuzz 中使用的Dialect枚举:
pub enum Dialect { #[default] PostgreSQL, MySQL, Hive, PRQL, Experimental, }不同方言在标识符引号(`或")、字符串引号、默认引号字符等细节上有差异(见同一文件中的is_ident_quote、is_string_quote等实现)。fuzz_parse_sql固定使用默认的PostgreSQL方言,这也意味着 MySQL、Hive 等方言的差异路径暂未纳入本 target——后续若要扩大覆盖面,可以让Dialect参数随 fuzz 输入轮换(如按字节选择方言),这是本 target 最容易扩展的方向。
四、构建 fuzzer
按原文档,进入 fuzz 目录后执行:
cd src/query/ast/fuzz cargo afl build要点说明:
cargo afl build会以 AFL 插桩方式编译当前 crate(即fuzz_parse_sql),产物为target/debug/fuzz_parse_sql;- 因为 fuzz 包已被声明为独立 workspace,命令必须在 src/query/ast/fuzz 目录内执行,而不是在仓库根目录,否则会受父工作区影响而失败;
- 首次构建会同时编译
databend-common-ast及其依赖链,耗时取决于机器性能; - 若机器缺少 gcc/clang 等 C 工具链,AFL 插桩编译可能失败,需要先安装基础编译工具。
五、准备种子语料(Seed Corpus)
模糊测试并非从零开始乱撞,而是以少量合法、有代表性的输入作为种子,再通过变异不断探索新路径。仓库已在 in 目录准备了 3 个种子文件:
| 种子文件 | 内容 | 覆盖点 |
|---|---|---|
| in/1.sql | SELECT count(*) FROM numbers(3); | 聚合函数、*、表函数numbers |
| in/2.sql | SELECT uniq(number % 3, number) FROM numbers(1000); | 多参数函数、取模运算、复合表达式 |
| in/3.sql | COPY INTO ontime200 from @s1 FILES = (...) FILE_FORMAT = (...); | COPY INTO、Stage 引用、结构化FILE_FORMAT参数 |
从源码角度看,这三个种子分别覆盖了「聚合/函数调用」「二元运算符 + 多参函数」「复杂 DDL/DML 语句中的表达式子项」,为 AFL 提供了三个不同的探索起点。你也可以根据实际需求继续向in/目录添加种子文件(例如带字符串转义、注释、嵌套括号的 SQL),遵循「小而多样、尽量合法」的原则,能显著提升变异效率。
六、启动模糊测试
使用fuzz_parse_sqltarget 正式开跑:
cargo afl fuzz -i in -o out target/debug/fuzz_parse_sql各参数含义:
-i in:指定种子语料输入目录(即上文的 in);-o out:指定输出目录,AFL 会把发现的崩溃(out/crashes)、超时(out/hangs)以及队列队列状态写入这里,首次运行时若out不存在会自动创建;target/debug/fuzz_parse_sql:第四步构建出的插桩版 fuzz 程序;- 程序启动后会进入交互式 AFL 状态界面,实时展示执行速度(exec speed)、路径总数(total paths)、崩溃数(crashes)等指标,按
Ctrl-C可随时停止。
启动后的实践建议:
- 观察崩溃:一旦界面出现非零的 crashes 计数,AFL 会自动将复现输入保存到
out/crashes/目录(文件名为触发崩溃的输入内容); - 复现与修复:用保存的崩溃样本直接重放即可稳定复现,例如
target/debug/fuzz_parse_sql < out/crashes/id:...(也可配合cargo afl fuzz的-C崩溃模式);修复解析器后再次运行同一样本验证; - 长时运行:模糊测试的价值随时间累积,建议在 CI 之外的专用机器上长时间运行,并定期归档
out/中的发现; - 超时与内存:如遇复杂输入导致解析超时(hangs),可调整 AFL 的
-t(单次执行超时毫秒数)与-m(内存限制)参数。
七、仓库内的其他模糊测试:分工与互补
除了本文聚焦的 AFL 解析器 fuzz,Databend 仓库还维护着一套基于语法生成(Grammar-based)的模糊测试,两者互补:
- 位置:tests/fuzz/fuzz.py 使用 Python 的
fuzzingbook.Grammars库,以生成式文法构造 SQL 语句,并通过 MySQL 协议(默认127.0.0.1:3307,可用QUERY_MYSQL_HANDLER_HOST、QUERY_MYSQL_HANDLER_PORT、MYSQL_USER等环境变量覆盖)连接正在运行的查询实例执行语句,观察执行错误; - 定位差异:
fuzz_parse_sql是无外部依赖的解析器白盒 fuzz(只在解析层打转,速度快、直接定位解析缺陷);fuzz.py是端到端黑盒 fuzz(覆盖 binder、优化器、执行器等后续链路,但需要起服务); - CI 集成:两者的入口都能在 scripts/ci/ci-run-fuzz-tests.sh 找到,该脚本
cd到tests/fuzz后执行python3 fuzz.py,说明语法生成式 fuzz 已纳入 CI 流水线;而 AFL 解析器 fuzz 因耗时较长,更适合作为独立的持续模糊任务离线运行。
选择建议:日常开发中快速回归解析器可用fuzz_parse_sql(秒级启动、无依赖);做发布前的深度安全验证,可将 AFL 长跑与fuzz.py端到端 fuzz 结合使用。
八、总结
通过本指南,你已经掌握了 Databend SQL 解析器模糊测试的完整闭环:cargo install cargo-afl安装工具 → 进入 src/query/ast/fuzz 独立 workspace 执行cargo afl build→ 使用 in 中的种子语料执行cargo afl fuzz -i in -o out target/debug/fuzz_parse_sql。同时,通过 fuzz_targets/fuzz_parse_sql.rs 的源码,我们印证了其底层调用的是 parser.rs 中的tokenize_sql与parse_expr,以及 input.rs 中的方言枚举。这套方案既可直接复现,也可按需扩展——例如扩充in/种子、轮换Dialect、或参考 tests/fuzz/fuzz.py 将覆盖面推进到执行层,是保障 Databend 解析器健壮性的第一道自动化防线。
【免费下载链接】databendData Agent Ready Warehouse : One for Analytics, Search, AI, Python Sandbox. — rebuilt from scratch. Unified architecture on your S3.项目地址: https://gitcode.com/GitHub_Trending/da/databend
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考