База данных SQLite обладает высокой устойчивостью к повреждениям. В случае вылета приложения или операционной системы и даже при потере питания посередине транзакции частично записанная транзакция будет автоматически откачена при следующем доступе к файлу базы данных. Процесс восстановления происходит полностью автоматически и не требует никаких действий со стороны пользователя или приложения.

Однако, несмотря на устойчивость SQLite к повреждениям баз данных, абсолютной защиты она обеспечить не может. В этом документе мы расскажем о различных способах повреждения базы данных SQLite.

1. Перезапись файла посторонним потоком или процессом

Файлы баз данных SQLite — это обычные дисковые файлы, поэтому любой процесс может открыть файл и переписать его, заполнив мусором. Библиотека SQLite никак не может защитить от этого.

1.1. Продолжение использования файлового дескриптора после его закрытия

Мы неоднократно сталкивались со случаями, когда файл открывали, а затем его файловый дескриптор закрывали и повторно открывали уже для базы данных SQLite. Позже другой поток продолжал записывать данные через старый файловый дескриптор, не подозревая, что исходный файл уже закрыт. Однако, поскольку SQLite повторно использовала этот дескриптор, данные, предназначенные для исходного файла, в итоге перезаписывали части базы данных SQLite, что приводило к её повреждению.

Один из таких случаев произошёл примерно 30 августа 2013 года в основном репозитории системы контроля версий Fossil DVCS. Тогда файловый дескриптор 2 (стандартный поток ошибок) был ошибочно закрыт (предположительно, программой stunnel) до вызова sqlite3_open_v2(). В результате файловый дескриптор 2 был использован для открытия файла базы данных репозитория. Позже из-за ошибки в приложении инструкция assert() попыталась вывести сообщение об ошибке, вызвав write(2,...). Но поскольку файловый дескриптор 2 к тому моменту уже был связан с файлом базы данных, сообщение об ошибке перезаписало часть базы данных. Чтобы предотвратить подобные ситуации, начиная с версии 3.8.1 (от 17 октября 2013 года) SQLite отказалась от использования для файлов баз данных файловых дескрипторов с малыми номерами. (Дополнительную информацию см. в разделе SQLITE_MINIMUM_FILE_DESCRIPTOR.)

Ещё один случай повреждения данных из-за использования закрытого файлового дескриптора описали инженеры Facebook в посте от 12 августа 2014 года.

Другой подобный случай был зарегистрирован в системе Fossil 11 июля 2019 года. Файловый дескриптор открывали для вывода отладочной информации, но затем закрывали, после чего SQLite повторно использовала его для открытия файла базы данных. При этом код, отвечающий за отладочный вывод, продолжал записывать данные через исходный файловый дескриптор.

Подробнее об ошибке и её исправлении можно прочитать в обсуждении на форуме.

1.2. Резервное копирование или восстановление в процессе активной транзакции

Системы, выполняющие автоматическое резервное копирование в фоновом режиме, могут попытаться создать резервную копию файла базы данных SQLite посередине транзакции. Из-за этого резервная копия будет содержать частично старые и частично новые данные, а следовательно, окажется повреждённой.

Существует несколько безопасных методик создания резервных копий баз данных SQLite — безопасных в том смысле, что они генерируют корректную неповреждённую резервную копию.

  1. Утилита sqlite3_rsync (доступна начиная с SQLite 3.47.0 (от 21 октября 2024 года) и старше) создаёт копию активной SQLite через SSH по протоколу с эффективным использованием канала.

  2. Команда VACUUM INTO filename копирует текущее состояние базы данных SQLite в отдельный файл.

  3. backup API — это интерфейс на языке C, способный создавать согласованную копию базы данных SQLite.

Все эти методики работают даже с активной базой данных. Кроме того, можно безопасно создавать копии файла базы данных SQLite при условии, что в процессе копирования не выполняются транзакции. Если предыдущая транзакция записи завершилась с ошибкой, важно скопировать вместе с самим файлом базы данных также и журнал отката (файл -journal) или журнал предварительной записи (файл -wal).

