| // 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/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 "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/sqlite_result_code.h" |
| #include "third_party/sqlite/sqlite3.h" |
| |
| namespace sql { |
| |
| namespace { |
| |
| constexpr char kMainDatabaseName[] = "main"; |
| |
| } // namespace |
| |
| // static |
| bool Recovery::ShouldAttemptRecovery(Database* database, int extended_error) { |
| return 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 Recovery::RecoverDatabase(Database* database, |
| Strategy strategy) { |
| auto recovery = Recovery(database, strategy); |
| return recovery.RecoverAndReplaceDatabase(); |
| } |
| |
| // static |
| bool Recovery::RecoverIfPossible(Database* database, |
| int extended_error, |
| Strategy strategy) { |
| if (!ShouldAttemptRecovery(database, 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(); |
| |
| auto result = Recovery::RecoverDatabase(database, strategy); |
| if (!IsSqliteSuccessCode(result)) { |
| DLOG(ERROR) << "Database recovery failed with result code: " << result; |
| } |
| |
| return true; |
| } |
| |
| Recovery::Recovery(Database* database, Strategy strategy) |
| : strategy_(strategy), |
| db_(database), |
| recover_db_(sql::DatabaseOptions{ |
| .page_size = database ? database->page_size() : 0, |
| .cache_size = 0, |
| }) { |
| 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(); |
| } |
| |
| Recovery::~Recovery() { |
| // 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 Recovery::SetRecoverySucceeded() { |
| // Recovery result must only be set once. |
| CHECK_EQ(result_, Result::kUnknown); |
| |
| result_ = Result::kSuccess; |
| } |
| |
| void Recovery::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 Recovery::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 Recovery::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 Recovery::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 Recovery::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; |
| } |
| |
| } // namespace sql |