Tailscale 半年排查,揪出潜伏 16 年的 SQLite 漏洞

一家以网络工具著称的公司,花了半年时间追查自家生产环境里反复出现的数据库损坏,最终定位到一个潜伏了十六年的底层漏洞。这个漏洞不在 Tailscale 自己的代码里,而在几乎所有人都信赖的嵌入式数据库 SQLite 之中。

半年拉锯:控制平面的数据库为何频频损坏

Tailscale 的工程师团队在排查中发现,公司的控制平面(control plane)数据库会间歇性地出现损坏,进而导致服务不稳定。这段时间里,客户和员工都深受其扰,而问题却像捉迷藏一样时隐时现——曾有连续六周的「虚假平静」,让团队一度以为已经修好。

真正的突破口,来自对 SQLite 预写日志(Write-Ahead Log,简称 WAL)机制的一次深入剖析。WAL 是现代 SQLite 在写入数据时采用的模式:新数据先追加到日志文件,再由「检查点」(checkpoint)操作把日志内容正式写回数据库主文件。Tailscale 团队发现,当一次写入事务与一次 WAL 重置(WAL-reset)发生在极短的时间窗口内重叠时,就会触发一个边界情况的竞态条件,进而让索引与数据不同步,最终造成数据库损坏。

漏洞藏在 WAL 重置的瞬间

问题的精确触发条件相当苛刻:一个连接正在执行检查点操作,且这次检查点必须顺利完成;紧接着第二个检查点启动,而在它运行期间,另一个数据库连接提交了一笔事务,把 WAL 文件冲刷一遍并向文件开头写入了新内容。由于数据访问发生冲突,第二个检查点没有意识到 WAL 文件已被那笔事务冲刷过,于是在 WAL 索引头里写入了错误的字段值,误以为部分事务已经提交。后续再有事务提交时,WAL 文件里的页数超过了第一次检查点时的页数,等到第三次检查点发生时,第二步写入的那部分事务被整段跳过,永远没能进入数据库主文件,损坏就此产生。

更令人意外的是,这个漏洞影响范围极广。它存在于自 SQLite 3.7.0(发布于 2010 年 7 月 21 日)起、直到 3.51.2 的所有版本。也就是说,十六年来几乎所有基于 WAL 模式的 SQLite 部署都携带这一隐患。修复先是在 3.51.3 版本(2025 年 3 月)中落地,部分较早版本也拿到了热修复补丁(3.44.6 与 3.50.7)。后续的 3.53.0 版本还新增了自动自愈的索引功能,从机制上防止过期表达式索引再次引发同类问题。

Tailscale 为何偏偏踩中:偏离了「标准路径」

值得玩味的是,绝大多数开发者从未遇到这个问题。SQLite 拥有远超代码量的测试套件,被公认为软件可靠性的标杆。Tailscale 之所以中招,是因为他们以非标准的方式使用了这项「无聊的技术」:团队手动接管了检查点过程,并按照自己激进的节奏来运行,从而踏上了常规配置之外的小径。

整个排查是一场跨职能的苦战,动用了数十人,包括 Tailscale 的工程与客服团队,以及 SQLite 的核心维护者。为了隔离这个竞态条件,Tailscale 还资助了一个开源的 SQLite VFS(虚拟文件系统)中间层工具,帮助在未来追踪类似问题。

等了两个月,才等来那声「令人愉快的警报」

为了拿到生产环境确实发生该竞态的正面证据,Tailscale 在自家的 SQLite 驱动里打了补丁:当写入事务与 WAL 重置重叠时,记录一条警告。随后便是漫长的等待——两周、一月、两月。就在团队开始怀疑补丁是否失效、理论是否出错时,那条等待已久的警报终于触发,证明这个精确条件在真实生产环境中确实会出现。此后,系统又平稳运行了四个月,再无数据库事故。

启示:成熟技术也怕「非常规用法」

这起事件给工程界提了个醒:运行成熟技术时,如果脱离常规配置、以非标准方式使用,风险会被悄然放大。常见的路径和标准配置经过了极其充分的测试,可靠性很高;而一旦为了性能或控制力手动接管底层机制,就必须为那些「几乎不可能发生」的边缘错误做好准备。对依赖 SQLite 存储关键数据的系统而言,定期执行完整性校验、保留可靠的备份与恢复流程,依旧是必要的兜底手段。

来源:Tailscale 工程博客《How we tracked down a 16-year-old SQLite bug》