1.3. Удаление активного журнала отката (hot journal)

Обычно SQLite хранит все данные в одном файле на диске. Однако при выполнении транзакции информация, необходимая для восстановления базы данных после сбоя или аварийного отключения питания, хранится во вспомогательных файлах журналов. Такие файлы журналов называются активными (hot). Имена файлов журналов совпадают с именем файла исходной базы данных с дополнительным суффиксом -journal или -wal.

Для восстановления после сбоя или отключения питания SQLite должна видеть файлы журналов. Если активные журналы отката перемещены, удалены или переименованы после сбоя или аварийного отключения питания, то автоматическое восстановление может не произойти, а база данных может оказаться повреждённой.

Ещё одним проявлением этой проблемы становится повреждение базы данных из-за непоследовательного использования имён файлов в формате 8.3.

1.4. Несоответствие файлов базы данных и активных журналов отката

Предыдущий пример — это частный случай более общей проблемы: состояние базы данных SQLite контролируется и файлом базы данных, и файлом журнала. В состоянии покоя файл журнала отсутствует, и значение имеет только файл базы данных. Но если файл журнала существует, его необходимо хранить вместе с файлом базы данных, чтобы избежать повреждения данных. Все перечисленные ниже действия, скорее всего, приведут к повреждению базы данных:

  • Подмена файлов журналов двух разных баз данных.

  • Перезапись файла журнала другим файлом журнала.

  • Перемещение файла журнала от одной базы данных к другой.

  • Копирование файла базы данных без копирования её журнала.

  • Перезапись файла базы данных другим без удаления активного журнала, связанного с исходной базой данных.

2. Проблемы блокировок доступа к файлам

SQLite использует блокировки файла базы данных, а также файла журнала предварительной записи (WAL), чтобы координировать доступ между одновременно работающими процессами. Без такой координации два потока или процесса могут одновременно попытаться внести несовместимые изменения в файл базы данных, что приведёт к её повреждению.

2.1. Файловые системы с поломанными или отсутствующими реализациями блокировок доступа

SQLite полагается на то, что файловая система, в которой работает база данных, обеспечивает блокировки доступа в соответствии с описанием в документации. Однако в некоторых файловых системах есть ошибки в механизмах блокировки, из-за которых блокировки не всегда работают так, как заявлено. Особенно это характерно для сетевых файловых систем, в частности NFS. Если SQLite используется в файловой системе, в механизмах блокировки которой есть ошибки, и два или более потока или процесса одновременно пытаются получить доступ к одной и той же базе данных, это может привести к её повреждению.

2.2. Отмена рекомендательных блокировок POSIX при вызове close() из другого потока

Механизм блокировок, который SQLite по умолчанию использует на Unix-платформах, основан на рекомендательных блокировках POSIX (POSIX advisory locking). К сожалению, у этого механизма есть особенности реализации, из-за которых он подвержен неправильному использованию и сбоям. В частности, любой поток в рамках одного процесса, имеющий файловый дескриптор, удерживающий 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 и к ней обращаются несколько процессов, однако они решают проблему полностью. Чтобы избежать повреждения данных, разработчикам следует следить за тем, чтобы close() никогда не вызывалась для файла базы данных SQLite, пока остаётся открытым хотя бы одно подключение к базе данных, даже если оно используется в другом потоке.

2.3. Несколько копий SQLite, подключённых к одному приложению

Как отмечалось в предыдущем разделе, SQLite принимает меры для обхода особенностей рекомендательных блокировок POSIX. Одна из таких мер заключается в ведении глобального списка открытых файлов баз данных SQLite, защищённого мьютексом. Однако если в одно приложение подключено несколько копий SQLite, то у каждой из них будет свой экземпляр этого глобального списка. Подключения к базе данных, открытые с помощью одной копии библиотеки SQLite, не будут знать о подключениях, открытых с помощью другой копии, и не смогут обходить особенности рекомендательных блокировок POSIX. Операция close() для одного подключения может незаметно снять блокировки, установленные другим подключением к базе данных, что приведёт к её повреждению.

