用 SQLancer 对 Turso(Limbo)进行数据库模糊测试:从本地脚本到持续化 Bug 检测的完整实战指南
【免费下载链接】tursoA SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.项目地址: https://gitcode.com/GitHub_Trending/tu/turso
Turso(仓库内代号 Limbo)是一个用 Rust 实现的 SQLite 兼容数据库。为了让它在海量随机 SQL 的冲击下依然正确,仓库引入 SQLancer——一款针对 SQL 数据库的自动化模糊测试工具,专门用于挖掘"查询结果不一致"这类逻辑 bug。本篇指南以 testing/sqlancer/README.md 为核心,完整讲解 SQLancer 与 Limbo 的对接方式、本地运行方法、Provider 适配原理,以及仓库内持续化运行 SQLancer 的自动化方案;读完你可以立即在本仓库环境跑起一轮模糊测试,并理解如何扩展测试覆盖面。
SQLancer 是什么,为什么 Limbo 需要它
SQLancer 是一套基于oracle(预言机)思想的数据库模糊测试框架:它不依赖预先写死的"正确结果",而是生成大量随机 SQL 语句,利用多个查询之间的内在等价关系来判断数据库行为是否正确。常见的 oracle 包括:
- NoREC(No Record Comparison):通过聚合函数(如
COUNT、SUM)间接比较不同查询路径的结果,避免直接比较海量行数据; - PQS(Partially Query Synthesis):先从已有结果反推出一条等价查询,再验证其输出是否一致;
- TLP(Ternary Logic Partitioning):把一条查询按
WHERE条件拆分为三段(条件为真 / 为假 / 为 NULL)分别执行,再与整体结果比对。
仓库在 testing/sqlancer/sqlancer-runner/README.md 中明确说明,SQLancer 持续运行的目的是检测 Bug 和数据库损坏(corruption),失败类型被归类为result_mismatch(oracle 检测到查询结果不一致)、corruption(完整性检查失败)、exception(Java 异常)、crash(进程崩溃)、timeout(运行超时)五类。
快速开始:60 秒跑一轮 SQLancer
仓库根目录的 scripts/run-sqlancer.sh 是本地运行 SQLancer 的入口,支持四种命令行参数:
# 默认运行 60 秒(使用 NoREC oracle) ./scripts/run-sqlancer.sh # 指定 oracle 并延长到 5 分钟 ./scripts/run-sqlancer.sh --oracle TLP --timeout 300 # 使用固定随机种子,便于复现某次失败 ./scripts/run-sqlancer.sh --seed 123456789 # 强制重新克隆并构建 SQLancer(当上游代码或补丁发生变更时) ./scripts/run-sqlancer.sh --clean各参数说明(对应 scripts/run-sqlancer.sh 中的注释):
| 参数 | 默认值 | 说明 |
|---|---|---|
--oracle ORACLE | NoREC | 使用的 SQLancer oracle,可选NoREC、PQS、TLP,也可通过环境变量ORACLE设置 |
--timeout SECONDS | 60 | 单次模糊测试的超时秒数,可通过环境变量TIMEOUT设置 |
--seed SEED | 随机 | 传给 SQLancer 的--random-seed,固定后可精确复现失败 |
--clean | - | 删除并重新克隆 SQLancer 工作目录,强制全量重建 |
环境要求
- Java 11 及以上:脚本启动时会先检查
java命令是否存在,不存在则直接报错退出(scripts/run-sqlancer.sh); - Rust 工具链:用于构建 Limbo 的 Java JDBC 驱动原生库;
- Maven:脚本不会强制要求预装——若找不到
mvn,会自动下载 Apache Maven 3.9.6 到/tmp/apache-maven-3.9.6并使用(支持curl或wget两种下载方式,见 scripts/run-sqlancer.sh)。
脚本做了什么:SQLancer 与 Limbo 的完整对接流程
run-sqlancer.sh并非简单地"克隆 SQLancer 再运行",而是一套包含六个步骤的完整流水线(scripts/run-sqlancer.sh),理解它才能明白 Limbo 是如何被 SQLancer 驱动的:
Step 1:按需构建 Limbo 的 JDBC 驱动。脚本读取 bindings/java/gradle.properties 中的projectVersion生成 JAR 路径,并对core/与bindings/java/rs_src/下所有.rs文件、bindings/java/src/下所有.java文件计算 SHA-256 哈希(scripts/run-sqlancer.sh)。若 JAR 缺失、原生库目录缺失或哈希与上次构建不一致,才触发重建:先make <platform>编译原生库,再./gradlew jar打包。这一哈希缓存机制保证了"Rust 源码变了就自动重建,没变就复用"。
Step 2:克隆 SQLancer。工作目录固定为/tmp/sqlancer-limbo,采用git clone --depth 1浅克隆;--clean会先删除该目录。
Step 3:注入 Limbo Provider 并打补丁。将 testing/sqlancer/patches/LimboProvider.java 复制到 SQLancer 源码的src/sqlancer/limbo/目录;同时把 testing/sqlancer/patches/SQLite3Schema.patch 应用到 SQLite 模块的 schema 读取逻辑上(补丁失败时还会用sed进行手工替换兜底)。
Step 4:修改 SQLancer 的pom.xml。在sqlite-jdbc依赖块之后追加 Limbo JDBC 驱动依赖(tech.turso:turso:0.4.0,以systemscope 指向本地构建的 JAR),见 scripts/run-sqlancer.sh。
Step 5:Maven 构建 SQLancer,执行mvn package -DskipTests。
Step 6:运行 SQLancer。关键调用(scripts/run-sqlancer.sh):
java -Djava.library.path="$NATIVE_LIB_DIR" \ -cp "$SQLANCER_JAR:$LIMBO_JAR" \ sqlancer.Main \ --timeout-seconds "$TIMEOUT" \ --num-threads 1 \ --print-progress-summary true \ $SEED_ARGS \ limbo \ --oracle "$ORACLE" \ --test-temp-tables false \ --test-fts false \ --test-rtree false \ --test-check-constraints false \ --test-nulls-first-last false \ --test-generated-columns false \ --test-foreign-keys false注意最后一批--test-* false参数:它们显式关闭了 Limbo 尚未完整支持的功能特性(临时表、全文检索 FTS、RTree、检查约束、NULLS FIRST/LAST、生成列、外键),避免模糊测试生成超出兼容范围的 SQL 而制造噪音。
核心适配层:LimboProvider 的降级与过滤策略
testing/sqlancer/patches/LimboProvider.java 是整个对接方案的核心。它继承 SQLancer 官方的SQLite3Provider,但针对 Limbo 当前兼容水平做了三处关键裁剪:
1. 默认 PRAGMA 精简到一条
// Limbo-compatible pragmas only (cache_size is supported) private static final List<String> DEFAULT_PRAGMAS = Arrays.asList( "PRAGMA cache_size = 50000;" );SQLite 生态常用的各种 PRAGMA 在 Limbo 中尚未全部实现,因此 Provider 只在数据库初始化时执行PRAGMA cache_size = 50000;,其余一律不碰(testing/sqlancer/patches/LimboProvider.java)。
2. 预期错误清单:把"未实现"当预期
private static final List<String> LIMBO_EXPECTED_ERRORS = Arrays.asList( "Not a valid pragma name", "not yet implemented", "not yet supported", "not supported", "not implemented", "unsupported", "Parse error", "TEMPORARY table", "ON CONFLICT", "INSERT OR", "UPDATE OR", "WITHOUT ROWID", "COLLATE", "AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY", "no such table", "cannot rollback - no transaction is active", "cannot commit - no transaction is active", "cannot start a transaction within a transaction", "INDEXED BY", "NOT INDEXED", "UNIQUE constraint failed" );这份清单采用子串匹配(注释中明确说明 partial matches work,见 testing/sqlancer/patches/LimboProvider.java),作用是:当 SQLancer 随机生成的语句触碰到 Limbo 尚未实现的功能时,产生的报错被标记为"预期行为"而非 Bug。所有生成的动作(Action枚举中的查询包装)都会把这份清单与 SQLite 标准错误集合合并后再执行(testing/sqlancer/patches/LimboProvider.java)。维护该清单正是 testing/sqlancer/README.md 中"Updating"一节的核心工作:Limbo 每实现一个功能,就应从清单中删除对应的错误子串,让 SQLancer 开始严格校验它。
3. Action 裁剪:只测 Limbo 已支持的语句类型
Provider 的Action枚举保留了CREATE_INDEX、CREATE_VIEW、CREATE_TABLE、INSERT、DELETE、UPDATE、DROP_INDEX、DROP_TABLE、DROP_VIEW、EXPLAIN以及事务相关的TRANSACTION_START、ROLLBACK_TRANSACTION、COMMIT(testing/sqlancer/patches/LimboProvider.java);同时以注释形式明确禁用了PRAGMA、CREATE_TRIGGER、CREATE_VIRTUALTABLE (FTS)、CREATE_RTREETABLE、VACUUM、REINDEX、ANALYZE、ALTER等动作。mapActions方法(testing/sqlancer/patches/LimboProvider.java)为每个动作分配随机执行次数——例如INSERT在 0 到maxNumberInserts之间随机、UPDATE在 0 到 30 之间随机、CREATE_INDEX在 0 到 5 之间随机,而CREATE_TABLE固定为 0(建表由generateDatabase流程单独驱动)。这些权重决定了随机 SQL 的形态分布。
此外,createDatabase(testing/sqlancer/patches/LimboProvider.java)显式加载tech.turso.JDBC驱动,通过jdbc:turso:<绝对路径>创建数据库文件,getDBMSName()返回"limbo",与命令行中的limbo目标名一一对应。
SQLite3Schema 补丁:绕开 Limbo 不支持的 sqlite_temp_master
SQLancer 官方 SQLite 模块在读取 schema 时会查询sqlite_temp_master系统表(用于枚举临时表和临时索引),而 Limbo 尚未实现它。仓库为此提供了 testing/sqlancer/patches/SQLite3Schema.patch,改动点包括:
- 表/视图查询从
sqlite_master UNION sqlite_temp_master三路联合,改为只查sqlite_master并按name分组(补丁中加注了Modified for Limbo: removed sqlite_temp_master queries (temp tables not supported)); - 索引查询同样删除
sqlite_temp_master的UNION分支。
由于 Limbo 侧已经通过--test-temp-tables false禁用了临时表测试,删掉这两处系统表查询不会损失测试覆盖,却能避免每次建库时都因查询失败而中断。
日志解读:一次运行留下了什么
SQLancer 运行期间,所有执行的 SQL 会按数据库分别写入日志目录:
/tmp/sqlancer-limbo/logs/limbo/每个数据库对应一个日志文件,记录该库执行过的全部语句。运行结束后脚本会自动统计并打印摘要(scripts/run-sqlancer.sh),包括:
- 总语句数(按
CREATE/INSERT/UPDATE/DELETE/SELECT/PRAGMA开头行统计); CREATE TABLE、CREATE INDEX、CREATE VIEW数量;INSERT、UPDATE、DELETE、SELECT数量;- 以
-- 0ms;结尾的失败语句数(Failed (0ms),即执行耗时 0ms 即报错)。
这些日志是定位 Bug 的第一手证据:当某个 oracle 断言失败时,对应的-cur.log文件即包含可复现的最小 SQL 序列,配合--seed固定种子可再次完整复现。
持续化模糊测试:ECS Runner 与自动建 Issue
本地脚本只适合开发期手动验证;仓库还提供了一套可部署在 AWS ECS Fargate 上的持续模糊测试方案,位于 testing/sqlancer/sqlancer-runner/,其核心能力包括:
- 循环运行 SQLancer,目前默认只启用
NoRECoracle(testing/sqlancer/sqlancer-runner/docker-entrypoint.sqlancer.ts),设计上支持多 oracle 轮换; - 每次运行前先随机生成 48 位随机种子并打日志,即使进程崩溃也能凭日志中的 seed 复现(testing/sqlancer/sqlancer-runner/docker-entrypoint.sqlancer.ts);
- 失败时解析输出、判定类型,并通过 GitHub App 自动创建 Issue(带去重,
MAX_OPEN_SQLANCER_ISSUES控制上限);timeout类型因"不是可行动的 Bug"而跳过建 Issue(testing/sqlancer/sqlancer-runner/docker-entrypoint.sqlancer.ts); - 对 corruption / crash / exception 类失败,调用 scripts/corruption-debug-tools/ 下的 Python 工具做损坏分析(WAL 帧统计、损坏帧定位、完整性检查),并把分析结果附进 Issue(testing/sqlancer/sqlancer-runner/corruptionAnalysis.ts);
- 运行结束把汇总统计(总运行次数、建 Issue 数、损坏数、超时数、各 oracle 的 run/failure 分布)发布到 Slack。
Runner 的全部行为由环境变量控制(testing/sqlancer/sqlancer-runner/README.md):
| 环境变量 | 默认值 | 说明 |
|---|---|---|
TIME_LIMIT_MINUTES | 240 | 总运行时长(4 小时) |
PER_RUN_TIMEOUT_SECONDS | 300 | 单次 SQLancer 运行的超时(注:入口脚本中实际默认600) |
SLEEP_BETWEEN_RUNS_SECONDS | 5 | 两轮运行之间的间隔 |
LOG_TO_STDOUT | false | 是否把 SQLancer 输出同步打到 stdout |
GIT_HASH | unknown | 用于 Issue 跟踪的提交哈希 |
GITHUB_APP_ID/GITHUB_APP_PRIVATE_KEY/GITHUB_APP_INSTALLATION_ID | - | GitHub App 认证三元组 |
MAX_OPEN_SQLANCER_ISSUES | 10 | 未关闭 Issue 达到上限后停止运行 |
SLACK_BOT_TOKEN/SLACK_CHANNEL | - | Slack 汇总通知 |
本地调试方式:在sqlancer-runner/下执行bun install后,用 dry-run 模式(只打日志、不发 Issue/Slack)启动:
cd testing/sqlancer/sqlancer-runner bun install TIME_LIMIT_MINUTES=5 LOG_TO_STDOUT=true bun docker-entrypoint.sqlancer.ts容器化构建则通过 testing/sqlancer/sqlancer-runner/Dockerfile.sqlancer 完成——这是一个三阶段构建:先基于cargo-chef规划依赖缓存,再编译 Limbo 的 JDBC 原生库(lib_turso_java.so,目标平台x86_64-unknown-linux-gnu)并打包 JAR,最后在精简运行镜像中装好 Java 11、Bun、SQLite3 CLI 与损坏分析工具,把turso.jar、原生库和 patch 文件一并拷入镜像,以bun docker-entrypoint.sqlancer.ts作为入口。构建与推送由 GitHub Actions 工作流在 main 分支相关文件变更或手动触发时执行。
维护指南:功能实现后如何扩大测试覆盖
testing/sqlancer/README.md 的 "Updating" 一节给出了两条明确的维护路径,都指向编辑 testing/sqlancer/patches/LimboProvider.java:
- 删除
LIMBO_EXPECTED_ERRORS中对应的错误条目:每当 Limbo 实现了某个此前未支持的功能(例如某条 PRAGMA、ON CONFLICT子句、COLLATE或WITHOUT ROWID),就从清单中去掉相关错误子串。此后 SQLancer 不再把它当作"预期错误",会开始用 oracle 严格验证其行为正确性,测试覆盖面随之扩大。 - 向
DEFAULT_PRAGMAS追加新支持的 PRAGMA:每支持一个新的 PRAGMA 就把它加入初始化列表,让随机生成的 SQL 在更接近真实 SQLite 的配置环境下运行。
两条规则背后的逻辑是一致的:Provider 清单反映的是"当前兼容水平",必须随实现进度同步收缩,否则被过滤掉的错误可能掩盖真实回归。
小结
SQLancer 与 Limbo 的对接构成了一套"本地快速验证 + 云端持续轰炸"的完整质量防线:本地通过 scripts/run-sqlancer.sh 一键跑通(构建 JDBC 驱动 → 克隆并打补丁 → Maven 构建 → 以limbo目标执行);Provider 层通过 LimboProvider.java 的 PRAGMA 精简、预期错误过滤与 Action 裁剪,把随机测试严格约束在 Limbo 已支持的功能范围内;ECS Runner 则把这一过程常态化,让每次失败都能自动沉淀为带复现种子和损坏分析报告的 Issue。如果你正在为本仓库贡献新功能,记住这条铁律:实现一个功能后,记得从LIMBO_EXPECTED_ERRORS里删掉对应的"预期错误",SQLancer 才会替你把关它。
【免费下载链接】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),仅供参考