[go: nahoru, domu]

blob: dde1b58e587f78c48756879d2c3cbfac1a8976d9 [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "sql/recovery.h"
#include <stddef.h>
#include <memory>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/dcheck_is_on.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/format_macros.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/types/pass_key.h"
#include "build/build_config.h"
#include "sql/database.h"
#include "sql/error_delegate_util.h"
#include "sql/internal_api_token.h"
#include "sql/meta_table.h"
#include "sql/recover_module/module.h"
#include "sql/sql_features.h"
#include "sql/sqlite_result_code.h"
#include "sql/sqlite_result_code_values.h"
#include "sql/statement.h"
#include "third_party/sqlite/sqlite3.h"
namespace sql {
namespace {
constexpr char kMainDatabaseName[] = "main";
} // namespace
// static
bool BuiltInRecovery::IsSupported() {
return base::FeatureList::IsEnabled(features::kUseBuiltInRecoveryIfSupported);
}
// static
bool BuiltInRecovery::ShouldAttemptRecovery(Database* database,
int extended_error) {
return BuiltInRecovery::IsSupported() && database && database->is_open() &&
!database->DbPath(InternalApiToken()).empty() &&
#if BUILDFLAG(IS_FUCHSIA)
// Recovering WAL databases is not supported on Fuchsia.
!database->UseWALMode() &&
#endif // BUILDFLAG(IS_FUCHSIA)
IsErrorCatastrophic(extended_error);
}
// static
SqliteResultCode BuiltInRecovery::RecoverDatabase(Database* database,
Strategy strategy) {
if (!BuiltInRecovery::IsSupported()) {
return SqliteResultCode::kAbort;
}
auto recovery = BuiltInRecovery(database, strategy);
return recovery.RecoverAndReplaceDatabase();
}
// static
bool BuiltInRecovery::RecoverIfPossible(
Database* database,
int extended_error,
Strategy strategy,
const base::Feature* const use_builtin_recovery_if_supported_flag) {
// If `BuiltInRecovery` is supported at all, check the flag for this specific
// database, provided by the feature team.
bool use_builtin_recovery =
BuiltInRecovery::IsSupported() &&
(!use_builtin_recovery_if_supported_flag ||
base::FeatureList::IsEnabled(*use_builtin_recovery_if_supported_flag));
if (use_builtin_recovery
? !BuiltInRecovery::ShouldAttemptRecovery(database, extended_error)
: !database || !database->is_open() ||
database->DbPath(InternalApiToken()).empty() ||
database->UseWALMode() ||
!Recovery::ShouldRecover(extended_error)) {
return false;
}
// Recovery should be attempted. Since recovery must only be attempted from
// within a database error callback, reset the error callback to prevent
// re-entry.
database->reset_error_callback();
if (use_builtin_recovery) {
CHECK(BuiltInRecovery::IsSupported());
auto result = BuiltInRecovery::RecoverDatabase(database, strategy);
if (!IsSqliteSuccessCode(result)) {
DLOG(ERROR) << "Database recovery failed with result code: " << result;
}
} else {
switch (strategy) {
case BuiltInRecovery::Strategy::kRecoverOrRaze:
Recovery::RecoverDatabase(database,
database->DbPath(InternalApiToken()));
break;
case BuiltInRecovery::Strategy::kRecoverWithMetaVersionOrRaze:
Recovery::RecoverDatabaseWithMetaVersion(
database, database->DbPath(InternalApiToken()));
break;
}
}
return true;
}
BuiltInRecovery::BuiltInRecovery(Database* database, Strategy strategy)
: strategy_(strategy),
db_(database),
recover_db_(sql::DatabaseOptions{
.page_size = database ? database->page_size() : 0,
.cache_size = 0,
}) {
CHECK(IsSupported());
CHECK(db_);
CHECK(db_->is_open());
// Recovery is likely to be used in error handling. To prevent re-entry due to
// errors while attempting to recover the database, the error callback must
// not be set.
CHECK(!db_->has_error_callback());
auto db_path = db_->DbPath(InternalApiToken());
// Corruption recovery for in-memory databases is not supported.
CHECK(!db_path.empty());
// Cache the database's histogram tag while the database is open.
database_uma_name_ = db_->histogram_tag();
recovery_database_path_ = db_path.AddExtensionASCII(".recovery");
// Break any outstanding transactions on the original database, since the
// recovery module opens a transaction on the database while recovery is in
// progress.
db_->RollbackAllTransactions();
}
BuiltInRecovery::~BuiltInRecovery() {
// Recovery result must be set before we reach this point.
CHECK_NE(result_, Result::kUnknown);
base::UmaHistogramEnumeration("Sql.Recovery.Result", result_);
UmaHistogramSqliteResult("Sql.Recovery.ResultCode",
static_cast<int>(sqlite_result_code_));
if (!database_uma_name_.empty()) {
base::UmaHistogramEnumeration(
base::StrCat({"Sql.Recovery.Result.", database_uma_name_}), result_);
UmaHistogramSqliteResult(
base::StrCat({"Sql.Recovery.ResultCode.", database_uma_name_}),
static_cast<int>(sqlite_result_code_));
}
if (db_) {
if (result_ == Result::kSuccess) {
// Poison the original handle, but don't raze the database.
db_->Poison();
} else {
db_->RazeAndPoison();
}
}
db_ = nullptr;
if (recover_db_.is_open()) {
recover_db_.Close();
}
// TODO(https://crbug.com/1385500): Don't always delete the recovery db if we
// are ever to keep around successfully-recovered, but unsuccessfully-restored
// databases.
sql::Database::Delete(recovery_database_path_);
}
void BuiltInRecovery::SetRecoverySucceeded() {
// Recovery result must only be set once.
CHECK_EQ(result_, Result::kUnknown);
result_ = Result::kSuccess;
}
void BuiltInRecovery::SetRecoveryFailed(Result failure_result,
SqliteResultCode result_code) {
// Recovery result must only be set once.
CHECK_EQ(result_, Result::kUnknown);
switch (failure_result) {
case Result::kUnknown:
case Result::kSuccess:
NOTREACHED();
break;
case Result::kFailedRecoveryInit:
case Result::kFailedRecoveryRun:
case Result::kFailedToOpenRecoveredDatabase:
case Result::kFailedMetaTableDoesNotExist:
case Result::kFailedMetaTableInit:
case Result::kFailedMetaTableVersionWasInvalid:
case Result::kFailedBackupInit:
case Result::kFailedBackupRun:
break;
}
result_ = failure_result;
sqlite_result_code_ = result_code;
}
SqliteResultCode BuiltInRecovery::RecoverAndReplaceDatabase() {
auto sqlite_result_code = AttemptToRecoverDatabaseToBackup();
if (sqlite_result_code != SqliteResultCode::kOk) {
return sqlite_result_code;
}
// Open a connection to the newly-created recovery database.
if (!recover_db_.Open(recovery_database_path_)) {
DVLOG(1) << "Unable to open recovery database.";
// TODO(https://crbug.com/1385500): It's unfortunate to give up now, after
// we've successfully recovered the database to a backup. Consider falling
// back to base::Move().
SetRecoveryFailed(Result::kFailedToOpenRecoveredDatabase,
ToSqliteResultCode(recover_db_.GetErrorCode()));
return SqliteResultCode::kError;
}
if (strategy_ == Strategy::kRecoverWithMetaVersionOrRaze &&
!RecoveredDbHasValidMetaTable()) {
DVLOG(1) << "Could not read valid version number from recovery database.";
return SqliteResultCode::kError;
}
return ReplaceOriginalWithRecoveredDb();
}
SqliteResultCode BuiltInRecovery::AttemptToRecoverDatabaseToBackup() {
CHECK(db_->is_open());
CHECK(!recover_db_.is_open());
// See full documentation for the corruption recovery module in
// https://sqlite.org/src/file/ext/recover/sqlite3recover.h
// sqlite3_recover_init() create a new sqlite3_recover handle, with data being
// recovered into a new database. This should very rarely fail - e.g. if
// memory for the recovery object itself could not be allocated. If it does
// fail, `recover` will be nullptr and an error code will surface when
// attempting to configure the recovery object below.
sqlite3_recover* recover =
sqlite3_recover_init(db_->db(InternalApiToken()), kMainDatabaseName,
recovery_database_path_.AsUTF8Unsafe().c_str());
// sqlite3_recover_config() configures the sqlite3_recover object.
//
// These functions should only fail if the above initialization failed, or if
// invalid parameters are passed.
// Don't bother creating a lost-and-found table.
sqlite3_recover_config(recover, SQLITE_RECOVER_LOST_AND_FOUND, nullptr);
// Do not attempt to recover records from pages that appear to be linked to
// the freelist, to avoid "recovering" deleted records.
int kRecoverFreelist = 0;
sqlite3_recover_config(recover, SQLITE_RECOVER_FREELIST_CORRUPT,
static_cast<void*>(&kRecoverFreelist));
// Attempt to recover ROWID values that are not INTEGER PRIMARY KEY.
int kRecoverRowIds = 1;
sqlite3_recover_config(recover, SQLITE_RECOVER_ROWIDS,
static_cast<void*>(&kRecoverRowIds));
auto sqlite_result_code =
ToSqliteResultCode(sqlite3_recover_errcode(recover));
if (sqlite_result_code != SqliteResultCode::kOk) {
CHECK_NE(sqlite_result_code, SqliteResultCode::kApiMisuse);
// The recovery could not be configured.
// TODO(https://crbug.com/1385500): This is likely a transient issue, so we
// could consider keeping the database intact in case the caller wants to
// try again later. For now, we'll always raze.
SetRecoveryFailed(Result::kFailedRecoveryInit, sqlite_result_code);
DVLOG(1) << "recovery config error: " << sqlite_result_code
<< sqlite3_recover_errcode(recover);
// Clean up the recovery object.
sqlite3_recover_finish(recover);
return sqlite_result_code;
}
// sqlite3_recover_run() attempts to construct an copy of the database with
// data corruption handled. It returns SQLITE_OK if recovery was successful.
sqlite_result_code = ToSqliteResultCode(sqlite3_recover_run(recover));
// sqlite3_recover_finish() cleans up the recovery object. It should return
// the same error code as from sqlite3_recover_run().
auto finish_result_code = ToSqliteResultCode(sqlite3_recover_finish(recover));
CHECK_EQ(finish_result_code, sqlite_result_code);
if (sqlite_result_code != SqliteResultCode::kOk) {
// Could not recover the database.
SetRecoveryFailed(Result::kFailedRecoveryRun, sqlite_result_code);
DVLOG(1) << "recovery error: " << sqlite_result_code
<< sqlite3_recover_errmsg(recover);
}
return sqlite_result_code;
}
SqliteResultCode BuiltInRecovery::ReplaceOriginalWithRecoveredDb() {
CHECK(db_->is_open());
CHECK(recover_db_.is_open());
// sqlite3_backup_init() fails if a transaction is ongoing. This should be
// rare, since we rolled back all transactions in this object's constructor.
sqlite3_backup* backup = sqlite3_backup_init(
db_->db(InternalApiToken()), kMainDatabaseName,
recover_db_.db(InternalApiToken()), kMainDatabaseName);
if (!backup) {
// Error code is in the destination database handle.
DVLOG(1) << "sqlite3_backup_init() failed: "
<< sqlite3_errmsg(db_->db(InternalApiToken()));
auto result_code =
ToSqliteResultCode(sqlite3_errcode(db_->db(InternalApiToken())));
// TODO(https://crbug.com/1385500): It's unfortunate to give up now, after
// we've successfully recovered the database. Consider falling back to
// base::Move().
SetRecoveryFailed(Result::kFailedBackupInit, result_code);
return result_code;
}
// sqlite3_backup_step() copies pages from the source to the destination
// database. It returns SQLITE_DONE if copying successfully completed, or some
// other error on failure.
// TODO(https://crbug.com/1385500): Some of these errors are transient and the
// operation could feasibly succeed at a later time. Consider keeping around
// successfully-recovered, but unsuccessfully-restored databases or falling
// back to base::Move().
constexpr int kUnlimitedPageCount = -1; // Back up entire database.
auto sqlite_result_code =
ToSqliteResultCode(sqlite3_backup_step(backup, kUnlimitedPageCount));
// sqlite3_backup_remaining() returns the number of pages still to be backed
// up, which should be zero if sqlite3_backup_step() completed successfully.
int pages_remaining = sqlite3_backup_remaining(backup);
// sqlite3_backup_finish() releases the sqlite3_backup object.
//
// It returns an error code only if the backup encountered a permanent error.
// We use the the sqlite3_backup_step() result instead, because it also tells
// us about temporary errors, like SQLITE_BUSY.
//
// We pass the sqlite3_backup_finish() result code through
// ToSqliteResultCode() to catch codes that should never occur, like
// SQLITE_MISUSE.
std::ignore = ToSqliteResultCode(sqlite3_backup_finish(backup));
if (sqlite_result_code != SqliteResultCode::kDone) {
CHECK_NE(sqlite_result_code, SqliteResultCode::kOk)
<< "sqlite3_backup_step() returned SQLITE_OK (instead of SQLITE_DONE) "
<< "when asked to back up the entire database";
DVLOG(1) << "sqlite3_backup_step() failed: "
<< sqlite3_errmsg(db_->db(InternalApiToken()));
SetRecoveryFailed(Result::kFailedBackupRun, sqlite_result_code);
return sqlite_result_code;
}
// The original database was successfully recovered and replaced. Hooray!
SetRecoverySucceeded();
CHECK_EQ(pages_remaining, 0);
return SqliteResultCode::kOk;
}
bool BuiltInRecovery::RecoveredDbHasValidMetaTable() {
CHECK(recover_db_.is_open());
if (!MetaTable::DoesTableExist(&recover_db_)) {
DVLOG(1) << "Meta table does not exist in recovery database.";
SetRecoveryFailed(Result::kFailedMetaTableDoesNotExist,
ToSqliteResultCode(recover_db_.GetErrorCode()));
return false;
}
// MetaTable::Init will not create a meta table if one already exists.
sql::MetaTable meta_table;
if (!meta_table.Init(&recover_db_, /*version=*/1,
/*compatible_version=*/1)) {
SetRecoveryFailed(Result::kFailedMetaTableInit,
ToSqliteResultCode(recover_db_.GetErrorCode()));
return false;
}
// Confirm that we can read a valid version number from the recovered table.
if (meta_table.GetVersionNumber() <= 0) {
SetRecoveryFailed(Result::kFailedMetaTableVersionWasInvalid,
ToSqliteResultCode(recover_db_.GetErrorCode()));
return false;
}
return true;
}
// static
std::unique_ptr<Recovery> Recovery::Begin(Database* database,
const base::FilePath& db_path) {
// Recovery is likely to be initiated in an error handler. Since recovery
// changes the state of the handle, protect against multiple layers attempting
// the same recovery.
if (!database->is_open()) {
// Warn about API mis-use.
DCHECK(database->poisoned(InternalApiToken()))
<< "Illegal to recover with closed Database";
return nullptr;
}
// Using `new` to access a non-public constructor
std::unique_ptr<Recovery> recovery(new Recovery(database));
if (!recovery->Init(db_path)) {
// TODO(shess): Should Init() failure result in Raze()?
recovery->Shutdown(POISON);
return nullptr;
}
return recovery;
}
// static
bool Recovery::Recovered(std::unique_ptr<Recovery> r) {
return r->Backup();
}
// static
void Recovery::Unrecoverable(std::unique_ptr<Recovery> r) {
CHECK(r->db_);
// ~Recovery() will RAZE_AND_POISON.
}
// static
void Recovery::Rollback(std::unique_ptr<Recovery> r) {
// TODO(shess): Crash / crash and dump?
r->Shutdown(POISON);
}
Recovery::Recovery(Database* connection)
: db_(connection),
recover_db_({
.exclusive_locking = false,
.page_size = db_->page_size(),
// The interface to the recovery module is a virtual table.
.enable_virtual_tables_discouraged = true,
}) {
// Files with I/O errors cannot be safely memory-mapped.
recover_db_.set_mmap_disabled();
// TODO(shess): This may not handle cases where the default page
// size is used, but the default has changed. I do not think this
// has ever happened. This could be handled by using "PRAGMA
// page_size", at the cost of potential additional failure cases.
}
Recovery::~Recovery() {
Shutdown(RAZE_AND_POISON);
}
bool Recovery::Init(const base::FilePath& db_path) {
#if DCHECK_IS_ON()
// set_error_callback() will DCHECK if the database already has an error
// callback. The recovery process is likely to result in SQLite errors, and
// those shouldn't get surfaced to any callback.
db_->set_error_callback(base::DoNothing());
// Undo the set_error_callback() above. We only used it for its DCHECK
// behavior.
db_->reset_error_callback();
#endif // DCHECK_IS_ON()
// Break any outstanding transactions on the original database to
// prevent deadlocks reading through the attached version.
// TODO(shess): A client may legitimately wish to recover from
// within the transaction context, because it would potentially
// preserve any in-flight changes. Unfortunately, any attach-based
// system could not handle that. A system which manually queried
// one database and stored to the other possibly could, but would be
// more complicated.
db_->RollbackAllTransactions();
// Disable exclusive locking mode so that the attached database can
// access things. The locking_mode change is not active until the
// next database access, so immediately force an access. Enabling
// writable_schema allows processing through certain kinds of
// corruption.
// TODO(shess): It would be better to just close the handle, but it
// is necessary for the final backup which rewrites things. It
// might be reasonable to close then re-open the handle.
std::ignore = db_->Execute("PRAGMA writable_schema=1");
std::ignore = db_->Execute("PRAGMA locking_mode=NORMAL");
std::ignore = db_->Execute("SELECT COUNT(*) FROM sqlite_schema");
// TODO(shess): If this is a common failure case, it might be
// possible to fall back to a memory database. But it probably
// implies that the SQLite tmpdir logic is busted, which could cause
// a variety of other random issues in our code.
if (!recover_db_.OpenTemporary(base::PassKey<Recovery>()))
return false;
// Enable the recover virtual table for this connection.
int rc = EnableRecoveryExtension(&recover_db_, InternalApiToken());
if (rc != SQLITE_OK) {
LOG(ERROR) << "Failed to initialize recover module: "
<< recover_db_.GetErrorMessage();
return false;
}
// Turn on |SQLITE_RecoveryMode| for the handle, which allows
// reading certain broken databases.
if (!recover_db_.Execute("PRAGMA writable_schema=1"))
return false;
if (!recover_db_.AttachDatabase(db_path, "corrupt", InternalApiToken()))
return false;
return true;
}
bool Recovery::Backup() {
CHECK(db_);
CHECK(recover_db_.is_open());
// TODO(shess): Some of the failure cases here may need further
// exploration. Just as elsewhere, persistent problems probably
// need to be razed, while anything which might succeed on a future
// run probably should be allowed to try. But since Raze() uses the
// same approach, even that wouldn't work when this code fails.
//
// The documentation for the backup system indicate a relatively
// small number of errors are expected:
// SQLITE_BUSY - cannot lock the destination database. This should
// only happen if someone has another handle to the
// database, Chromium generally doesn't do that.
// SQLITE_LOCKED - someone locked the source database. Should be
// impossible (perhaps anti-virus could?).
// SQLITE_READONLY - destination is read-only.
// SQLITE_IOERR - since source database is temporary, probably
// indicates that the destination contains blocks
// throwing errors, or gross filesystem errors.
// SQLITE_NOMEM - out of memory, should be transient.
//
// AFAICT, SQLITE_BUSY and SQLITE_NOMEM could perhaps be considered
// transient, with SQLITE_LOCKED being unclear.
//
// SQLITE_READONLY and SQLITE_IOERR are probably persistent, with a
// strong chance that Raze() would not resolve them. If Delete()
// deletes the database file, the code could then re-open the file
// and attempt the backup again.
//
// For now, this code attempts a best effort.
// Backup the original db from the recovered db.
sqlite3_backup* backup = sqlite3_backup_init(
db_->db(InternalApiToken()), kMainDatabaseName,
recover_db_.db(InternalApiToken()), kMainDatabaseName);
if (!backup) {
// Error code is in the destination database handle.
LOG(ERROR) << "sqlite3_backup_init() failed: "
<< sqlite3_errmsg(db_->db(InternalApiToken()));
return false;
}
// -1 backs up the entire database.
int rc = sqlite3_backup_step(backup, -1);
int pages = sqlite3_backup_pagecount(backup);
// TODO(shess): sqlite3_backup_finish() appears to allow returning a
// different value from sqlite3_backup_step(). Circle back and
// figure out if that can usefully inform the decision of whether to
// retry or not.
sqlite3_backup_finish(backup);
DCHECK_GT(pages, 0);
if (rc != SQLITE_DONE) {
LOG(ERROR) << "sqlite3_backup_step() failed: "
<< sqlite3_errmsg(db_->db(InternalApiToken()));
}
// The destination database was locked. Give up, but leave the data
// in place. Maybe it won't be locked next time.
if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED) {
Shutdown(POISON);
return false;
}
// Running out of memory should be transient, retry later.
if (rc == SQLITE_NOMEM) {
Shutdown(POISON);
return false;
}
// TODO(shess): For now, leave the original database alone. Some errors should
// probably route to RAZE_AND_POISON.
if (rc != SQLITE_DONE) {
Shutdown(POISON);
return false;
}
// Clean up the recovery db, and terminate the main database
// connection.
Shutdown(POISON);
return true;
}
void Recovery::Shutdown(Recovery::Disposition raze) {
if (!db_)
return;
recover_db_.Close();
if (raze == RAZE_AND_POISON) {
db_->RazeAndPoison();
} else if (raze == POISON) {
db_->Poison();
}
db_ = nullptr;
}
bool Recovery::AutoRecoverTable(const char* table_name,
size_t* rows_recovered) {
// Query the info for the recovered table in database [main].
std::string query(
base::StringPrintf("PRAGMA main.table_info(%s)", table_name));
Statement s(db()->GetUniqueStatement(query.c_str()));
// The columns of the recover virtual table.
std::vector<std::string> create_column_decls;
// The columns to select from the recover virtual table when copying
// to the recovered table.
std::vector<std::string> insert_columns;
// If PRIMARY KEY is a single INTEGER column, then it is an alias
// for ROWID. The primary key can be compound, so this can only be
// determined after processing all column data and tracking what is
// seen. |pk_column_count| counts the columns in the primary key.
// |rowid_decl| stores the ROWID version of the last INTEGER column
// seen, which is at |rowid_ofs| in |create_column_decls|.
size_t pk_column_count = 0;
size_t rowid_ofs = 0; // Only valid if rowid_decl is set.
std::string rowid_decl; // ROWID version of column |rowid_ofs|.
while (s.Step()) {
const std::string column_name(s.ColumnString(1));
const std::string column_type(s.ColumnString(2));
const ColumnType default_type = s.GetColumnType(4);
const bool default_is_null = (default_type == ColumnType::kNull);
const int pk_column = s.ColumnInt(5);
// http://www.sqlite.org/pragma.html#pragma_table_info documents column 5 as
// the 1-based index of the column in the primary key, otherwise 0.
if (pk_column > 0)
++pk_column_count;
// Construct column declaration as "name type [optional constraint]".
std::string column_decl = column_name;
// SQLite's affinity detection is documented at:
// http://www.sqlite.org/datatype3.html#affname
// The gist of it is that CHAR, TEXT, and INT use substring matches.
// TODO(shess): It would be nice to unit test the type handling,
// but it is not obvious to me how to write a test which would
// fail appropriately when something was broken. It would have to
// somehow use data which would allow detecting the various type
// coercions which happen. If STRICT could be enabled, type
// mismatches could be detected by which rows are filtered.
if (base::Contains(column_type, "INT")) {
if (pk_column == 1) {
rowid_ofs = create_column_decls.size();
rowid_decl = column_name + " ROWID";
}
column_decl += " INTEGER";
} else if (base::Contains(column_type, "CHAR") ||
base::Contains(column_type, "TEXT")) {
column_decl += " TEXT";
} else if (column_type == "BLOB") {
column_decl += " BLOB";
} else if (base::Contains(column_type, "DOUB")) {
column_decl += " FLOAT";
} else {
// TODO(shess): AFAICT, there remain:
// - contains("CLOB") -> TEXT
// - contains("REAL") -> FLOAT
// - contains("FLOA") -> FLOAT
// - other -> "NUMERIC"
// Just code those in as they come up.
NOTREACHED() << " Unsupported type " << column_type;
return false;
}
create_column_decls.push_back(column_decl);
// Per the NOTE in the header file, convert NULL values to the
// DEFAULT. All columns could be IFNULL(column_name,default), but
// the NULL case would require special handling either way.
if (default_is_null) {
insert_columns.push_back(column_name);
} else {
// The default value appears to be pre-quoted, as if it is
// literally from the sqlite_schema CREATE statement.
std::string default_value = s.ColumnString(4);
insert_columns.push_back(base::StringPrintf(
"IFNULL(%s,%s)", column_name.c_str(), default_value.c_str()));
}
}
// Receiving no column information implies that the table doesn't exist.
if (create_column_decls.empty()) {
return false;
}
// If the PRIMARY KEY was a single INTEGER column, convert it to ROWID.
if (pk_column_count == 1 && !rowid_decl.empty())
create_column_decls[rowid_ofs] = rowid_decl;
std::string recover_create(base::StringPrintf(
"CREATE VIRTUAL TABLE temp.recover_%s USING recover(corrupt.%s, %s)",
table_name, table_name,
base::JoinString(create_column_decls, ",").c_str()));
// INSERT OR IGNORE means that it will drop rows resulting from constraint
// violations. INSERT OR REPLACE only handles UNIQUE constraint violations.
std::string recover_insert(base::StringPrintf(
"INSERT OR IGNORE INTO main.%s SELECT %s FROM temp.recover_%s",
table_name, base::JoinString(insert_columns, ",").c_str(), table_name));
std::string recover_drop(
base::StringPrintf("DROP TABLE temp.recover_%s", table_name));
if (!db()->Execute(recover_create.c_str()))
return false;
if (!db()->Execute(recover_insert.c_str())) {
std::ignore = db()->Execute(recover_drop.c_str());
return false;
}
*rows_recovered = db()->GetLastChangeCount();
// TODO(shess): Is leaving the recover table around a breaker?
return db()->Execute(recover_drop.c_str());
}
bool Recovery::SetupMeta() {
// clang-format off
static const char kCreateSql[] =
"CREATE VIRTUAL TABLE temp.recover_meta USING recover("
"corrupt.meta,"
"key TEXT NOT NULL,"
"value ANY" // Whatever is stored.
")";
// clang-format on
return db()->Execute(kCreateSql);
}
bool Recovery::GetMetaVersionNumber(int* version) {
DCHECK(version);
// TODO(shess): DCHECK(db()->DoesTableExist("temp.recover_meta"));
// Unfortunately, DoesTableExist() queries sqlite_schema, not
// sqlite_temp_master.
static const char kVersionSql[] =
"SELECT value FROM temp.recover_meta WHERE key = 'version'";
sql::Statement recovery_version(db()->GetUniqueStatement(kVersionSql));
if (!recovery_version.Step())
return false;
*version = recovery_version.ColumnInt(0);
return true;
}
namespace {
// Collect statements from [corrupt.sqlite_schema.sql] which start with |prefix|
// (which should be a valid SQL string ending with the space before a table
// name), then apply the statements to [main]. Skip any table named
// 'sqlite_sequence', as that table is created on demand by SQLite if any tables
// use AUTOINCREMENT.
//
// Returns |true| if all of the matching items were created in the main
// database. Returns |false| if an item fails on creation, or if the corrupt
// database schema cannot be queried.
bool SchemaCopyHelper(Database* db, const char* prefix) {
const size_t prefix_len = strlen(prefix);
DCHECK_EQ(' ', prefix[prefix_len - 1]);
sql::Statement s(
db->GetUniqueStatement("SELECT DISTINCT sql FROM corrupt.sqlite_schema "
"WHERE name<>'sqlite_sequence'"));
while (s.Step()) {
std::string sql = s.ColumnString(0);
// Skip statements that don't start with |prefix|.
if (sql.compare(0, prefix_len, prefix) != 0)
continue;
sql.insert(prefix_len, "main.");
if (!db->Execute(sql.c_str()))
return false;
}
return s.Succeeded();
}
} // namespace
// This method is derived from SQLite's vacuum.c. VACUUM operates very
// similarily, creating a new database, populating the schema, then copying the
// data.
//
// TODO(shess): This conservatively uses Rollback() rather than Unrecoverable().
// With Rollback(), it is expected that the database will continue to generate
// errors. Change the failure cases to Unrecoverable().
//
// static
std::unique_ptr<Recovery> Recovery::BeginRecoverDatabase(
Database* db,
const base::FilePath& db_path) {
std::unique_ptr<sql::Recovery> recovery = sql::Recovery::Begin(db, db_path);
if (!recovery) {
// Close the underlying sqlite* handle. Windows does not allow deleting
// open files, and all platforms block opening a second sqlite3* handle
// against a database when exclusive locking is set.
db->Poison();
// When this code was written, histograms showed that most failures happened
// while attaching a corrupt database. In this case, a large proportion of
// attachment failures were SQLITE_NOTADB.
//
// We currently only delete the database in that specific failure case.
{
Database probe_db;
if (!probe_db.OpenInMemory() ||
probe_db.AttachDatabase(db_path, "corrupt", InternalApiToken()) ||
probe_db.GetErrorCode() != SQLITE_NOTADB) {
return nullptr;
}
}
// The database has invalid data in the SQLite header, so it is almost
// certainly not recoverable without manual intervention (and likely not
// recoverable _with_ manual intervention). Clear away the broken database.
if (!sql::Database::Delete(db_path))
return nullptr;
// Windows deletion is complicated by file scanners and malware - sometimes
// Delete() appears to succeed, even though the file remains. The following
// attempts to track if this happens often enough to cause concern.
{
Database probe_db;
if (!probe_db.Open(db_path))
return nullptr;
if (!probe_db.Execute("PRAGMA auto_vacuum"))
return nullptr;
}
// The rest of the recovery code could be run on the re-opened database, but
// the database is empty, so there would be no point.
return nullptr;
}
#if DCHECK_IS_ON()
// This code silently fails to recover fts3 virtual tables. At this time no
// browser database contain fts3 tables. Just to be safe, complain loudly if
// the database contains virtual tables.
//
// fts3 has an [x_segdir] table containing a column [end_block INTEGER]. But
// it actually stores either an integer or a text containing a pair of
// integers separated by a space. AutoRecoverTable() trusts the INTEGER tag
// when setting up the recover vtable, so those rows get dropped. Setting
// that column to ANY may work.
if (db->is_open()) {
sql::Statement s(db->GetUniqueStatement(
"SELECT 1 FROM sqlite_schema WHERE sql LIKE 'CREATE VIRTUAL TABLE %'"));
DCHECK(!s.Step()) << "Recovery of virtual tables not supported";
}
#endif
// TODO(shess): vacuum.c turns off checks and foreign keys.
// TODO(shess): vacuum.c turns synchronous=OFF for the target. I do not fully
// understand this, as the temporary db should not have a journal file at all.
// Perhaps it does in case of cache spill?
// Copy table schema from [corrupt] to [main].
if (!SchemaCopyHelper(recovery->db(), "CREATE TABLE ") ||
!SchemaCopyHelper(recovery->db(), "CREATE INDEX ") ||
!SchemaCopyHelper(recovery->db(), "CREATE UNIQUE INDEX ")) {
// No RecordRecoveryEvent() here because SchemaCopyHelper() already did.
Recovery::Rollback(std::move(recovery));
return nullptr;
}
// Run auto-recover against each table, skipping the sequence table. This is
// necessary because table recovery can create the sequence table as a side
// effect, so recovering that table inline could lead to duplicate data.
{
sql::Statement s(recovery->db()->GetUniqueStatement(
"SELECT name FROM sqlite_schema WHERE sql LIKE 'CREATE TABLE %' "
"AND name!='sqlite_sequence'"));
while (s.Step()) {
const std::string name = s.ColumnString(0);
size_t rows_recovered;
if (!recovery->AutoRecoverTable(name.c_str(), &rows_recovered)) {
Recovery::Rollback(std::move(recovery));
return nullptr;
}
}
if (!s.Succeeded()) {
Recovery::Rollback(std::move(recovery));
return nullptr;
}
}
// Overwrite any sequences created.
if (recovery->db()->DoesTableExist("corrupt.sqlite_sequence")) {
std::ignore = recovery->db()->Execute("DELETE FROM main.sqlite_sequence");
size_t rows_recovered;
if (!recovery->AutoRecoverTable("sqlite_sequence", &rows_recovered)) {
Recovery::Rollback(std::move(recovery));
return nullptr;
}
}
// Copy triggers and views directly to sqlite_schema. Any tables they refer
// to should already exist.
static const char kCreateMetaItemsSql[] =
"INSERT INTO main.sqlite_schema "
"SELECT type, name, tbl_name, rootpage, sql "
"FROM corrupt.sqlite_schema WHERE type='view' OR type='trigger'";
if (!recovery->db()->Execute(kCreateMetaItemsSql)) {
Recovery::Rollback(std::move(recovery));
return nullptr;
}
return recovery;
}
void Recovery::RecoverDatabase(Database* db, const base::FilePath& db_path) {
std::unique_ptr<sql::Recovery> recovery = BeginRecoverDatabase(db, db_path);
if (recovery)
std::ignore = Recovery::Recovered(std::move(recovery));
}
void Recovery::RecoverDatabaseWithMetaVersion(Database* db,
const base::FilePath& db_path) {
std::unique_ptr<sql::Recovery> recovery = BeginRecoverDatabase(db, db_path);
if (!recovery)
return;
int version = 0;
if (!recovery->SetupMeta() || !recovery->GetMetaVersionNumber(&version)) {
sql::Recovery::Unrecoverable(std::move(recovery));
return;
}
std::ignore = Recovery::Recovered(std::move(recovery));
}
// static
bool Recovery::ShouldRecover(int extended_error) {
// Trim extended error codes.
int error = extended_error & 0xFF;
switch (error) {
case SQLITE_NOTADB:
// SQLITE_NOTADB happens if the SQLite header is broken. Some earlier
// versions of SQLite return this where other versions return
// SQLITE_CORRUPT, which is a recoverable case. Later versions only
// return this error only in unrecoverable cases, in which case recovery
// will fail with no changes to the database, so there's no harm in
// attempting recovery in this case.
return true;
case SQLITE_CORRUPT:
// SQLITE_CORRUPT generally means that the database is readable as a
// SQLite database, but some inconsistency has been detected by SQLite.
// In many cases the inconsistency is relatively trivial, such as if an
// index refers to a row which was deleted, in which case most or even all
// of the data can be recovered. This can also be reported if parts of
// the file have been overwritten with garbage data, in which recovery
// should be able to recover partial data.
return true;
// TODO(shess): Possible future options for automated fixing:
// - SQLITE_CANTOPEN - delete the broken symlink or directory.
// - SQLITE_PERM - permissions could be fixed.
// - SQLITE_READONLY - permissions could be fixed.
// - SQLITE_IOERR - rewrite using new blocks.
// - SQLITE_FULL - recover in memory and rewrite subset of data.
default:
return false;
}
}
// static
int Recovery::EnableRecoveryExtension(Database* db, InternalApiToken) {
return sql::recover::RegisterRecoverExtension(db->db(InternalApiToken()));
}
} // namespace sql