Этот сценарий может показаться надуманным, однако разработчикам SQLite известен как минимум один коммерческий продукт, выпущенный именно с таким багом. Представители компании-разработчика обратились к создателям SQLite за помощью в поиске причины периодически возникавших повреждений базы данных в Linux и Mac. В итоге выяснилось, что приложение было скомпоновано с двумя отдельными копиями SQLite. Решение заключалось в изменении процедур сборки приложения: вместо двух копий SQLite следовало подключать только одну.

2.4. Два процесса, использующих разные протоколы блокировки доступа

По умолчанию на Unix-платформах SQLite в качестве механизма блокировок доступа использует рекомендательные блокировки POSIX, однако существуют и другие варианты. Выбрав альтернативную sqlite3_vfs, использующую интерфейс sqlite3_open_v2(), приложение может применить другие протоколы блокировки, более подходящие для определённых файловых систем. Например, для приложения, которое должно работать в файловой системе NFS, не поддерживающей рекомендательные блокировки POSIX, можно выбрать блокировку с помощью DOT-файлов (dot-file locking).

Важно, чтобы все соединения с одной базой данных использовали одинаковый протокол блокировки. Если одно приложение использует рекомендательные блокировки POSIX, а другое — блокировку с помощью DOT-файлов, то эти приложения не увидят блокировок друг друга и не смогут координировать доступ к базе данных, что потенциально способно привести к повреждению базы данных.

2.5. Удаление или переименование используемого файла базы данных

Если два процесса имеют открытые подключения к одному и тому же файлу базы данных, и один из них закрывает своё подключение, удаляет файл, затем создаёт на его месте новый файл базы данных с тем же именем и открывает его, то оба процесса будут работать с разными файлами базы данных, имеющими одинаковое имя. (Обратите внимание: такое возможно только в POSIX- и POSIX-подобных системах, которые позволяют удалять файл, пока он открыт для чтения и записи. Windows не допускает такого поведения.) Поскольку имена файлов журналов отката и файлов WAL определяются именем файла базы данных, у двух разных файлов базы данных окажется общий файл журнала отката или WAL. При откате или восстановлении одной из баз данных могут быть использованы данные из другой базы, что приведёт к повреждению данных. Аналогичная проблема возникает, если переименовать открытый файл базы данных, а затем создать новый файл со старым именем.

Иными словами, удаление или переименование открытого файла базы данных приводит к неопределённому и, вероятно, нежелательному поведению.

Начиная с версии SQLite 3.7.17 (от 20 мая 2013 года), интерфейс взаимодействия с ОС в Unix в случае удаления файла базы данных в процессе его использования отправляет в журнал ошибок сообщения SQLITE_WARNING.

2.6. Множественные ссылки на один файл

Если у одного файла базы данных несколько ссылок (жёстких или символических), это означает, что у файла как будто есть несколько имён. Если два или более процесса открывают базу данных, используя разные имена, они будут использовать разные файлы журналов отката и WAL. Это означает, что в случае сбоя одного процесса другой не сможет восстановить незавершённую транзакцию, поскольку будет искать соответствующий файл журнала не там, где нужно.

Иными словами, открытие и использование файла базы данных, имеющего несколько имён, приведёт к неопределённому и, вероятно, нежелательному поведению.

Начиная с версии SQLite 3.7.17 (от 20 мая 2013 года), интерфейс взаимодействия с ОС в Unix в случае наличия у файла базы данных нескольких жёстких ссылок отправляет в журнал ошибок сообщения SQLITE_WARNING.

