教程·阅读约 3 分钟·
SQLite 藏了 16 年的 WAL-Reset 损坏 bug:检查你的版本,三步升级修复

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 会漏掉一部分已提交事务,数据永久丢失,数据库文件损坏。

具体触发链(官方文档描述):

  1. 连接 A 完成一次完整 checkpoint,WAL 文件处于可重置状态
  2. 连接 B 紧接着开始第二次 checkpoint
  3. 就在第二次 checkpoint 启动的瞬间,连接 C 提交了一个事务,重置了 WAL 文件并在开头写入新内容
  4. 由于数据竞态,第二次 checkpoint 没意识到 WAL 已被重置,在 WAL-Index 头部留下错误的标记——把"还没拷进主库的页"标记成"已拷过"
  5. 后续的事务让 WAL 页数超过第一次 checkpoint 时的数量
  6. 第三次 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 版本

code
sqlite3 --version
# 输出示例:3.45.1 2024-01-30 ...

如果输出版本在 3.7.0 到 3.51.2 之间(且不是 3.44.6 / 3.50.7 的 backport),就需要注意了。

但版本检查只是第一步——真正重要的是你的应用实际链接的 SQLite。现代应用大多不直接用系统 sqlite3 CLI:

  • Pythonpython3 -c "import sqlite3; print(sqlite3.sqlite_version)"
  • Node.jsnode -e "console.log(require('node:sqlite').databaseVersion ?? require('better-sqlite3')().prepare('select sqlite_version()').get())"(取决于你用的库)
  • Go:用 modernc.org/sqlitemattn/go-sqlite3 的话,查看依赖版本
  • 浏览器:所有主流浏览器内置 SQLite(WASM 版),版本随浏览器更新,一般较新

每个语言、每个依赖库都有自己的 SQLite 副本,必须逐个检查

第二步:确认是否真的受影响

满足以下条件才需要担心:

code
-- 在目标数据库上执行,查看日志模式
PRAGMA journal_mode;
-- 输出 wal → WAL 模式,有触发可能
-- 输出 delete → rollback journal,不受影响

再确认是否有多个连接同时访问。判断方法:你的应用里是否有多线程/多进程共用同一个 .db 文件?如果有,且用的是 WAL 模式,就属于受影响场景。

另外一个重要场景:依赖 SQLite 的桌面应用。很多 App 内部用 SQLite 存数据(浏览器书签、聊天记录、各类缓存),你无法控制它们的 SQLite 版本。好消息是:

  • 这个 bug 在真实世界的发生率极低。SQLite 官方原话:"根据遥测,实际发生率不超过 SSD 故障或宇宙射线击中的预期概率"
  • 它不是一个紧急漏洞,而是一个"迟早要升级"的问题

第三步:升级到修复版本

系统级升级

Debian/Ubuntu:

code
sudo apt update
sudo apt upgrade sqlite3 libsqlite3-0

RHEL/Fedora:

code
sudo dnf update sqlite

macOS(Homebrew):

code
brew upgrade sqlite

升级后验证:

code
sqlite3 --version
# 应显示 3.51.3 或更高(或 backport 版本 3.44.6+ / 3.50.7+)

应用依赖升级

  • Python:升级系统 Python 或使用新版 pysqlite3。注意 CPython 的 sqlite3 模块绑定的是编译时的 SQLite 版本,升级 Python 或换 pysqlite3-binary(自带新版 SQLite)
  • Node.jsbetter-sqlite3 升级到捆绑新版 SQLite 的版本;Node 22+ 内置的 node:sqlite 随 Node 版本更新
  • Go:升级 modernc.org/sqlitemattn/go-sqlite3 依赖版本,然后重新编译

第四步:验证数据库完整性

升级后,建议对重要的既有数据库跑一次完整性检查,确认历史数据没有损坏:

code
sqlite3 your.db "PRAGMA integrity_check;"
# 输出 ok 表示完好

如果输出大量 row X missing from index Y 之类的错误,说明数据库可能已经损坏。此时:

  1. 停止所有写入
  2. 用最近一次可信备份恢复
  3. 如果没有备份,尝试 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 分钟能做完的事,别等数据库损坏的那天。

分享到
微博Twitter

© 2026 四月

原文链接:https://www.aprilzz.com/tutorials/sqlite-wal-reset-bug