How To Corrupt An SQLite Database File
如何破坏 SQLite 数据库文件
1.由非法线程或进程覆盖文件
1.1.在关闭后继续使用文件描述符
1.2.在事务处于活动状态时进行备份或恢复
1.3。删除热日志
2.文件锁定问题
2.1。具有损坏或缺少锁定实现的文件系统
2.2。Posix咨询锁被一个单独的线程取消close()
2.2.1。链接到同一应用程序的SQLite的多个副本
2.3。两个进程使用不同的锁定协议
2.4。在使用时取消链接或重命名数据库文件
2.5。多个链接到同一个文件
3.未能同步
3.1。不遵守同步请求的磁盘驱动器
3.2。使用PRAGMA禁用同步
4.磁盘驱动器和闪存故障
4.1。非功率安全的闪存控制器
4.2。假容量USB棒
5.内存损坏
6.其他操作系统问题
6.1。Linux线程
6.2。QNX上的mmap()失败
6.3。文件系统损坏
7. SQLite配置错误
8. SQLite中的错误
8.1。由于数据库收缩造成的虚假腐败报告
8.2。在回滚和WAL模式之间切换后的损坏
8.3。获取锁定时的I / O会导致腐败
8.4。数据库页面从免费页面列表中泄漏
8.5。3.6和3.7交替写入后的腐败。
8.6。在Windows系统恢复竞争条件。
概观
SQLite数据库高度抵抗腐败。如果应用程序崩溃或操作系统崩溃,甚至在事务中间发生电源故障,那么在下次访问数据库文件时应该自动回滚部分写入的事务。恢复过程是完全自动的,不需要用户或应用程序的任何操作。
尽管SQLite能抵抗数据库损坏,但它并不是免疫的。本文档描述了SQLite数据库可能损坏的各种方式。
1.由非法线程或进程覆盖文件
SQLite数据库文件是普通的磁盘文件。这意味着任何进程都可以打开文件并用垃圾覆盖文件。SQLite库没有什么能够做到这一点。
1.1。在关闭后继续使用文件描述符
我们已经看到多个文件描述符在文件上打开的情况,然后文件描述符被关闭并在SQLite数据库上重新打开。后来,其他一些线程继续写入旧的文件描述符,没有意识到原始文件已经关闭。但是由于文件描述符已经被SQLite重新打开,原本打算写入原始文件的信息最终会覆盖部分SQLite数据库,导致数据库损坏。
其中一个例子是2013-08-30在Fossil DVCS的规范仓库中发生的。在这种情况下,在sqlite3_open_v2()之前,文件描述符2(标准错误)被错误地关闭(我们怀疑是stunnel),因此用于存储库数据库文件的文件描述符为2.稍后,应用程序错误导致断言)语句通过调用write(2,...)发出错误消息。但由于文件描述符2现在已连接到数据库文件,因此错误消息会覆盖部分数据库。为了防范这种问题,SQLite 版本3.8.1(2013-10-17)和稍后拒绝使用低编号的文件描述符来处理数据库文件。(有关更多信息,请参阅SQLITE_MINIMUM_FILE_DESCRIPTOR。)
Facebook工程师在2014-08-12的博文中报道了另一个使用封闭文件描述符导致的腐败案例。
1.2。在事务处于活动状态时进行备份或恢复
在后台运行自动备份的系统可能会尝试在事务处理过程中创建SQLite数据库文件的备份副本。然后备份副本可能包含一些旧的和一些新的内容,从而被损坏。
制作SQLite数据库的可靠备份副本的最佳方法是利用作为SQLite库一部分的备份API。否则,只要没有任何进程正在进行事务,就可以安全地创建SQLite数据库文件的副本。如果以前的事务失败,那么任何回滚日志(*-journal
文件)或预写日志(*-wal
文件)都与数据库文件本身一起复制非常重要。
1.3。删除热日志
SQLite通常将所有内容存储在单个磁盘文件中。但是,在执行事务时,在崩溃或电源故障后回滚该事务所需的信息将存储在辅助日志文件中。这些日志文件与原始数据库文件具有相同的名称,但添加了后缀-journal
或-wal
后缀。
SQLite必须看到日志文件才能从崩溃或电源故障中恢复。如果日志文件在崩溃或电源故障后被移动,删除或重命名,则自动恢复将不起作用,并且数据库可能会损坏。
这个问题的另一个表现是由于8 + 3文件名不一致而导致的数据库损坏。
2.文件锁定问题
SQLite使用数据库文件上的文件锁定以及预写日志或WAL文件来协调并发进程之间的访问。如果不协调,两个线程或进程可能会尝试同时对数据库文件进行不兼容的更改,从而导致数据库损坏。
2.1。具有损坏或缺少锁定实现的文件系统
SQLite依赖于底层文件系统来进行锁定,正如文档所述。但是一些文件系统在其锁定逻辑中包含错误,使得锁不总是按照所宣称的那样工作。网络文件系统和NFS尤其如此。如果在锁定原语包含错误的文件系统上使用SQLite,并且两个或多个线程或进程尝试同时访问同一数据库,则可能导致数据库损坏。
2.2。Posix咨询锁被一个单独的线程取消close()
SQLite在unix平台上使用的默认锁定机制是POSIX通告锁定。不幸的是,POSIX咨询锁定的设计怪癖使得它容易被滥用和失败。特别是,任何具有带有POSIX顾问锁的文件描述符的进程都可以使用不同的文件描述符来覆盖该锁。一个特别有害的问题是,close()
系统调用将取消所有线程和进程中所有文件描述符在同一文件上的所有POSIX顾问锁。
所以,举个例子,假设一个多线程进程有两个或多个线程,并且有单独的SQLite数据库连接到同一个数据库文件。然后第三个线程出现,并且想要从它自己的同一个数据库文件中读取某些内容,而不使用SQLite库。第三个线程做一个open()
,一个read()
然后一个close()
。有人会认为这将是无害的。但是close()
系统调用导致所有其他线程在数据库上保留的锁被丢弃。那些其他线程无法知道它们的锁刚刚被丢弃(POSIX没有提供任何机制来确定这一点),所以它们在假设它们的锁仍然有效的情况下继续运行。这可能会导致两个或更多线程或进程尝试同时写入数据库,从而导致数据库损坏。
请注意,两个或多个线程使用SQLite库访问相同的SQLite数据库文件是完全安全的。SQLite的unix驱动程序了解POSIX咨询锁定怪癖并解决它们。只有当线程试图绕过SQLite库并直接读取数据库文件时才会出现此问题。
2.2.1。链接到同一应用程序的SQLite的多个副本
正如前一段所指出的,SQLite需要采取措施解决POSIX咨询锁定的问题。该解决方案的一部分涉及保持开放式SQLite数据库文件的全局列表(互斥体保护)。但是,如果SQLite的多个副本链接到同一个应用程序中,则会有多个此全局列表的实例。使用SQLite库的一个副本打开的数据库连接将不会意识到使用另一个副本打开的数据库连接,并且将无法解决POSIX咨询锁定怪癖。close()
对一个连接的操作可能会在不知不觉中清除不同数据库连接上的锁,导致数据库损坏。
上面的情况听起来很牵强。但是SQLite开发人员知道至少有一个商业产品正是与这个bug一起发布的。该供应商来到SQLite开发人员寻求帮助,以追踪他们在Linux和Mac上看到的一些罕见的数据库损坏问题。问题最终被追溯到应用程序正在链接两个独立的SQLite副本。解决方案是将应用程序构建过程更改为仅与SQLite的一个副本链接而不是两个。
2.3。两个进程使用不同的锁定协议
SQLite在unix平台上使用的默认锁定机制是POSIX顾问锁定,但还有其他选项。通过使用sqlite3_open_v2()接口选择替代sqlite3_vfs,应用程序可以使用其他更适合某些文件系统的锁定协议。例如,点文件锁定可能选择用于必须在不支持POSIX通告锁定的NFS文件系统上运行的应用程序。
与同一个数据库文件的所有连接使用相同的锁定协议是很重要的。如果一个应用程序正在使用POSIX顾问锁并且另一个应用程序正在使用点文件锁定,那么这两个应用程序将不会看到彼此的锁并且将无法协调数据库访问,可能会导致数据库损坏。
2.4。在使用时取消链接或重命名数据库文件
如果两个进程对同一个数据库文件有开放连接,并且一个进程关闭其连接,则断开文件链接,然后在相同位置创建一个具有相同名称的新数据库文件,并重新打开新文件,然后这两个进程将与不同的进程交谈具有相同名称的数据库文件。(请注意,这仅适用于Posix和类似Posix的系统,它允许文件在读写时仍处于打开状态时取消链接,Windows不允许这样做)。由于回滚日志和WAL文件基于数据库文件的名称,这两个不同的数据库文件将共享相同的回滚日志或WAL文件。其中一个数据库的回滚或恢复可能会使用来自其他数据库的内容,从而导致损坏。
换句话说,取消链接或重命名打开的数据库文件会导致未定义的行为,并且可能不合需要。
从SQLite 版本3.7.17(2013-05-20)开始,如果数据库文件在仍处于使用状态时未链接,则unix OS接口将向错误日志发送SQLITE_WARNING消息。
2.5。多个链接到同一个文件
如果单个数据库文件具有多个链接(硬链接或软链接),那么这就是说文件具有多个名称的另一种方式。如果两个或多个进程使用不同的名称打开数据库,则它们将使用不同的回滚日志和WAL文件。这意味着,如果一个进程崩溃,另一个进程将无法恢复正在进行的事务,因为它将在错误的地方查找适当的日志。
换句话说,打开并使用具有两个或更多名称的数据库文件会导致未定义的行为,并且可能不合需要。
从SQLite 版本3.7.17(2013-05-20)开始,如果数据库文件具有多个硬链接,则unix OS接口将向错误日志发送SQLITE_WARNING消息。
从SQLite 版本3.10.0(2016-01-06)开始,unix操作系统接口将尝试解析符号链接并通过其规范名称打开数据库文件。在版本3.10.0之前,通过符号链接打开数据库文件类似于打开具有多个硬链接并导致未定义行为的数据库文件。
3.未能同步
为了保证数据库文件始终保持一致,SQLite偶尔会要求操作系统将所有挂起的写入刷新到持久存储,然后等待该刷新完成。这是通过fsync()
unix和FlushFileBuffers()
Windows 下的系统调用完成的。我们把这个同步写入的同步称为“同步”。
实际上,如果只关心原子性和一致性写入,并且愿意放弃持久写入,则同步操作不需要等到内容完全存储在持久性媒体上。相反,同步操作可以被认为是I / O障碍。只要同步之前发生的所有写入在同步之后发生任何写入之前完成,就不会发生数据库损坏。如果同步作为I / O屏障而不是真正的同步操作,则电源故障或系统崩溃可能会导致一个或多个先前提交的事务回滚(违反“ACID”的“持久”属性),但数据库至少会继续保持一致,这是大多数人关心的。
3.1。不遵守同步请求的磁盘驱动器
不幸的是,大多数消费级海量存储设备都与同步有关。只要磁盘驱动器到达轨道缓冲区并在实际写入氧化物之前,磁盘驱动器就会报告内容在安全媒体上安全存储。这使得磁盘驱动器似乎运行得更快(这对制造商来说非常重要,以便他们可以在贸易杂志中显示出好的基准数字)。公平地说,只要在轨道缓冲区实际写入氧化物之前没有功率损失或硬重置,通常就不会造成伤害。但是如果发生掉电或硬重置,并且如果这导致在同步到达氧化物之后写入的内容,而在同步之前写入的内容仍处于轨道缓冲区中,则可能发生数据库损坏。
USB闪存棒似乎对同步请求特别有害。通过向USB记忆棒上的SQLite数据库提交大量事务,可以很容易地看到这一点。COMMIT命令会比较快地返回,表示记忆棒告诉操作系统并且操作系统告诉SQLite所有内容都安全地存储在持久存储器中,但记忆棒末端的LED将继续闪烁几次更多秒。在LED仍然闪烁时拔出记忆棒通常会导致数据库损坏。
请注意,SQLite必须相信操作系统和硬件会告诉它有关同步请求的状态。SQLite没有办法检测到是否在撒谎,写入操作可能是乱序发生的。但是,WAL模式下的SQLite对乱序写操作的依赖远远大于默认回滚日志模式。在WAL模式下,在检查点操作期间,唯一一次失败的同步操作可能导致数据库损坏。COMMIT期间的同步失败可能会导致失去耐久性,但不会损坏数据库文件。因此,针对由于失败的同步操作导致数据库损坏的一道防线是在WAL模式下使用SQLite并尽可能不频繁地使用检查点。
3.2。使用PRAGMA禁用同步
SQLite执行的帮助确保完整性的同步操作可以在运行时使用同步附注禁用。通过设置PRAGMA synchronous = OFF,所有的同步操作都被省略。这使得SQLite似乎运行得更快,但它也允许操作系统自由地重新排序写入,如果在所有内容到达持久性存储之前发生电源故障或硬重置,则可能导致数据库损坏。
为了获得最大的可靠性和针对数据库损坏的健壮性,SQLite应该始终以默认的同步设置FULL运行。
4.磁盘驱动器和闪存故障
如果文件内容由于磁盘驱动器或闪存故障而更改,SQLite数据库可能会损坏。这是非常罕见的,但磁盘偶尔会在扇区中间翻转一点。
4.1。非功率安全的闪存控制器
我们被告知,在一些闪存控制器中,如果在写入过程中断电,磨损平衡逻辑会导致随机文件系统的损坏。例如,这可以表现为在断电时甚至没有打开的文件中间的随机变化。因此,例如,当发生断电时,设备会将内容写入闪存中的MP3文件,并且可能导致SQLite数据库被损坏,即使数据库在断电时尚未被使用。
4.2。假容量USB棒
有很多盗版的U盘在报告中有高容量(例如:8GB),但实际上只能存储少量(例如1GB)。尝试在这些设备上写入通常会导致不相关的文件被覆盖。因此,使用欺诈闪存设备很容易导致数据库损坏。诸如“假容量usb”等互联网搜索将会引发大量令人不安的关于这个问题的信息。
5.内存损坏
SQLite是一个C库,与其提供的应用程序在相同的地址空间中运行。这意味着应用程序中的散列指针,缓冲区溢出,堆损坏或其他故障可能会损坏内部SQLite数据结构并最终导致数据库文件损坏。通常,这些类型的问题在发生任何数据库损坏之前都会表现为段错误,但是也存在应用程序代码错误导致SQLite巧妙故障以损坏数据库文件而不是恐慌的情况。
使用内存映射I / O时,内存损坏问题变得更加严重。当全部或部分数据库文件映射到应用程序的地址空间时,覆盖该映射空间的任何部分的零散指针将立即破坏数据库文件,而不需要应用程序执行后续write()系统调用。
6.其他操作系统问题
有时候操作系统会出现可能导致问题的非标准行为。有时候这种非标准的行为是故意的,有时候这是实施过程中的错误。但无论如何,如果操作的执行方式与SQLite希望执行的方式不同,则存在数据库损坏的可能性。
6.1。Linux线程
一些较早版本的Linux使用LinuxThreads库进行线程支持。LinuxThreads与Pthreads类似,但在处理POSIX顾问锁方面略有不同。SQLite版本2.2.3到3.6.23认识到LinuxThreads在运行时被使用,并采取适当的行动来解决LinuxThreads的非标准行为。但是大多数现代Linux实现都使用更新,更正确的Pthreads的NPTL实现。从SQLite 版本3.7.0(2010-07-21)开始,假定使用NPTL。没有进行检查。因此,如果在使用LinuxThreads的旧版Linux系统上运行的多线程应用程序中使用SQLite,则最新版本的SQLite将发生微妙的故障并可能损坏数据库文件。
6.2。QNX上的mmap()失败
QNX上的mmap()存在一些微妙的问题,使得针对单个文件描述符进行第二次mmap()调用可能导致从第一次mmap()调用获得的内存归零。unix上的SQLite使用mmap()为WAL模式下的事务协调创建一个共享内存区域,它将多次调用mmap()来处理大型事务。QNX mmap()已被证明在该场景下损坏数据库文件。QNX工程师意识到这个问题并正在研究解决方案; 在阅读本文时,问题可能已经得到解决。
在QNX上运行时,建议不要使用内存映射I / O。此外,要使用WAL模式,建议应用程序使用独占锁定模式以便使用无共享内存的WAL。
6.3。文件系统损坏
由于SQLite数据库是普通的磁盘文件,文件系统中的任何故障都可能损坏数据库。现代操作系统中的文件系统非常可靠,但仍然存在错误。例如,2013年10月1日,在主机计算机转移到在文件系统层出现问题的(linux)内核的恶意构建后几天,保存Tcl / Tk维基的SQLite数据库就遭到破坏。在这种情况下,文件系统最终变得非常糟糕,机器无法使用,但最早出现问题的症状是损坏的SQLite数据库。
7. SQLite配置错误
SQLite有许多内置的防止数据库损坏的保护。但许多这些保护可以通过配置选项禁用。如果保护功能被禁用,则可能会发生数据库损坏。
以下是禁用 SQLite 的内置保护机制的示例:
- 如果存在操作系统异常或电源故障,将 PRAGMA synchronous = OFF 设置可能会导致数据库损坏,尽管此设置可避免由于应用程序崩溃造成的损坏。
- 在打开其他数据库连接时更改PRAGMA schema_version。
- 使用PRAGMA journal_mode = OFF或PRAGMA journal_mode = MEMORY并在写入事务处理过程中发生应用程序崩溃。
- 设置PRAGMA writable_schema = ON,然后使用DML语句更改数据库模式可能会导致数据库完全不可读,如果不谨慎的话。
8. SQLite中的错误
SQLite经过了非常仔细的测试,以确保它尽可能没有bug。在为每个SQLite版本执行的许多测试中,都会进行模拟电源故障,I / O错误和内存不足(OOM)错误的测试,并验证在这些事件中是否发生数据库损坏。SQLite也经过现场验证,约有20亿活动部署,没有严重问题。
尽管如此,没有任何软件是完美的。SQLite中存在一些可能导致数据库损坏的历史错误(现在已修复)。可能还有一些未被发现的东西。由于SQLite的广泛测试和广泛使用,导致数据库损坏的错误往往非常模糊。应用程序遇到SQLite错误的可能性很小。为了说明这一点,下面给出了从2009-04-01到2013-04-15四年期间在SQLite中发现的所有数据库损坏错误的帐户。这个帐户应该让读者直观地了解SQLite中的各种错误,这些错误可以通过测试程序并将其发布到发行版中。
8.1。由于数据库收缩造成的虚假腐败报告
如果数据库是由SQLite版本3.7.0或更高版本编写的,然后再由SQLite版本3.6.23或更早的版本写入,以便减小数据库文件的大小,那么下次SQLite版本3.7.0访问数据库文件时,它可能会报告数据库文件已损坏。但是,数据库文件并不真正损坏。3.7.0版本的腐败检测过于热情。
问题在2011年2月20日解决。该修补程序首先出现在SQLite 版本3.7.6(2011-04-12)中。
8.2。在回滚和WAL模式之间切换后的损坏
在一个进程或线程中反复切换进出WAL模式的SQLite数据库并在交换机之间运行VACUUM命令可能导致打开数据库文件的另一个进程或线程错过数据库已更改的事实。第二个进程或线程可能会尝试使用陈旧的缓存修改数据库并导致数据库损坏。
这个问题在内部测试期间被发现,并且在野外从未观察到。问题在2011年1月27日和3.7.5版中修复。
8.3。获取锁定时的I / O会导致腐败
如果操作系统在尝试在WAL模式下获取共享内存上的某个锁时返回I / O错误,则SQLite可能无法重置其缓存,如果尝试进行后续写入,则可能导致数据库损坏。
请注意,只有在尝试获取锁导致I / O错误时才会出现此问题。如果锁定没有被授予(因为其他某个线程或进程已经持有冲突的锁定),则不会发生任何损坏。我们不知道任何操作系统在尝试获取共享内存上的文件锁定时会因I / O错误而失败。所以这是一个理论问题,而不是一个真正的问题。不用说,这个问题在野外从未被观察到。在模拟I / O错误的测试工具中对SQLite进行压力测试时发现问题。
对于SQLite版本3.7.3,此问题在2010年9月20日修复。
8.4。数据库页面从免费页面列表中泄漏
当从SQLite数据库中删除内容时,不再使用的页面将被添加到空闲列表中,并被重用以保存后续插入添加的内容。当使用incremental_vacuum时,出现在版本3.6.16至3.7.2中的SQLite中的一个错误可能会导致页面丢失在空闲列表之外。这不会导致数据丢失。但这会导致数据库文件比所需的大。这将导致integrity_check编译指示报告空闲列表中缺少的页面。
SQLite版本3.7.2在2010-08-23修复了此问题。
8.5。3.6和3.7交替写入后的腐败。
SQLite版本3.7.0引入了对SQLite数据库文件格式(如但不限于WAL)的一些新增功能。3.7.0版本是这些新功能的摇摆版本。我们期望找到问题,并没有失望。
如果数据库最初是使用SQLite版本3.7.0创建的,然后由SQLite版本3.6.23.1编写,这样数据库文件的大小增加,然后再由SQLite 3.7.0版编写,数据库文件可能会损坏。
对于SQLite版本3.7.1,此问题在2010-08-04修复。
8.6。在Windows系统恢复竞争条件。
SQLite 版本3.7.16.2修复了 Windows 系统上锁定逻辑中的细微竞态条件。当数据库文件需要恢复时,因为前一个写入该进程的进程在事务中间崩溃,并且两个或更多进程尝试同时打开该数据库,则竞争条件可能会导致其中一个进程得到恢复已完成的错误提示,从而允许该进程继续使用数据库文件而不先运行恢复。如果该进程写入文件,则该文件可能会损坏。这种竞争条件显然存在于2004年以前的所有 Windows 版 SQLite 中,但竞争非常激烈。实际上,您需要一个快速多核计算机,在该计算机上启动两个进程以在两个单独的内核上同时运行恢复。此缺陷仅在 Windows 系统上,并未影响 POSIX OS 界面。