Начиная с версии SQLite 3.10.0 (от 6 января 2016 года), интерфейс взаимодействия с ОС в Unix пытается разрешать символические ссылки и открывать файл базы данных по его каноническому имени. До версии 3.10.0 открытие файла базы данных через символическую ссылку было аналогично открытию файла с несколькими жёсткими ссылками и приводило к неопределённому поведению.

2.7. Перенос открытого подключения к базе данных через fork()

Не следует открывать подключение к базе данных SQLite, после чего выполнять fork(), а затем пытаться использовать эту базу данных в дочернем процессе. Это приведёт к всевозможным проблемам блокировки доступа, в результате чего база данных легко может быть повреждена. SQLite не рассчитана на поддержку подобного поведения. Все подключения к базам данных, используемые в дочернем процессе, должны открываться в дочернем процессе, а не наследоваться из родительского.

Не следует даже вызывать sqlite3_close() для подключения к базе данных из дочернего процесса, если подключение было открыто в родительском процессе. Можно безопасно закрыть файловый дескриптор, однако интерфейс sqlite3_close() может выполнить действия по очистке, которые удалял содержимое из-под родительского процесса Это приведёт к ошибкам и, возможно, даже к повреждению базы данных.

3. Сбой при синхронизации

Для обеспечения гарантии постоянной целостности базы данных SQLite время от времени просит операционную систему сбросить все ожидающие записи в постоянное хранилище, а затем дожидается завершения этой операции. В Unix для этого используется системный вызов fsync(), а в Windows — FlushFileBuffers(). Такой сброс ожидающих записей мы называем «синхронизацией».

На самом деле, если пользователя интересует только атомарность и согласованность записей, а долговечностью записей можно пренебречь, операция синхронизации не обязана дожидаться полного сохранения данных на энергонезависимом носителе. Вместо этого её можно рассматривать как барьер ввода-вывода. Если все записи, выполненные до синхронизации, завершаются раньше любой записи, выполняемой после синхронизации, повреждения базы данных не произойдёт. Если синхронизация работает как барьер ввода-вывода, а не как полноценная синхронизация, то сбой питания или аварийное завершение работы системы могут привести к откату одной или нескольких ранее зафиксированных транзакций (что нарушает свойство «устойчивости», durability, в модели ACID). Но база данных по крайней мере останется согласованной, а именно это волнует большинство пользователей.

3.1. Дисковые накопители, не учитывающие запросы на синхронизацию

К сожалению, большинство запоминающих устройств потребительского уровня обманывает о синхронизации. Дисковые накопители сообщают, что содержимое надёжно сохранено в устройстве, как только оно попадает в буфер дорожки, хотя фактически на магнитное покрытие они записываются позднее. Из-за этого кажется, что диски работают быстрее (а это жизненно важно производителю, чтобы он мог демонстрировать хорошие показатели бенчмарков в журналах). И справедливости ради стоит отметить, что обычно этот обман не приводит к неприятным последствиям, если только питание не отключается и не происходит аппаратный сброс до того, как содержимое буфера дорожки будет фактически записано на магнитное покрытие. Но если питание всё же отключится или произойдёт аппаратный сброс и в результате данные, записанные после синхронизации, попадут на магнитное покрытие, а данные, записанные до неё, всё ещё останутся в буфере дорожки, это может привести к повреждению базы данных.

Похоже, особенно часто обманывают при выполнении запросов на синхронизацию USB-флешки. Это легко проверить, зафиксировав большую транзакцию в базе данных SQLite, расположенной на USB-флешке. Команда COMMIT выполнится относительно быстро. Это означает, что флешка сообщила операционной системе, а операционная система — SQLite, что все данные надёжно сохранены в энергонезависимой памяти. И всё же светодиод на конце флешки будет продолжать мигать ещё несколько секунд. Извлечение флешки при всё ещё мигающем светодиоде часто приводит к повреждению базы данных.

