Spec-Zone.ru › SQLite

Как повредить файл базы данных SQLite

Содержание
1. Перезапись файла посторонним потоком или процессом
1.1. Продолжение использования файлового дескриптора после его закрытия
1.2. Резервное копирование или восстановление во время активной транзакции
1.3. Удаление горячего журнала
1.4. Несоответствие файлов баз данных и горячих журналов
2. Проблемы с блокировкой файлов
2.1. Файловые системы с неисправной или отсутствующей реализацией блокировок
2.2. Снятие блокировок POSIX отдельным потоком при вызове close()
2.3. Несколько копий SQLite, связанных с одним приложением
2.4. Два процесса, использующие разные протоколы блокировки
2.5. Удаление или переименование используемого файла базы данных
2.6. Несколько ссылок на один и тот же файл
2.7. Перенос открытого подключения к базе данных через fork()
3. Сбой синхронизации
3.1. Дисковые накопители, не выполняющие запросы синхронизации
3.2. Отключение синхронизации с помощью PRAGMA
4. Сбои дисковых накопителей и флеш-памяти
4.1. Контроллеры флеш-памяти, не защищенные от потери питания
4.2. USB-накопители с поддельной емкостью
5. Повреждение памяти
6. Другие проблемы операционной системы
6.1. Потоки Linux
6.2. Сбои mmap() в QNX
6.3. Повреждение файловой системы
7. Ошибки конфигурации SQLite
8. Ошибки в SQLite
8.1. Состояние гонки при записи в базу данных в режиме WAL.
8.2. Устаревшие индексы по выражениям
8.3. Ложные сообщения о повреждении из-за уменьшения размера базы данных
8.4. Повреждение при переключении между режимами отката и WAL
8.5. Ошибка ввода-вывода при получении блокировки приводит к повреждению
8.6. Утечка страниц базы данных из списка свободных страниц
8.7. Повреждение при чередовании записей из версий 3.6 и 3.7
8.8. Состояние гонки при восстановлении в системах Windows
8.9. Ошибка граничного значения во вторичных журналах, используемых вложенными транзакциями

Обзор

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

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

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

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

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

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

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

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

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

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

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

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

  1. Утилита sqlite3_rsync (доступна начиная с SQLite 3.47.0 (2024-10-21)) позволяет копировать работающую базу данных SQLite по SSH с помощью протокола, экономно расходующего пропускную способность.

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

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

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

1.3. Удаление горячего журнала

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

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

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

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

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

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

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

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

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

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

2.2. Снятие блокировок POSIX отдельным потоком при вызове close()

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

2.3. Несколько копий SQLite, связанных с одним приложением

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Для обеспечения максимальной надежности и защиты от повреждения базы данных SQLite следует всегда запускать со значением синхронизации по умолчанию — 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 используется более новая и корректная реализация Pthreads — NPTL. Начиная с версии 3.7.0 SQLite (2010-07-21) предполагается использование NPTL. Проверки не выполняются. Поэтому последние версии SQLite могут незаметно работать неправильно и повредить файлы баз данных, если использовать их в многопоточном приложении на старых системах Linux с LinuxThreads.

6.2. Сбои mmap() в QNX

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

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

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

Поскольку базы данных SQLite — это обычные файлы на диске, любой сбой файловой системы может повредить базу данных. Файловые системы современных операционных систем очень надежны, но ошибки все же случаются. Например, 2013-10-01 база данных 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, обнаруженных за четырехлетний период с 2009-04-01 по 2013-04-15, которые приводили к повреждению базы данных. Этот перечень поможет читателю получить интуитивное представление о том, каким ошибкам SQLite удается пройти тестирование и попасть в выпущенную версию.

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

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

8.2. Устаревшие индексы выражений

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

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

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

Проблема была исправлена 2011-02-20. Исправление впервые вошло в версию 3.7.6 SQLite (2011-04-12).

8.4. Повреждение после переключения между режимами отката и WAL

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

Эта проблема была обнаружена во время внутреннего тестирования и никогда не наблюдалась в реальных условиях. Она была исправлена 2011-01-27 и вошла в версию 3.7.5.

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

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

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

Проблема была исправлена 2010-09-20 для версии SQLite 3.7.3.

8.6. Страницы базы данных теряются из списка свободных страниц

Когда содержимое удаляется из базы данных SQLite, неиспользуемые страницы добавляются в список свободных страниц и повторно используются для хранения содержимого, добавляемого последующими операциями вставки. Из-за ошибки в SQLite, присутствовавшей в версиях от 3.6.16 до 3.7.2, страницы могли пропадать из списка свободных страниц при использовании incremental_vacuum. Это не приводило к потере данных, но увеличивало размер файла базы данных сверх необходимого. Кроме того, прагма integrity_check сообщала об отсутствии страниц в списке свободных страниц.

Проблема была исправлена 2010-08-23 для версии 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, файл базы данных мог повредиться.

Проблема была исправлена 2010-08-04 для версии 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. Ошибка возникает только в том случае, если вторичные журналы хранятся в памяти.

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

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

Проблема была исправлена в версии 3.37.2 (2022-01-06).

Последнее обновление этой страницы: 2026-03-19 12:11:29Z

SQLite is in the Public Domain.
https://sqlite.org/howtocorrupt.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API