
SQLite 藏了 16 年的 WAL-Reset 损坏 bug:检查你的版本,三步升级修复
SQLite 3.7.0 到 3.51.2 存在 WAL-Reset 竞态 bug,特定条件下导致数据库静默损坏,Tailscale 半年踩坑 19 次。本文教你检查 SQLite 版本、确认是否受影响,并安全升级到修复版本。
原创。SQLite 从 3.7.0 到 3.51.2 藏着一个存在 16 年的 WAL-Reset 竞态 bug,特定条件下会让数据库静默损坏——Tailscale 半年内踩了 19 次。本文教你 10 分钟检查自己的环境并安全升级。
上周,Tailscale 发布了一篇博客,讲他们如何追查一个让 SQLite 数据库反复损坏的 bug——最后发现根源在 SQLite 内核里,一个存在了 16 年的竞态条件。SQLite 官方将其命名为 WAL-Reset bug,并在 3.51.3 中修复。这篇文章在 Hacker News 上拿到了 500+ 赞,因为几乎所有用 SQLite 做存储的开发者都可能受影响。
这个 bug 到底是什么
简单说:WAL 模式下,当两个连接在极短的时间窗口内同时做"写入 + checkpoint"操作时,checkpoint 会漏掉一部分已提交事务,数据永久丢失,数据库文件损坏。
具体触发链(官方文档描述):
- 连接 A 完成一次完整 checkpoint,WAL 文件处于可重置状态
- 连接 B 紧接着开始第二次 checkpoint
- 就在第二次 checkpoint 启动的瞬间,连接 C 提交了一个事务,重置了 WAL 文件并在开头写入新内容
- 由于数据竞态,第二次 checkpoint 没意识到 WAL 已被重置,在 WAL-Index 头部留下错误的标记——把"还没拷进主库的页"标记成"已拷过"
- 后续的事务让 WAL 页数超过第一次 checkpoint 时的数量
- 第三次 checkpoint 时,跳过步骤 3 提交的事务,这部分数据永远不会写入数据库文件 → 数据库损坏
这个 bug 之所以能藏 16 年,是因为触发条件极其苛刻:需要多个连接、精确的时序碰撞。SQLite 开发者在实验室里都没能自然复现,最后只能往源码里加测试钩子强制触发来验证修复。
—— 广告 ——
影响范围
| 版本 | 状态 |
|---|---|
| 3.7.0(2010-07-21)到 3.51.2(2026-01-09) | ❌ 受影响 |
| 3.51.3(2026-03-13)及之后 | ✅ 已修复 |
| 3.44.6、3.50.7(backport) | ✅ 已修复 |
注意两个前提条件:
- 数据库必须运行在 WAL 模式(
PRAGMA journal_mode=wal) - 同一文件上必须同时有多个连接(多线程或多进程),且正好在"写入 + checkpoint"的瞬间碰撞
单连接使用、或 rollback journal 模式,不会触发。
第一步:检查你的 SQLite 版本
sqlite3 --version
# 输出示例:3.45.1 2024-01-30 ...如果输出版本在 3.7.0 到 3.51.2 之间(且不是 3.44.6 / 3.50.7 的 backport),就需要注意了。
但版本检查只是第一步——真正重要的是你的应用实际链接的 SQLite。现代应用大多不直接用系统 sqlite3 CLI:
- Python:
python3 -c "import sqlite3; print(sqlite3.sqlite_version)" - Node.js:
node -e "console.log(require('node:sqlite').databaseVersion ?? require('better-sqlite3')().prepare('select sqlite_version()').get())"(取决于你用的库) - Go:用
modernc.org/sqlite或mattn/go-sqlite3的话,查看依赖版本 - 浏览器:所有主流浏览器内置 SQLite(WASM 版),版本随浏览器更新,一般较新
每个语言、每个依赖库都有自己的 SQLite 副本,必须逐个检查。
第二步:确认是否真的受影响
满足以下条件才需要担心:
-- 在目标数据库上执行,查看日志模式
PRAGMA journal_mode;
-- 输出 wal → WAL 模式,有触发可能
-- 输出 delete → rollback journal,不受影响再确认是否有多个连接同时访问。判断方法:你的应用里是否有多线程/多进程共用同一个 .db 文件?如果有,且用的是 WAL 模式,就属于受影响场景。
另外一个重要场景:依赖 SQLite 的桌面应用。很多 App 内部用 SQLite 存数据(浏览器书签、聊天记录、各类缓存),你无法控制它们的 SQLite 版本。好消息是:
- 这个 bug 在真实世界的发生率极低。SQLite 官方原话:"根据遥测,实际发生率不超过 SSD 故障或宇宙射线击中的预期概率"
- 它不是一个紧急漏洞,而是一个"迟早要升级"的问题
第三步:升级到修复版本
系统级升级
Debian/Ubuntu:
sudo apt update
sudo apt upgrade sqlite3 libsqlite3-0RHEL/Fedora:
sudo dnf update sqlitemacOS(Homebrew):
brew upgrade sqlite升级后验证:
sqlite3 --version
# 应显示 3.51.3 或更高(或 backport 版本 3.44.6+ / 3.50.7+)应用依赖升级
- Python:升级系统 Python 或使用新版
pysqlite3。注意 CPython 的sqlite3模块绑定的是编译时的 SQLite 版本,升级 Python 或换pysqlite3-binary(自带新版 SQLite) - Node.js:
better-sqlite3升级到捆绑新版 SQLite 的版本;Node 22+ 内置的node:sqlite随 Node 版本更新 - Go:升级
modernc.org/sqlite或mattn/go-sqlite3依赖版本,然后重新编译
第四步:验证数据库完整性
升级后,建议对重要的既有数据库跑一次完整性检查,确认历史数据没有损坏:
sqlite3 your.db "PRAGMA integrity_check;"
# 输出 ok 表示完好如果输出大量 row X missing from index Y 之类的错误,说明数据库可能已经损坏。此时:
- 停止所有写入
- 用最近一次可信备份恢复
- 如果没有备份,尝试
VACUUM INTO 'recovered.db'导出,或从 WAL 文件恢复
顺便说一句:Tailscale 之所以能发现问题,靠的就是定期备份 + 对备份跑 PRAGMA integrity_check。这个习惯值得所有人养成——备份不检查等于没备份。
写在最后
这个 bug 最值得记住的不是技术细节,而是运维教训:
"罕见"不等于"不会发生"。Tailscale 半年内遇到 19 次损坏,SQLite 官方说发生率低于 SSD 故障概率——两者并不矛盾:你在单个库上遇到它的概率极低,但当你管理几百个分片、每个分片都在跑备份检查时,小概率事件也会变成每个月都来的客人。
行动清单:
- 逐个检查所有 SQLite 依赖的版本(系统 + Python + Node + Go + 应用内置)
- 确认哪些数据库跑在 WAL 模式且多连接访问
- 升级到 3.51.3+ 或 backport 版本
- 对关键数据库执行
PRAGMA integrity_check - 建立"备份 + 定期完整性检查"机制
10 分钟能做完的事,别等数据库损坏的那天。
© 2026 四月
原文链接:https://www.aprilzz.com/tutorials/sqlite-wal-reset-bug