SQLite вынуждена доверять операционной системе и оборудованию, когда те сообщают о состоянии запросов на синхронизацию. SQLite никак не может определить, что кто-то из них обманывает и что записи могут выполняться не в том порядке. Однако SQLite в режиме WAL гораздо лучше переносит нарушение порядка записей, чем в режиме журнала отката, используемом по умолчанию. В режиме WAL сбой синхронизации может привести к повреждению базы данных только во время операции создания контрольной точки. Сбой синхронизации во время выполнения COMMIT может привести к потере durability, но не к повреждению файла базы данных. Поэтому один из способов защиты от повреждения базы данных из-за сбоев синхронизации заключается в использовании SQLite в режиме WAL и в как можно более редком создании контрольных точек.

3.2. Отключение синхронизации при помощи PRAGMA

Операции синхронизации, которые SQLite выполняет для обеспечения целостности данных, можно отключить в среде исполнения с помощью synchronous pragma. Если установить PRAGMA synchronous=OFF, то все операции синхронизации будут пропущены. Из-за этого может показаться, что SQLite работает быстрее, но при этом операционная система может свободно менять порядок записей. Если до того, как все данные будут сохранены в энергонезависимой памяти, произойдёт сбой питания или аппаратный сброс, это может привести к повреждению базы данных.

Для обеспечения максимальной надёжности и защиты от повреждения базы данных SQLite всегда следует использовать по умолчанию synchronous=FULL.

4. Сбои дисков и флеш-памяти

База данных SQLite может оказаться повреждённой, если содержимое файла меняется вследствие сбоя диска или флеш-памяти. Такое бывает очень редко, но диски иногда инвертируют биты посередине сектора.

4.1. Контроллеры флеш-памяти, не обеспечивающие сохранность данных при сбое питания

Нам сообщают, что логика выравнивания износа в некоторых контроллерах флеш-памяти может в случае сбоя питания во время записи приводить к произвольному повреждению файловой системы. Это может проявляться, например, в виде случайных изменений посередине файла, который даже не был открыт в момент потери питания. Допустим, в момент сбоя питания устройство записывало содержимое в файл MP3 во флеш-памяти, но это может привести к повреждению базы данных SQLite, даже если база данных в это время не использовалась.

4.2. USB-накопители с поддельной ёмкостью

В продаже можно найти множество мошеннических USB-накопителей, сообщающих о большой ёмкости (например, 8 ГБ), но на самом деле способных хранить гораздо меньший объём (например, 1 ГБ). При попытках записи на эти устройства зачастую перезаписываются никак не связанные с записью файлы, поэтому любое использование мошеннических устройств флеш-памяти легко может приводить к повреждениям баз данных. Выполнив в Интернете поиск по запросу «fake capacity usb», вы можете найти много информации об этой проблеме.

5. Повреждение памяти

SQLite — это библиотека на языке C, выполняемая в том же адресном пространстве, что и обслуживаемое ею приложение. Из-за этого ошибочные указатели, выход за границы буфера, повреждение кучи и другие сбои в приложении могут повредить внутренние структуры данных SQLite и в конечном итоге привести к повреждению файла базы данных. Обычно подобные проблемы проявляются в виде ошибок сегментации ещё до того, как успевает повредиться база данных, однако известны случаи, когда ошибки в коде приложения приводили к незаметным сбоям в работе SQLite, в результате которых повреждался файл базы данных, а не происходило аварийное завершение программы.

Проблема повреждения памяти становится ещё острее при использовании ввода-вывода с отображением в память. Если весь файл базы данных или его часть отображены в адресное пространство приложения, то ошибочный указатель, перезаписывающий любую область этой памяти, немедленно повредит файл базы данных; для этого приложению даже не потребуется впоследствии выполнять системный вызов write().

6. Проблемы с другими операционными системами

