的 SQL Fuzz 测试实战指南)
Databend 基于文法Grammar的 SQL Fuzz 测试实战指南【免费下载链接】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 仓库中 tests/fuzz/readme.md 为骨架系统讲解该项目如何使用**文法生成Grammar-based Fuzzing**方式对 SQL 引擎进行模糊测试从运行原理、环境准备、执行流程到如何新增自定义文法 fuzzer 的完整四步流程。读完本文你将掌握fuzz.py中mysql_client、QueryGenerator、FuzzRunner等核心组件的实现机制并能够独立为 Databend 编写新的 SQL 文法模糊测试。一、Fuzz 测试的基本原理与设计目标Databend 的 fuzz 测试位于仓库的 tests/fuzz/ 目录其核心思路在 readme.md 中只有一句话但非常关键Fuzz test get sql in given grammar, execute it using mysql client.即按照给定的文法Grammar随机生成 SQL 语句再通过 MySQL 客户端协议把生成的 SQL 交给 Databend 执行以此探测 SQL 引擎在非预期输入下的健壮性。这种测试方式属于「文法驱动的黑盒模糊测试」测试者不关心 SQL 引擎内部实现只关心一条重要判据——凡是 Databend 自身没有返回 SQL 错误、也没有正常返回的用例即为测试失败。它能把语法正确但边界刁钻的 SQL 组合如null与比较运算符混用、多表交叉连接、group by与聚合函数叠加、order by DESC等自动爆炸式地生成出来是传统手写回归用例的有效补充。该测试在 CI 中以 standalone 模式运行相关工作流定义在 .github/actions/test_fuzz_standalone_linux/action.yml对应 reuse.linux.yml 中test_fuzz_standalone任务。二、失败判定条件failed condition在fuzz.py中判定一条 SQL 是否通过验证的逻辑是query_validate()函数def query_validate(result): if result is None: # sql success return True if Code in str(result): # sql error return True return False # other error as a failed test对应 readme 中描述的失败条件返回None表示 SQL 执行成功属于正常结果通过错误信息中包含Code表示这是 Databend 返回的标准 SQL 错误Databend 的异常信息带Code: NNNN格式的错误码属于被引擎正常拒绝的预期行为同样通过其他任何结果例如客户端连接中断、协议异常、超时、进程崩溃等非 SQL 级错误一律判定为测试失败。一旦某个生成的 query 触发失败FuzzRunner.run()会立即抛出ExpansionError(query {} failed.format(query))中止整个测试并打印出这条触发故障的 SQL便于复现定位。这里值得注意测试允许引擎报 SQL 错误但不允许意外失败——这正是模糊测试对查询处理器健壮性的最低要求。三、环境准备与依赖安装按照 readme.md 的要求运行 fuzz 测试需要Python 3安装两个 Python 包fuzzingbook提供文法定义与校验能力与mysql-connectorMySQL 客户端驱动。这两个依赖已固化在 tests/fuzz/requirements.txt 中可直接执行pip3 install fuzzingbook mysql-connector # 或等价地pip3 install -r tests/fuzz/requirements.txt其中fuzzingbook是知名开源书籍《The Fuzzing Book》https://www.fuzzingbook.org配套的 Python 库fuzz.py中引入了它的两个核心设施from fuzzingbook.Grammars import Grammar, is_valid_grammarGrammar字典类型的文法类型标记键为形如start的非终结符值为展开候选列表is_valid_grammar(grammar)用于校验文法是否合法例如非终结符是否有可达的展开、是否存在无法终止的递归等。除了 Python 依赖运行 fuzz 还需要一个正在监听 MySQL 协议端口默认 3307的 DatabendQuery 实例这一点在下一节详述。四、如何运行 Fuzz 测试4.1 一键运行脚本仓库提供了现成的 CI 脚本 scripts/ci/ci-run-fuzz-tests.sh它会自动完成起服务 → 跑 fuzz全流程#!/bin/bash set -e echo Starting standalone DatabendQuery and DatabendMeta ./scripts/ci/deploy/databend-query-standalone.sh SCRIPT_PATH$(cd $(dirname $0) /dev/null 21 pwd) cd $SCRIPT_PATH/../../tests/fuzz || exit echo Starting databend fuzz tests python3 fuzz.py其内部调用的 scripts/ci/deploy/databend-query-standalone.sh 会依次完成清理可能残留的databend-query/databend-meta进程用target/${BUILD_PROFILE}/databend-meta -c scripts/ci/deploy/config/databend-meta-node-1.toml启动 meta 节点并通过python3 scripts/ci/wait_tcp.py --timeout 30 --port 9191等待其就绪用target/${BUILD_PROFILE}/databend-query -c scripts/ci/deploy/config/databend-query-node-1.toml --internal-enable-sandbox-tenant启动 query 节点等待端口 8000HTTP handler就绪。之后才进入tests/fuzz目录执行python3 fuzz.py。4.2 MySQL 协议连接配置fuzz.py中的mysql_client类通过mysql.connector建立连接默认配置如下config { user: root, host: 127.0.0.1, port: 3307, database: default, }其中port 3307与 CI 部署配置一致——在 scripts/ci/deploy/config/databend-query-node-1.toml 中可以看到mysql_handler_host 0.0.0.0、mysql_handler_port 3307。连接参数还支持通过环境变量覆盖方便在集群或多实例环境下运行环境变量覆盖的配置项说明QUERY_MYSQL_HANDLER_HOSThostDatabendQuery 的 MySQL handler 监听地址QUERY_MYSQL_HANDLER_PORTportMySQL handler 端口MYSQL_USERuser连接用户名MYSQL_DATABASEdatabase默认数据库mysql_client.run(sql)执行 SQL 时的行为与失败判定直接相关def run(self, sql): cursor self._connection.cursor(bufferedTrue) try: cursor.execute(sql) return None # 执行成功 → 返回 None except Exception as err: print(sql: {} execute with error: {} .format(sql, str(err))) return err # 抛异常 → 返回错误对象五、Fuzz 的执行流程与核心组件fuzz.py的入口逻辑非常简单if __name__ __main__: f FuzzRunner(generator_list, QueryExecutor()) f.run()整个执行链路由四个类协作完成QueryExecutor ──prepare── 建表 灌入数据 │ QueryGenerator ──next()── grammar_fuzzer 按文法随机展开出 SQL │ FuzzRunner ──run()── 逐个执行 SQL并用 query_validate 校验结果5.1 数据准备prepare_sqls在开始 fuzz 之前QueryExecutor.prepare()会先执行一组固定 SQL见fuzz.py中的prepare_sqls列表构造出 6 张结构一致的表并填充数据t1/t2/t3普通表schema 为(row1 INT, row2 INT NULL, row3 FLOAT, row4 BOOLEAN, row5 VARCHAR, row6 DATE, row7 TIMESTAMP, row8 ARRAY(INT))random_t1/random_t2/random_t3使用ENGINERANDOM的随机引擎表schema 与上面完全一致用于随时生成随机数据源最后通过insert into t1 select * from random_t1 limit 110;等语句为三张普通表各灌入 110 行随机数据。之所以精心设计这张 schema是因为它同时覆盖了INT、NULL可空列、FLOAT、BOOLEAN、VARCHAR、DATE、TIMESTAMP、ARRAY(INT)共 8 种有代表性的数据类型使文法中的任意target_rows组合都能命中不同的类型运算路径。5.2 文法定义select_grammar 与 drop_grammarfuzz.py内置了两套文法。核心的select_grammar是一个最小实现版的 SELECT 文法其设计参照了 Databend 的 SELECT 语法 RFC代码注释中标注了对应 issue #4916select_grammar: Grammar { start: [ SELECT select_list FROM table_reference_list limit_list, SELECT select_list FROM table_reference_list where condition_expression limit_list, SELECT function_reference(target_rows) FROM table_reference_list where condition_expression group by group_by_list limit_list, SELECT function_reference(target_rows) FROM table_reference_list group by group_by_list limit_list, SELECT select_list FROM table_reference_list order by expression ASC limit_list, SELECT select_list FROM table_reference_list order by expression DESC limit_list, ], select_list: [*, select_target, select_target,select_target], select_target: [target_rows], condition_expression: [target_rows expr value], table_reference_list: [ table_reference, table_reference, table_reference, ], function_reference: [sum, avg, count, min, max], group_by_list: [expression, following_expression], limit_list: [limit 1, limit 10, limit 100], following_expression: [expression, expression,expression], expression: [target_rows, target_rows,target_rows], table_reference: [t1, t2, t3], target_rows: [row1, row2, row3, row4, row5, row6, row7, row8], expr: [, , , , !, ], value: [1, 0, null], }可以看到start的 6 条展开候选覆盖了 Databend SELECT 的主要形态基础投影、带where过滤、聚合函数 wheregroup by、聚合函数 group by、order by ASC / DESC。非终结符层层展开后会组合出形如SELECT sum(row3) FROM t1,t2 where row1 null group by row2 limit 10这类语法成立但语义刁钻的语句——尤其是null参与比较运算、ARRAY列参与聚合、两表无连接条件的笛卡尔积等边界场景正是模糊测试最有价值的探测面。另一套drop_grammar则用于测试 DDL 的健壮性drop_grammar: Grammar { start: [drop table drop_option table_reference all_reference], drop_option: [if exists, ], table_reference: [t1, t2, t3], all_reference: [all, ], }两套文法在加载时都会经过合法性断言assert is_valid_grammar(select_grammar)从源头保证文法本身不产生非法展开。5.3 文法展开算法grammar_fuzzerfuzz.py中的grammar_fuzzer()实现了经典的自顶向下随机展开算法代码注释引用了《The Fuzzing Book》的 Grammars 章节从起始符号start开始随机挑选当前串中的一个非终结符从该符号的候选展开中随机选择一个替换它如果替换后非终结符数量超过上限max_nonterminals默认 10则本次替换作废并重试连续max_expansion_trials默认 100次无法展开则抛出ExpansionError避免无限递归直到串中不再含有非终结符返回最终生成的 SQL 文本。QueryGenerator对每次生成的句子还会随机化最大非终结符数量random.randint(5, 10)进一步增大生成 SQL 的形态多样性。5.4 执行与调度QueryExecutor 与 FuzzRunnerQueryExecutor封装mysql_client负责prepare()建表灌数和execute(sql)单条执行FuzzRunner持有 generator 列表与 executor按顺序为每个 generator 循环执行execute_times次每次取一条生成 SQL 执行并调用query_validate()校验一旦失败立即抛出异常终止。fuzz.py末尾的默认调度配置为generator_list [ QueryGenerator(select_grammar, 1000), # 生成 1000 条 SELECT 语句 QueryGenerator(drop_grammar, 10), # 生成 10 条 DROP 语句 ]即默认对 SELECT 文法执行 1000 次、对 DROP 文法执行 10 次。六、如何新增一个文法 Fuzzer四步走这是 readme 给出的核心扩展指南完整对应到fuzz.py的代码结构Step 1定义新文法在fuzz.py中新增一个文法字典例如仿照select_grammarmy_grammar: Grammar { start: [some_statement tail], some_statement: [..., ...], tail: [..., ], }文法必须包含起始符号start所有引用的非终结符都必须有对应的展开规则。Step 2校验文法合法性紧跟文法定义增加assert断言assert is_valid_grammar(my_grammar)is_valid_grammar来自fuzzingbook.Grammars会在启动阶段即时发现文法书写错误避免带着坏文法跑完整轮测试。Step 3注册到 generator 列表并指定执行次数generator_list [ QueryGenerator(select_grammar, 1000), QueryGenerator(drop_grammar, 10), QueryGenerator(my_grammar, 100), # 新增项 ]QueryGenerator(grammar, execute_times)的第二个参数控制该文法生成的 SQL 条数可按测试耗时与覆盖面权衡。Step 4运行python3 fuzz.py此时FuzzRunner会自动遍历包含新文法的generator_list执行全部用例。若新文法依赖新的表或数据形态记得同步扩充prepare_sqls确保被测 SQL 不会因缺表而大面积报错、淹没真正有价值的失败信号。七、与 CI 的集成方式tests/fuzz的用例被定义为独立的 CI 任务。在 .github/actions/test_fuzz_standalone_linux/action.yml 中该 action 会先运行bash ./scripts/setup/dev_setup.sh -yd准备构建环境再执行bash ./scripts/ci/ci-run-fuzz-tests.sh即 4.1 节的一键脚本失败时上传现场工件artifact_failure便于排查。需要说明的是当前仓库的 reuse.linux.yml 中test_fuzz_standalone任务处于注释状态对应代码位置约在 420-432 行且带有timeout-minutes: 10与continue-on-error: true说明该测试目前作为按需/被禁用的任务保留。如果你在本地复现 CI 效果可直接依次执行bash ./scripts/ci/deploy/databend-query-standalone.sh cd tests/fuzz python3 fuzz.py前提是本地已构建好target/debug或target/release通过BUILD_PROFILE环境变量指定下的databend-meta与databend-query二进制且 fuzz 所需的 Python 依赖已按第三节安装。八、小结Databend 的 fuzz 测试tests/fuzz/是一套轻量而完整的文法驱动 SQL 模糊测试方案以fuzzingbook的文法工具生成海量组合 SQL通过 MySQL 协议灌入 Databend 查询引擎并利用非 SQL 错误的异常即失败这一严格判据捕捉引擎的健壮性问题。其核心价值在于覆盖手写用例难以触及的边界组合null比较、类型混用、无连接条件多表查询、聚合与group by叠加等扩展成本极低新增文法只需遵循定义 → 断言 → 注册 → 运行四步即可批量产出新形态的测试语句与 CI 深度集成standalone 模式下一条命令即可完成部署与测试失败时还会打印触发故障的 SQL 原文便于快速复现。对于希望为 Databend 贡献测试能力或验证本地 SQL 引擎健壮性的开发者而言tests/fuzz/fuzz.py 是一个理想的起点读懂它你就能在几分钟内设计出属于自己的 SQL 文法 fuzzer。【免费下载链接】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),仅供参考