如何破坏SQLite数据库文件

Hacker News Top 工具

摘要

这篇来自SQLite官方文档的文章解释了SQLite数据库可能被损坏的各种方式,例如恶意进程覆盖文件、文件描述符的误用以及不安全的备份程序,同时提供了相应的缓解策略。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/06/30 06:34

# 如何破坏 SQLite 数据库文件 来源:https://www.sqlite.org/howtocorrupt.html 如何破坏 SQLite 数据库文件 目录 ## 概述 SQLite 数据库具有很强的抗损坏能力。如果在事务执行期间发生应用程序崩溃、操作系统崩溃甚至电源故障,下次访问数据库文件时,部分写入的事务应自动回滚。恢复过程完全自动,无需用户或应用程序执行任何操作。 尽管 SQLite 能够抵抗数据库损坏,但并非绝对免疫。本文档描述了 SQLite 数据库可能损坏的多种方式。 ## 1. 恶意线程或进程覆盖文件 SQLite 数据库文件是普通的磁盘文件。这意味着任何进程都可以打开该文件并用垃圾数据覆盖它。SQLite 库无法防御这种情况。 ### 1.1. 文件描述符关闭后继续使用 我们遇到过多次这样的情况:一个文件描述符打开了一个文件,然后该文件描述符被关闭并重新打开到另一个 SQLite 数据库上。之后,某个其他线程继续向旧的文件描述符写入数据,却没有意识到原文件已经关闭。但由于该文件描述符已被 SQLite 重新使用,原本应写入原文件的数据最终覆盖了 SQLite 数据库的部分内容,导致数据库损坏。 这种情况的一个例子大约发生在 2013-08-30,涉及 Fossil DVCS(http://www.fossil-scm.org/)的规范仓库。在该事件中,文件描述符 2(标准错误)在 `sqlite3_open_v2()`(https://www.sqlite.org/c3ref/open.html)之前被错误地关闭(我们怀疑是由 stunnel(http://www.stunnel.org/)所致),因此用于仓库数据库文件的文件描述符是 2。随后,一个应用程序错误导致 `assert()` 语句通过调用 `write(2,...)` 发出错误消息。但由于文件描述符 2 现在连接到一个数据库文件,错误消息覆盖了数据库的一部分。为了防止此类问题,SQLite 3.8.1 版本(https://www.sqlite.org/releaselog/3_8_1.html)(2013-10-17)及更高版本拒绝使用低编号的文件描述符来打开数据库文件。(详见 SQLITE_MINIMUM_FILE_DESCRIPTOR(https://www.sqlite.org/compile.html#minimum_file_descriptor)。) 另一个因使用已关闭文件描述符导致损坏的例子由 Facebook 工程师(https://code.facebook.com/posts/313033472212144/debugging-file-corruption-on-ios/)在 2014-08-12 的一篇博文中报告。 另一个类似错误针对 Fossil(https://fossil-scm.org/)于 2019-07-11 报告。一个文件描述符被打开用于调试输出,但随后被 SQLite 关闭并重新使用。然而,调试逻辑继续向原始文件描述符写入。参见论坛讨论(https://fossil-scm.org/forum/forumpost/c51b9a1169)了解错误报告及修复链接。 ### 1.2. 事务活动期间进行备份或还原 在后台运行自动备份的系统可能会在事务进行中尝试制作 SQLite 数据库文件的备份副本。这样生成的备份副本可能包含部分旧内容和部分新内容,从而导致损坏。 有多种安全的方法可以制作 SQLite 数据库的备份副本——安全意味着它们能生成正确、未损坏的备份。按顺序列举如下: 1. `sqlite3_rsync`(https://www.sqlite.org/rsync.html)实用程序(从 SQLite 3.47.0(2024-10-21)及更高版本开始提供)可以通过 SSH 使用带宽高效的协议复制活动的 SQLite 数据库。 2. `VACUUM INTO`*filename*(https://www.sqlite.org/lang_vacuum.html#vacuuminto)命令将 SQLite 数据库的当前状态复制到一个单独的文件中。 3. 备份 API(https://www.sqlite.org/backup.html)是一个 C 语言接口,可以创建 SQLite 数据库的一致性副本。 上述任何方法即使在活动数据库上也能正常工作。只要在复制过程中没有正在进行的务,复制 SQLite 数据库文件也是安全的。如果上一次写事务失败,那么必须将回滚日志(`-journal` 文件)或预写日志(`-wal` 文件)与数据库文件本身一起复制。 ### 1.3. 删除热日志 SQLite 通常将所有内容存储在一个磁盘文件中。然而,在执行事务期间,用于在崩溃或电源故障后恢复数据库的信息存储在辅助日志文件中。这些日志文件被称为“热日志”(https://www.sqlite.org/fileformat2.html#hotjrnl)。日志文件的名称与原始数据库文件相同,但附加了 `-journal` 或 `-wal` 后缀。 为了从崩溃或电源故障中恢复,SQLite 必须看到这些日志文件。如果在崩溃或电源故障后,热日志文件(https://www.sqlite.org/fileformat2.html#hotjrnl)被移动、删除或重命名,则自动恢复将无法工作,数据库可能会损坏。 该问题的另一种表现形式是:因不一致地使用 8+3 文件名(https://www.sqlite.org/shortnames.html#db83corrupt)而导致的数据库损坏。 ### 1.4. 数据库文件与热日志错误配对 前述例子是一个更普遍问题的特定情况:SQLite 数据库的状态由数据库文件和日志文件共同控制。在静默状态下,日志文件不存在,只有数据库文件重要。但如果日志文件存在,则必须将其与数据库一起保留,以避免损坏。以下操作很可能导致损坏: - 在两个不同的数据库之间交换日志文件。 - 用不同的日志文件覆盖一个日志文件。 - 将日志文件从一个数据库移动到另一个数据库。 - 复制数据库文件时未同时复制其日志。 - 用另一个数据库文件覆盖一个数据库文件,但未删除与原数据库关联的任何热日志。 ## 2. 文件锁定问题 SQLite 使用数据库文件以及预写日志(https://www.sqlite.org/wal.html)或 WAL(https://www.sqlite.org/wal.html)文件上的文件锁来协调并发进程之间的访问。如果没有协调,两个线程或进程可能同时尝试对数据库文件进行不兼容的更改,从而导致数据库损坏。 ### 2.1. 锁定实现损坏或缺失的文件系统 SQLite 依赖于底层文件系统按照文档所述执行锁定。但某些文件系统的锁定逻辑存在缺陷,导致锁的行为并不总是如所述的那样。这在网络文件系统(尤其是 NFS)中尤为突出。如果在锁定原语存在缺陷的文件系统上使用 SQLite,并且两个或多个线程或进程同时尝试访问同一个数据库,则可能导致数据库损坏。 ### 2.2. 单独线程执行 close() 取消 POSIX 建议锁 在 Unix 平台上,SQLite 使用的默认锁定机制是 POSIX 建议锁。不幸的是,POSIX 建议锁存在设计上的缺陷,使其容易被误用和失败。特别是,同一进程中持有 POSIX 建议锁的文件描述符的任何线程都可以使用另一个文件描述符覆盖该锁。一个特别棘手的问题是,`close()` 系统调用会取消进程内所有线程和所有文件描述符对同一文件的所有 POSIX 建议锁。 因此,例如,假设一个多线程进程中有两个或多个线程分别使用不同的 SQLite 数据库连接访问同一个数据库文件。然后第三个线程出现了,想在不使用 SQLite 库的情况下从同一个数据库文件中读取一些内容。也许第三个线程想制作数据库的备份副本。或者第三个线程只是想识别文件类型,因此尝试读取前 16 个字节以确定它是否真的是 SQLite 数据库。无论原因如何,第三个线程执行了 `open()`、`read()` 和 `close()`。人们可能认为这无害。但 `close()` 系统调用导致其他所有线程持有的数据库锁被释放。这些其他线程无法知道它们的锁已被破坏(POSIX 没有提供任何机制来确定这一点),因此它们继续运行,认为自己的锁仍然有效。这可能导致两个或多个线程或进程同时尝试写入数据库,从而造成数据库损坏。 请注意,两个或多个线程使用 SQLite 库访问同一个 SQLite 数据库文件是完全安全的。SQLite 的 Unix 驱动程序了解 POSIX 建议锁的缺陷并对其进行了规避。只有在线程尝试绕过 SQLite 库直接读取数据库文件时,才会出现此问题。 从 SQLite 3.51.0 版本(2025-11-04)开始,SQLite 实现了额外的防御措施,试图避免因 `close()` 破坏锁而导致的问题。这些新防御措施在数据库处于 WAL 模式(https://www.sqlite.org/wal.html)且被多个进程访问时有所帮助。但并非万能药。为避免损坏,开发人员应谨慎,确保在一个或多个数据库连接打开期间(即使在其他线程中),永远不要对 SQLite 数据库文件调用 `close()`。 ### 2.3. 同一应用程序中链接了多个 SQLite 副本 如前一节所述,SQLite 采取了措施来规避 POSIX 建议锁的缺陷。该规避措施的一部分涉及维护一个全局列表(互斥保护)来记录打开的 SQLite 数据库文件。但是,如果同一应用程序中链接了多个 SQLite 副本,则会有多个该全局列表的实例。使用一个 SQLite 库副本打开的数据库连接无法感知使用另一个副本打开的数据库连接,因此无法规避 POSIX 建议锁的缺陷。一个连接上的 `close()` 操作可能会在不知情的情况下清除另一个不同数据库连接上的锁,从而导致数据库损坏。 上述场景听起来不太可能,但 SQLite 开发者知道至少有一款商业产品就存在这个错误。该供应商向 SQLite 开发者寻求帮助,以追踪他们在 Linux 和 Mac 上偶尔遇到的数据库损坏问题。最终问题被追溯到该应用程序链接了两个独立的 SQLite 副本。解决方案是更改应用程序构建流程,仅链接一个 SQLite 副本。 ### 2.4. 两个进程使用不同的锁定协议 在 Unix 平台上,SQLite 使用的默认锁定机制是 POSIX 建议锁,但也有其他选项。通过使用 `sqlite3_open_v2()`(https://www.sqlite.org/c3ref/open.html)接口选择替代的 `sqlite3_vfs`(https://www.sqlite.org/c3ref/vfs.html),应用程序可以利用其他可能更适合某些文件系统的锁定协议。例如,在必须运行于不支持 POSIX 建议锁的 NFS 文件系统上的应用程序中,可以选择点文件锁定。 确保对同一数据库文件的所有连接使用相同的锁定协议至关重要。如果一个应用程序使用 POSIX 建议锁,而另一个应用程序使用点文件锁定,那么这两个应用程序将无法看到对方的锁,从而无法协调数据库访问,可能导致数据库损坏。 ### 2.5. 数据库文件在使用中时被取消链接或重命名 如果两个进程对同一个数据库文件都有打开的连接,其中一个进程关闭其连接,取消链接该文件,然后在其位置创建一个同名的新数据库文件并重新打开新文件,那么这两个进程将使用同一个名称但实际上是不同的数据库文件。(注意:这仅可能在 Posix 及类似 Posix 的系统上发生,这些系统允许在文件仍处于读写打开状态时取消链接。Windows 不允许这种情况。)由于回滚日志和 WAL 文件基于数据库文件的名称,这两个不同的数据库文件将共享同一个回滚日志或 WAL 文件。其中一个数据库的回滚或恢复可能会使用另一个数据库的内容,从而导致损坏。如果数据库文件在打开时被重命名,并用旧名称创建了新文件,也会出现类似问题。 换句话说,对已打开的数据库文件进行取消链接或重命名操作会导致未定义且可能不希望出现的行为。 从 SQLite 3.7.17 版本(https://www.sqlite.org/releaselog/3_7_17.html)(2013-05-20)开始,如果数据库文件在使用中被取消链接,Unix 操作系统接口会向错误日志(https://www.sqlite.org/errlog.html)发送 SQLITE_WARNING 消息。 ### 2.6. 同一文件的多个链接 如果一个数据库文件有多个链接(无论是硬链接还是软链接),这相当于说该文件有多个名称。如果两个或多个进程使用不同的名称打开数据库,它们将使用不同的回滚日志和 WAL 文件。这意味着,如果一个进程崩溃,另一个进程将无法恢复正在进行的事务,因为它在错误的位置查找相应的日志。 换句话说,打开并使用具有两个或多个名称的数据库文件会导致未定义且可能不希望出现的行为。 从 SQLite 3.7.17 版本(https://www.sqlite.org/releaselog/3_7_17.html)(2013-05-20)开始,如果数据库文件有多个硬链接,Unix 操作系统接口会向错误日志(https://www.sqlite.org/errlog.html)发送 SQLITE_WARNING 消息。 从 SQLite 3.10.0 版本(https://www.sqlite.org/releaselog/3_10_0.html)(2016-01-06)开始,Unix 操作系统接口会尝试解析符号链接,并通过规范名称打开数据库文件。在 3.10.0 版本之前,通过符号链接打开数据库文件类似于打开具有多个硬链接的数据库文件,会导致未定义行为。 ### 2.7. 在 fork() 后保持打开的数据库连接 不要在打开 SQLite 数据库连接后执行 `fork()`,然后在子进程中尝试使用该数据库连接。这会导致各种锁定问题,很容易造成数据库损坏。SQLite 的设计不支持此类行为。在子进程中使用的任何数据库连接都必须在子进程中打开,而不是从父进程继承。 甚至不要对从父进程打开的数据库连接在子进程中调用 `sqlite3_close()`(https://www.sqlite.org/c3ref/close.html)。关闭底层文件描述符是安全的,但 `sqlite3_close()`(https://www.sqlite.org/c3ref/close.html)接口可能会触发清理活动,从而删除父进程中的内容,导致错误甚至数据库损坏。 ## 3. 未同步 为了保证数据库文件始终一致,SQLite 偶尔会要求操作系统将所有待写入的数据刷新到持久存储,并等待刷新完成。这在 Unix 下通过 `fsync()` 系统调用实现,在 Windows 下通过 `FlushFileBuffers()` 实现。我们将这种待写入数据的刷新称为“同步”。

相似文章

用细齿梳检查 SQLite

Lobsters Hottest

John Regehr 描述了使用 tis-interpreter 在 SQLite 中搜索未定义行为,发现了其他工具遗漏的错误,如悬空指针使用和未初始化读取。

故意破坏ZFS文件

Hacker News Top

一篇技术指南,介绍如何使用'zinject'工具或手动字节级编辑故意破坏ZFS文件,以了解ZFS的错误处理和自愈行为。