Иногда операционные системы демонстрируют нестандартное поведение, способное приводить к проблемам. Бывает, что такое нестандартное поведение реализовано намеренно, а иногда это ошибка реализации. Как бы то ни было, если операционная система ведёт себя неожиданным для SQLite образом, возникает вероятность повреждения базы данных.

6.1. Потоки Linux

В некоторых старых версиях Linux для поддержки потоков использовалась библиотека LinuxThreads. LinuxThreads похожа на Pthreads, но её поведение немного отличается в обработке рекомендательных блокировок POSIX. SQLite версий с 2.2.3 по 3.6.23 распознавала, что в среде исполнения используется LinuxThreads, и предпринимала соответствующие действия для обхода нестандартного поведения LinuxThreads, однако в современных реализациях Linux применяется более новая корректная NPTL-реализация Pthreads. Начиная с SQLite версии 3.7.0 (от 21 июля 2010 года), предполагается использование NPTL без выполнения проверок. Из-за этого новые версии версии SQLite могут давать незаметные сбои и повреждать файлы баз данных, если использовать их в многопоточных приложениях, работающих на старых системах Linux с LinuxThreads.

6.2. Сбои mmap() в QNX

В QNX существует трудноуловимая проблема с mmap(): повторный вызов mmap() для одного и того же файлового дескриптора может привести к обнулению памяти, выделенной при первом вызове mmap(). В Unix SQLite использует mmap() для создания области разделяемой памяти, необходимой для координации транзакций в режиме WAL, и при работе с большими транзакциями может вызывать mmap() несколько раз. Было продемонстрировано, что в таких условиях mmap() в QNX приводит к повреждению файла базы данных. Инженеры QNX знают об этой проблеме и работают над её устранением; возможно, к тому времени, когда вы будете читать этот текст, она уже будет устранена.

При работе в QNX рекомендуется никогда не использовать ввод-вывод с отображением в память. Кроме того, для использования режима WAL рекомендуется, чтобы приложения задействовали режим эксклюзивной блокировки, позволяющий использовать WAL без разделяемой памяти

6.3. Повреждение файловой системы

Так как базы данных SQLite представляют собой обычные файлы на диске, любые сбои в работе файловой системы могут привести к повреждению базы данных. Файловые системы современных операционных систем очень надёжны, но ошибки всё же случаются. Например, 1 октября 2013 года база данных SQLite, в которой хранилась вики Tcl/Tk, оказалась повреждена спустя несколько дней после того, как хост-компьютер перевели на сомнительную сборку ядра Linux, в которой были проблемы в слое файловой системы. В итоге файловая система оказалась настолько сильно повреждена, что машину стало невозможно использовать. Однако первым признаком неполадок стало именно повреждение базы данных 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 крайне тщательно тестируется для максимального избавления от багов. Среди многочисленных тестов, которые проводятся для каждой версии SQLite, есть тесты, имитирующие сбои питания, ошибки ввода-вывода и ошибки нехватки памяти (OOM). Они позволяют убедиться, что ни одно из этих событий не приводит к повреждению базы данных. Надёжность SQLite подтверждена и многолетним практическим использованием: библиотека активно применяется примерно в двух миллиардах развёрнутых инстансов и не вызывает серьёзных проблем.

Тем не менее ни одно программное обеспечение не бывает идеальным на 100%. В истории SQLite было несколько багов (которые к настоящему времени устранены), способных привести к повреждению базы данных. Возможно, остаются и другие, пока не обнаруженные ошибки. Благодаря тщательному тестированию и широкому распространению SQLite баги, приводящие к повреждению базы данных, как правило, очень трудно выявить. Вероятность того, что приложение столкнётся с таким багом SQLite, невелика. Чтобы проиллюстрировать это, ниже приведён перечень всех багов SQLite, обнаруженных за четырёхлетний период с 1 апреля 2009 года по 15 апреля 2013 года, которые приводили к повреждению базы данных. Этот перечень поможет читателю получить общее представление о том, какие ошибки SQLite всё же проходят через процедуры тестирования и попадают в выпущенные версии.

