拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Turso 差分模糊测试器(Differential Fuzzer)实战指南:以 SQLite 为 Oracle 捕获正确性缺陷

Turso 差分模糊测试器Differential Fuzzer实战指南以 SQLite 为 Oracle 捕获正确性缺陷【免费下载链接】tursoA SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.项目地址: https://gitcode.com/GitHub_Trending/tu/turso差分模糊测试differential fuzzing是数据库工程中最有效的正确性验证手段之一对同一段 SQL让被测引擎与参考引擎分别执行再逐行比对结果任何不一致都是潜在缺陷。本文以 Turso 仓库中的differential-fuzzer工具为主线完整讲解它的定位、运行方式、参数体系、输出产物、错误复现与最小化流程并结合testing/differential-oracle/fuzzer/下的真实源码说明其底层实现帮助你把它直接用于日常的回归测试与 bug 排查。工具是什么用 SQLite 当裁判测 Turso 的正确性Turso 是一个用 Rust 编写的 SQLite 兼容数据库引擎。要验证它“兼容”最直接的办法就是差分把同样一批 SQL 语句同时喂给 Turso 和 SQLite然后比较两边返回的行集、错误行为以及执行后的数据库状态。differential_fuzzer正是为此设计的自动化工具其定位在 testing/differential-oracle/fuzzer/main.rs 的文档注释中写得很清楚This binary runs a differential testing fuzzer that compares Turso results against SQLite for generated SQL statements.它的工作模式大致如下对应 runner.rs 中的run_inner主循环用相同的随机种子同时创建 Turso 与 SQLite 两个内存数据库通过 SQL 生成器按当前 schema 生成一条条 SQL 语句DDL/DML 混合在两边分别执行并由差分 Oracle 比对结果执行 DDL 后重新 introspection 两边的 schema 并校验一致性结束后对两边执行PRAGMA integrity_check任一引擎报损坏即记为失败。与“影子状态shadow state”方案不同该 fuzzer 采用schema introspection实时感知当前数据库结构再基于真实结构生成后续语句避免了状态镜像漂移的问题见 lib.rs 的模块说明。源码位置与整体架构工具全部位于testing/differential-oracle/fuzzer/相关 SQL 生成器在其上层目录。关键文件与职责如下表文件职责main.rsCLI 参数解析、单次运行与 loop 子命令入口runner.rs主仿真循环建库、生成语句、双端执行、schema 校验、状态 dumporacle.rs差分 OracleTurso 与 SQLite 结果比对、子查询 unnesting 不变式schema.rs从两个数据库 introspection 当前 schemagenerate.rs生成器抽象层sql_gen与sql_gen_prop两个后端切换shrink.rs失败语句的自动最小化基于解析器 AST 做语法级化简probe.rsdifferential_probe二进制逐行脚本双端回放工具memory/内存 IO 实现保证仿真的确定性docker-runner/CI/生产环境下的容器化运行器含 GitHub Issue 自动提交与 Slack 通知SQL 生成器本体分两个 cratesql_gen类型状态驱动的生成器与 sql_gen_prop基于 proptest 的生成器fuzzer 通过--generator参数在两者间切换。运行方式与全部参数单次运行在仓库根目录下执行需要 Rust 工具链见 rust-toolchain.toml# 基本运行100 条语句随机种子 cargo run --bin differential_fuzzer # 指定种子保证可复现 cargo run --bin differential_fuzzer -- --seed 12345 # 更多语句并输出详细日志 cargo run --bin differential_fuzzer -- -n 1000 --verbose # 运行结束后保留数据库文件便于事后调试 cargo run --bin differential_fuzzer -- --seed 12345 --keep-filesmain.rs中的参数定义基于 clap derive完整罗列如下cargo run --bin differential_fuzzer -- \ --seed SEED # 确定性随机种子默认随机生成 -n NUM # 生成并执行的语句条数默认 100 -t NUM # 创建的表数量默认 2 -c NUM # 每张表的列数默认 5 -g KIND # SQL 生成器后端sql-gen默认| sql-gen-prop --profile PROFILE # 语句权重画像balanced | ddl | triggers | writes | correlated-subqueries --verbose # 逐条打印 SQL 语句 --keep-files # 把 .db 文件落盘 --coverage # 写出覆盖率报告到 simulator-output/coverage.txt --full-tree # 覆盖率报告使用完整层级树默认简化扁平视图 --mvcc # 启用实验性 MVCC 模式 --window-function-probability F # 每条 SELECT 列生成窗口函数的概率0.0..1.0 --recursive-cte-focus # 聚焦有界递归 CTE 的 SELECT 工作负载需 -g sql-gen-prop其中几个参数值得展开说明--seed种子是复现的钥匙。fuzzer 内部用ChaCha8Rng见runner.rs中Fuzzer::new驱动整个生成与执行流程同一 seed 同一 profile 必然重放同一序列。--profileWeightProfile枚举定义了五种负载画像见 generate.rs每个画像通过修改顶层语句权重来侧重引擎的不同部位Profile侧重方向语句权重特征select/insert/update/delete/DDL 等balanced默认通用混合40/20/30/10少量 DDLddlschema 频繁变更create/drop/alter 表与索引权重显著提高triggers触发器路径create_trigger30写操作频繁触发触发器writes约束与冲突路径insert35、update30、delete20correlated-subqueries相关子查询改写select80所有子查询均带外层列依赖每个画像都会确保 select/insert/update/delete 权重均大于 0generate.rs中有every_profile_can_read_and_write测试守护这一点避免出现空负载画像。--window-function-probability当设置为较大值如0.6时SELECT 列表中的列有较高概率被生成为func(...) OVER (...)窗口函数用于压力测试窗口函数发射管线emit pipeline。设置非零值时生成器会自动切换到WindowFramePolicy::Exclude见generate.rs的new_with_window_weight。--mvcc启用后会在 ATTACH 之后执行PRAGMA journal_mode mvccATTACH 本身在 MVCC 模式下不受支持见runner.rs。注意 MVCC 模式下运行结束时会跳过PRAGMA integrity_check该检查当前不支持 MVCC。--coverage运行结束后生成simulator-output/coverage.txt默认是简化扁平视图加--full-tree可输出完整层级树对应sql_gen::TreeMode见runner.rs的write_coverage_report。循环模式Loop Mode需要长时间持续轰炸时使用loop子命令每次迭代自动换随机种子# 无限循环运行 cargo run --bin differential_fuzzer -- loop # 运行 50 次迭代 cargo run --bin differential_fuzzer -- loop 50loop 模式支持--report path选项指定后不会在第一个失败处停止而是收集所有失败并写出 JSON 报告含每次失败的 seed、错误信息、已执行语句数、oracle 失败数、警告数以及完整配置快照对应main.rs中的FailureRecord/LoopReport结构。报告非空时进程以退出码 1 结束。Docker RunnerCI/生产环境仓库提供现成的容器化运行器适合接入 CI 或常驻服务器# 从仓库根目录构建并运行 docker build -f testing/differential-oracle/fuzzer/docker-runner/Dockerfile -t fuzzer . docker run -e GITHUB_TOKENxxx -e SLACK_WEBHOOK_URLxxx fuzzer从 Dockerfile 可以看到镜像使用 cargo-chef 分层缓存依赖、构建differential_fuzzer二进制运行时以 Bun 执行 TypeScript 入口脚本docker-entrypoint.fuzzer.ts可自动向 GitHub 提交 Issuegithub.ts并发送 Slack 通知slack.ts。运行器支持的环境变量环境变量作用默认值TIME_LIMIT_MINUTES总运行时长上限144024 小时PER_RUN_TIMEOUT_SECONDS单次运行的超时120020 分钟NUM_STATEMENTS每次运行的语句条数1000LOG_TO_STDOUT是否把 fuzzer 输出打到 stdoutfalseGITHUB_TOKEN自动提交 Issue 的令牌无SLACK_WEBHOOK_URL通知用的 Webhook无输出文件说明所有输出统一写入当前工作目录下的simulator-output/文件说明test.sql本次执行的全部 SQL 语句失败的语句以-- FAILED:前缀标记错误语句以-- ERROR:标记另有-- WARNING、-- SKIPPED、-- PANIC等注释标记schema.json运行结束时或失败时的数据库 schema JSON 快照test.dbTurso 数据库文件仅--keep-files时生成test-sqlite.dbSQLite 数据库文件仅--keep-files时生成coverage.txt覆盖率报告仅--coverage时生成minimized.sql自动最小化后的失败复现脚本oracle 失败时生成turso-state.sql/sqlite-state.sql两个引擎在失败时刻的完整状态还原脚本含 main/temp/aux 三库的表、数据、索引、触发器从源码看test.sql由runner.rs的write_sql_file在每次运行结束即使出错时写出turso-state.sql/sqlite-state.sql由dump_failure_state在 oracle 失败时生成——它会从sqlite_master读取所有对象的 DDL并按表逐行生成INSERT INTO ... VALUES(quote(...))语句最后把触发器/索引放在数据之后重建确保脚本自包含可重放。错误复现与最小化六步流程这是整个工具最核心的实战部分。oracle 失败后请严格按以下步骤操作1. 从错误输出中找到 seed 与 profile失败输出会打印一行配置摘要INFO: Starting differential_fuzzer with config: SimConfig { seed: 12345, ..., weight_profile: Writes }seed 只有在同一 profile 下才能重放——profile 改变语句权重分布生成序列就会变。2. 用该 seed 与 profile 重跑cargo run --bin differential_fuzzer -- --seed 12345 --profile writes --verbose --keep-files3. 先读最小化复现文件oracle 失败时fuzzer 会自动向simulator-output/写出多份复现素材minimized.sql自动生成的“缩小版状态脚本 缩小版失败语句”从这里开始排查。turso-state.sql/sqlite-state.sql每个引擎的完整状态还原脚本当最小化版本不足以复现时使用。test.sql全部已执行语句失败语句标记为-- FAILED:。当失败依赖的是“状态是如何构建出来的”而非状态内容本身时最小化器会退回到重放这段历史。schema.json失败时刻的表结构。最小化器shrink.rs的实现值得一提它不是做文本裁剪而是先用解析器把失败语句解析成 AST然后逐个节点化简——删掉一个子句、把某个表达式替换为1或或它的某个子节点——再把树打印回 SQL。因此每个候选语句在语法上都是合法的不会被字面量里的引号或关键字干扰。每次化简只保留“与原失败同类且错误前缀一致”的编辑Divergence枚举区分TursoErr/SqliteErr/ResultMismatch三种分歧类型避免最小化过程漂移到另一个 bug 上单次最小化候选执行数上限为 800 次MAX_CANDIDATES防止病态语句拖垮整个 fuzzing 循环。4. 用 differential_probe 逐行探测differential_probe把“每行一条语句”的脚本在 Turso 与 SQLite 上并排回放打印每条语句在两端的结果、标记分歧点并在最后比较两边最终表内容。任一语句分歧或最终表内容不一致时退出码为 1。cargo run -q -p differential-fuzzer --bin differential_probe -- \ simulator-output/minimized.sql为什么必须用 probe 而不是把 SQL 分别灌进两个 shell因为 tursodb shell 不支持ATTACH :memory: AS aux而 fuzzer 的复现脚本状态 dump中带有auxschema——只有通过 probe 才能原样跑通。probe 的 Turso 连接默认开启了 ATTACHDatabaseOpts::new().with_attach(true)并且脚本要求aux时会自动挂载一个内存库见 probe.rs 的文档注释。probe 也支持从 stdin 读入echo SELECT ~X96; | cargo run -q -p differential-fuzzer --bin differential_probeprobe 对每条语句输出三行line N: 语句、turso : 结果摘要、sqlite: 结果摘要分歧语句额外标出^^^ DIVERGES每行最多截断 160 字符行摘要最多 300 字符便于阅读超长生成语句。5. 手工二分编辑脚本逐步化简复制minimized.sql一次只简化一处把某个表达式换成常量、删掉某一列、删掉某一行状态语句。每次编辑后重跑 probe分歧标记会立刻告诉你这次编辑是否保留了 bug。这个循环通常能收敛到一个单行内核one-line kernel交给两端的EXPLAIN对比执行计划。probe 的输出格式设计得很适合这个流程非--开头的行才会被执行所以你可以把-- FAILING STATEMENT:之后的语句行取消注释来启用它而用--注释掉其他语句。6. 从内核创建回归测试把收敛出的内核写成回归测试优先使用.sqltest格式仓库中sqlite/conformance/下有大量.sqltest用例可参考也可以写成.rs单元测试。编写时始终参考仓库的 Debugging skill 指南。理解失败类型Oracle 的判定逻辑差分 Oracle 的判定逻辑位于 oracle.rs 的DifferentialOracle::check。它把两端执行结果归约为三种形态QueryResultRows(行集)、Ok无行返回、Error(错误信息)然后按以下矩阵判定Oracle 失败类型致命行集不一致Row set mismatch——Turso 返回的行与 SQLite 不同diff_results找出仅在一端出现的行报告中分别列出Only in Turso与Only in SQLite。Turso 报错但 SQLite 成功Turso errored but SQLite succeeded——Turso 拒绝了本应合法的 SQL。SQLite 报错但 Turso 成功SQLite errored but Turso succeeded——Turso 接受了非法 SQL。Schema 不一致Schema mismatch——DDL 之后两边表/列/索引/触发器结构不同。关于第 4 类runner.rs的introspect_and_verify_schemas会在每个 DDL 语句后重新 introspection 两端 schema 并逐项比对表名集合、索引名集合、触发器名 所属表、每张表的 STRICT 标志、列名顺序、索引的目标表/UNIQUE 标志/列集合任何一项不一致都会以Table mismatch/Index mismatch/Trigger mismatch/Column mismatch等明确消息中止。非致命警告无序 LIMIT 不一致Unordered LIMIT mismatch——没有ORDER BY的LIMIT查询两引擎各自返回合法但可能不同的行因此被降级为警告而非失败。生成器会记录has_unordered_limit与原因如limit_order_by_scalar_subquery警告信息以结构化前缀NONDET_LIMIT_WARNING reason... kind... sql_hash...输出对应oracle.rs的format_nondet_limit_warning。额外的执行前校验与不变式检查EXPLAIN 预检生成的 SQL 可能在某个永远不会执行的子句中包含错误SQLite 会先剪掉该分支而 Turso 可能拒绝。为避免“只在一端执行了写操作”污染后续所有比较check_differential会先对两端执行EXPLAIN stmt任一端 prepare 失败则该语句整体标记为Skipped统计为statements_skipped并记录sql_hash便于去重。子查询 unnesting 不变式对于带相关子查询的语句check_unnesting_invariant标志Oracle 会分别在“强制 unnesting”和“禁用 unnesting”两种模式下对比执行计划与结果验证改写前后结果一致PassWithUnnestingInvariant统计项unnesting_invariants_checked。实现细节见oracle.rs的check_subquery_unnesting_invariant。DML 后快照校验对于mutates_data的语句Oracle 还会对 schema 中每张表执行SELECT rowid, * FROM 表 ORDER BY rowid快照比对verify_table_snapshots用于捕获那些“两语句各自结果相同但隐藏的表状态已分叉”的缺陷——oracle.rs 的测试test_check_differential_fails_on_hidden_table_state_mismatch专门验证了这条路径。完整性检查仿真结束时对两端执行PRAGMA integrity_check任一引擎返回值不是ok都记为失败MVCC 模式跳过。生成器的工程化规避为了保证差分结论可靠生成器在generate.rs中主动规避了一批“两边都合法但必然分叉”的构造这些注释本身就是很好的 SQLite/Turso 语义差异教材UPDATE ... OR IGNORE概率置 0多行争抢同一 UNIQUE 值时保留哪一行取决于扫描顺序两端可能不同UPDATE ... FROM仅允许单真实表禁 join、自连接、子查询源Turso 会用临时索引评估 JOIN 而 SQLite 扫描匹配顺序无法对齐当 schema 含触发器或存在 TEMP 表遮蔽同名常驻表时禁用ALTER TABLE的 rename/drop column/rename columnSQLite 会重解析已存索引与触发器Turso 不会导致同一 ALTER 两端判定不一致单条生成 SQL 超过 64 KiBMAX_GENERATED_SQL_BYTES直接跳过深度嵌套表达式会耗尽进程栈main.rs为此把 fuzzer 主线程栈扩到 256 MiB。统计输出解读每次运行结束会打印一张结果表SimStats::to_table见runner.rs包含以下指标指标含义Seed / Target Statements本次种子与目标语句数Statements Executed实际成功执行的语句数Statements Skipped因 EXPLAIN 预检失败或超长 SQL 跳过的语句数Warnings非致命警告数如无序 LIMITOracle Failures致命分歧数非 0 时进程退出码为 1Errors执行错误数Unnesting invariants已校验的子查询 unnesting 不变式数量调试输出RUST_LOG需要更细粒度的日志时通过RUST_LOG环境变量控制main.rs中基于tracing_subscriber初始化默认 INFO 级别stdin 非终端时自动关闭 ANSI 颜色RUST_LOGdebug cargo run --bin differential_fuzzer -- --seed 12345debug级别下可以看到每条语句的生成与执行细节、schema 更新日志、unnesting 计划对比等内部信息子查询 unnesting 相关日志可通过 targetsubquery_unnesting单独过滤。小结differential_fuzzer把“SQLite 兼容”从口号变成了可机械验证的断言随机生成、双端执行、Oracle 判定、自动最小化、probe 回放、回归固化形成一条完整的正确性缺陷发现闭环。无论你是想为 Turso 的某个新特性做差分测试、复现一个历史 bug还是把 fuzzing 接入 CIDocker Runner 已内置 GitHub Issue 自动提交与 Slack 通知都可以直接套用本文的流程。深入理解 oracle.rs 与 runner.rs 的判定细节还能帮你快速判断一个“分歧”究竟是引擎缺陷、生成器构造问题还是 SQL 本身的合法不确定性。【免费下载链接】tursoA SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.项目地址: https://gitcode.com/GitHub_Trending/tu/turso创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门