8.1. Состояние гонки при записи в базу данных в режиме WAL.

Если несколько подключений к базе данных, открытых в разных потоках или процессах, одновременно работают с одной и той же базой данных в режиме WAL, и оба подключения пытаются одновременно записывать данные или создавать контрольную точку, возникает состояние гонки, которое может привести к повреждению файла базы данных. Это ошибка сброса WAL. Она существовала во всех версиях SQLite начиная с 3.7.0 и заканчивая 3.51.2. Подробнее см. в документации об ошибке сброса WAL.

8.2. Устаревшие индексы по выражениям

«Индекс по выражению» (expression index) — это индекс, который ссылается на выражение или виртуальный вычисляемый столбец. В индексах по выражениям должны использоваться только детерминированные функции. Однако при переносе базы данных между разными платформами или смене версии SQLite результат работы некоторых функций, считающихся детерминированными, может немного измениться. Из-за этого индекс может выглядеть повреждённым. Подробнее см. в документации об устаревших индексах по выражениям.

8.3. Ложные сообщения о повреждениях, связанные с уменьшением базы данных

Если база данных была записана с помощью SQLite версии 3.7.0 или более поздней, а затем повторно изменена с помощью SQLite версии 3.6.23 или более ранней таким образом, что размер файла базы данных уменьшился, то при следующем обращении SQLite версии 3.7.0 к этому файлу может сообщить, что он повреждён. Однако на самом деле файл базы данных не повреждён. Просто версия 3.7.0 слишком рьяно выявляла повреждения.

Проблема была исправлена 20 февраля 2011 года. Исправление было внесено в версии SQLite 3.7.6 (от 12 апреля 2011 года).

8.4. Повреждение базы данных при переключении между режимами журнала отката и WAL

Многократное переключение базы данных SQLite между режимом WAL и обычным режимом журнала отката с выполнением команды VACUUM между переключениями в одном процессе или потоке может привести к тому, что другой процесс или поток, у которого открыт файл базы данных, не заметит, что база данных изменилась. В результате этот процесс или поток может попытаться изменить базу данных, используя устаревший кэш, что приведёт к повреждению базы данных.

Эта проблема была обнаружена в ходе внутреннего тестирования, и в реальных условиях она ни разу не наблюдалась. Её устранили 27 января 2011 года в версии 3.7.5.

8.5. Ошибка ввода-вывода при получении блокировки приводит к повреждению базы данных

Если операционная система возвращает ошибку ввода-вывода при попытке получить блокировку общей памяти в режиме WAL, SQLite может не сбросить свой кэш. Если после этого попытаться выполнить запись, это может привести к повреждению базы данных.

Стоит отметить, что эта проблема возникает только в том случае, если попытка получить блокировку завершилась ошибкой ввода-вывода. Если же блокировка просто не была предоставлена (потому что другой поток или процесс уже удерживал конфликтующую блокировку), повреждение данных не произойдёт ни при каких обстоятельствах. Нам неизвестны операционные системы, которые возвращали бы ошибку ввода-вывода при попытке получить блокировку файла в общей памяти, поэтому это теоретическая проблема, а не реальная. Само собой, в реальных условиях эта проблема ни разу не наблюдалась. Её обнаружили при стресс-тестировании SQLite с помощью тестового стенда, имитирующего ошибки ввода-вывода.

Проблема была устранена 20 сентября 2010 года в версии SQLite 3.7.3.

8.6. Утечка страниц базы данных из списка свободных страниц

При удалении содержимого из базы SQLite больше не используемые страницы добавляются в список свободных страниц. Впоследствии они повторно используются для хранения содержимого, добавляемых при выполнении новых операций INSERT. Ошибка в SQLite, присутствовавшая в версиях с 3.6.16 по 3.7.2, могла приводить к исчезновению страниц из списка свободных при использовании incremental_vacuum. Потери данных при этом не происходило. Однако файл базы данных становился больше, чем требовалось, а integrity_check pragma сообщала об отсутствии страниц в списке свободных.

Проблема была устранена 23 августа 2010 года в версии SQLite 3.7.2.

8.7. Повреждение базы данных при чередовании операций записи из версий 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, файл базы данных мог оказаться повреждённым.

Проблема была устранена 4 августа 2010 года в версии SQLite 3.7.1.

8.8. Состояние гонки при восстановлении в системах с Windows

В SQLite версии 3.7.16.2 исправлено трудноуловимое состояние гонки в логике блокировок в системах с Windows. Если база данных нуждалась в восстановлении из-за того, что предыдущий процесс, выполнявший запись, аварийно завершился посередине транзакции, и несколько процессов пытались одновременно открыть эту базу данных, состояние гонки могло привести к тому, что один из процессов ошибочно решал, будто восстановление уже завершено. В результате он продолжал работу с файлом базы данных, не выполнив предварительно восстановление. Если этот процесс записывал данные в файл, тот мог оказаться повреждённым. По всей видимости, это состояние гонки существовало во всех предыдущих версиях SQLite для Windows, начиная с 2004 года, однако вероятность его возникновения была очень мала. На практике для этого требовался быстрый многоядерный компьютер, на котором два процесса запускали восстановление одновременно на разных ядрах. Этот дефект затрагивал только системы с Windows и не влиял на интерфейс POSIX взаимодействия с ОС.

8.9. Ошибка граничного значения во вспомогательных журналах, используемых при вложенных транзакциях

При запуске вложенной транзакции с помощью SAVEPOINT SQLite использует дополнительный журнал отката для отслеживания изменений во вложенной транзакции на случай, если её потребуется отменить. Дополнительные журналы не участвуют в защите базы данных от повреждения из-за сбоев программы или отключения питания. Они используются только при откате внутренней транзакции, входящей во вложенную.

Эти дополнительные журналы могут храниться как в оперативной памяти, так и во временных файлах на диске. По умолчанию они хранятся на диске. Однако это поведение можно изменить с помощью параметра компиляции -DSQLITE_TEMP_STORE или во время выполнения программы с помощью инструкции PRAGMA temp_store. Ошибка возникает только в том случае, если дополнительные журналы хранятся в оперативной памяти.

В SQLite 3.35.0 (от 12 марта 2021 года) была добавлена новая оптимизация, позволяющая уменьшить объём используемой памяти при хранении дополнительных журналов в оперативной памяти. К сожалению, проверка граничного условия в новой логике была реализована неправильно: вместо оператора «<» были использован оператор «<=». Эта ошибка могла привести к тому, что при откате дополнительный журнал переходил в несогласованное состояние. Если после этого вносились дополнительные изменения, а внешняя транзакция в итоге фиксировалась, база данных могла оказаться в несогласованном состоянии.

Эту проблему обнаружил независимый исследователь, который пытался найти баги в SQLite с помощью фаззера. Фаззер обнаружил сбой в операторе assert(), предназначенном для проверки внутреннего состояния дополнительного журнала. Баг был настолько редким частным случаем, что мог оставаться незамеченным ещё много лет, если бы не активное использование операторов assert() в SQLite, настойчивость исследователей безопасности и их специализированный фаззер, созданный с применением самых современных методов.

Проблема была устранена в версии 3.37.2 (от 6 января 2022 года).

Комментарии (3)


  1. CitizenOfDreams
    05.11.2026 08:07

    Краткое содержание статьи: если в файл записывать мусор, он портится.


  1. InsiderCrush
    05.11.2026 08:07

    9 Черные лебеди
    9.1 Баба Шура - уборщица в серверной и разлитое ведро воды


    1. Ruinov
      05.11.2026 08:07

      10 Черная дыра

      10.1 Удалил файл базы данных, а последняя резервная копия была сделана 3 года назад до появления этого файла