Bug 1432583 - Better corruption handling for favicons.sqlite. r=standard8
authorMarco Bonardo <mbonardo@mozilla.com>
Wed, 09 May 2018 14:53:59 +0200
changeset 472984 2204626cd4f80564ade6a1e3b41d1cacfb43173b
parent 472983 e03a8d74e9eb187265e6033b70620134308fc558
child 472985 ac5b12d16f70e702a5a97dde4438c1b7e0b8ee95
push id9374
push userjlund@mozilla.com
push dateMon, 18 Jun 2018 21:43:20 +0000
treeherdermozilla-beta@160e085dfb0b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersstandard8
bugs1432583
milestone62.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1432583 - Better corruption handling for favicons.sqlite. r=standard8 MozReview-Commit-ID: LEotg2dbibB
toolkit/components/places/Database.cpp
toolkit/components/places/Database.h
toolkit/components/places/PlacesDBUtils.jsm
toolkit/components/places/tests/head_common.js
toolkit/components/places/tests/maintenance/corruptDB.sqlite
toolkit/components/places/tests/maintenance/head.js
toolkit/components/places/tests/maintenance/test_corrupt_favicons.js
toolkit/components/places/tests/maintenance/test_corrupt_favicons_schema.js
toolkit/components/places/tests/maintenance/test_corrupt_places_schema.js
toolkit/components/places/tests/maintenance/test_corrupt_telemetry.js
toolkit/components/places/tests/maintenance/test_favicons_replaceOnStartup.js
toolkit/components/places/tests/maintenance/test_favicons_replaceOnStartup_clone.js
toolkit/components/places/tests/maintenance/test_places_replaceOnStartup.js
toolkit/components/places/tests/maintenance/test_places_replaceOnStartup_clone.js
toolkit/components/places/tests/maintenance/test_preventive_maintenance.js
toolkit/components/places/tests/maintenance/test_preventive_maintenance_checkAndFixDatabase.js
toolkit/components/places/tests/maintenance/test_preventive_maintenance_runTasks.js
toolkit/components/places/tests/maintenance/xpcshell.ini
toolkit/components/places/tests/moz.build
toolkit/components/places/tests/unit/corruptDB.sqlite
toolkit/components/places/tests/unit/test_corrupt_telemetry.js
toolkit/components/places/tests/unit/test_database_replaceOnStartup.js
toolkit/components/places/tests/unit/test_preventive_maintenance.js
toolkit/components/places/tests/unit/test_preventive_maintenance_checkAndFixDatabase.js
toolkit/components/places/tests/unit/test_preventive_maintenance_runTasks.js
toolkit/components/places/tests/unit/xpcshell.ini
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -37,24 +37,22 @@
 
 #include "nsXULAppAPI.h"
 
 // Time between corrupt database backups.
 #define RECENT_BACKUP_TIME_MICROSEC (int64_t)86400 * PR_USEC_PER_SEC // 24H
 
 // Filename of the database.
 #define DATABASE_FILENAME NS_LITERAL_STRING("places.sqlite")
-// Filename used to backup corrupt databases.
-#define DATABASE_CORRUPT_FILENAME NS_LITERAL_STRING("places.sqlite.corrupt")
-#define DATABASE_RECOVER_FILENAME NS_LITERAL_STRING("places.sqlite.recover")
 // Filename of the icons database.
 #define DATABASE_FAVICONS_FILENAME NS_LITERAL_STRING("favicons.sqlite")
 
-// Set when the database file was found corrupt by a previous maintenance.
-#define PREF_FORCE_DATABASE_REPLACEMENT "places.database.replaceOnStartup"
+// Set to the database file name when it was found corrupt by a previous
+// maintenance run.
+#define PREF_FORCE_DATABASE_REPLACEMENT "places.database.replaceDatabaseOnStartup"
 
 // Whether on corruption we should try to fix the database by cloning it.
 #define PREF_DATABASE_CLONEONCORRUPTION "places.database.cloneOnCorruption"
 
 // Set to specify the size of the places database growth increments in kibibytes
 #define PREF_GROWTH_INCREMENT_KIB "places.database.growthIncrementKiB"
 
 // Set to disable the default robust storage and use volatile, in-memory
@@ -129,50 +127,49 @@ using namespace mozilla;
 namespace mozilla {
 namespace places {
 
 namespace {
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Helpers
 
+
 /**
- * Checks whether exists a database backup created not longer than
+ * Get the filename for a corrupt database.
+ */
+nsString getCorruptFilename(const nsString& aDbFilename) {
+  return aDbFilename + NS_LITERAL_STRING(".corrupt");
+}
+/**
+ * Get the filename for a recover database.
+ */
+nsString getRecoverFilename(const nsString& aDbFilename) {
+  return aDbFilename + NS_LITERAL_STRING(".recover");
+}
+
+/**
+ * Checks whether exists a corrupt database file created not longer than
  * RECENT_BACKUP_TIME_MICROSEC ago.
  */
 bool
-hasRecentCorruptDB()
+isRecentCorruptFile(const nsCOMPtr<nsIFile>& aCorruptFile)
 {
   MOZ_ASSERT(NS_IsMainThread());
-
-  nsCOMPtr<nsIFile> profDir;
-  NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(profDir));
-  NS_ENSURE_TRUE(profDir, false);
-  nsCOMPtr<nsISimpleEnumerator> entries;
-  profDir->GetDirectoryEntries(getter_AddRefs(entries));
-  NS_ENSURE_TRUE(entries, false);
-  bool hasMore;
-  while (NS_SUCCEEDED(entries->HasMoreElements(&hasMore)) && hasMore) {
-    nsCOMPtr<nsISupports> next;
-    entries->GetNext(getter_AddRefs(next));
-    NS_ENSURE_TRUE(next, false);
-    nsCOMPtr<nsIFile> currFile = do_QueryInterface(next);
-    NS_ENSURE_TRUE(currFile, false);
-
-    nsAutoString leafName;
-    if (NS_SUCCEEDED(currFile->GetLeafName(leafName)) &&
-        leafName.Length() >= DATABASE_CORRUPT_FILENAME.Length() &&
-        leafName.Find(".corrupt", DATABASE_FILENAME.Length()) != -1) {
-      PRTime lastMod = 0;
-      currFile->GetLastModifiedTime(&lastMod);
-      NS_ENSURE_TRUE(lastMod > 0, false);
-      return (PR_Now() - lastMod) > RECENT_BACKUP_TIME_MICROSEC;
-    }
+  bool fileExists = false;
+  if (NS_FAILED(aCorruptFile->Exists(&fileExists)) || !fileExists) {
+    return false;
   }
-  return false;
+  PRTime lastMod = 0;
+  if (NS_FAILED(aCorruptFile->GetLastModifiedTime(&lastMod)) ||
+      lastMod <= 0 ||
+      (PR_Now() - lastMod) > RECENT_BACKUP_TIME_MICROSEC) {
+    return false;
+  }
+  return true;
 }
 
 /**
  * Sets the connection journal mode to one of the JOURNAL_* types.
  *
  * @param aDBConn
  *        The database connection.
  * @param aJournalMode
@@ -574,34 +571,64 @@ Database::EnsureConnection()
                           this, &Database::NotifyConnectionInitalized)
       );
     });
 
     nsCOMPtr<mozIStorageService> storage =
       do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
     NS_ENSURE_STATE(storage);
 
-    // Init the database file and connect to it.
-    bool databaseCreated = false;
-    nsresult rv = InitDatabaseFile(storage, &databaseCreated);
-    if (NS_SUCCEEDED(rv) && databaseCreated) {
+    nsCOMPtr<nsIFile> profileDir;
+    nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+                                         getter_AddRefs(profileDir));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsCOMPtr<nsIFile> databaseFile;
+    rv = profileDir->Clone(getter_AddRefs(databaseFile));
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = databaseFile->Append(DATABASE_FILENAME);
+    NS_ENSURE_SUCCESS(rv, rv);
+    bool databaseExisted = false;
+    rv = databaseFile->Exists(&databaseExisted);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsAutoString corruptDbName;
+    if (NS_SUCCEEDED(Preferences::GetString(PREF_FORCE_DATABASE_REPLACEMENT,
+                                            corruptDbName)) &&
+        !corruptDbName.IsEmpty()) {
+      // If this pref is set, maintenance required a database replacement, due to
+      // integrity corruption.
+      // Be sure to clear the pref to avoid handling it more than once.
+      (void)Preferences::ClearUser(PREF_FORCE_DATABASE_REPLACEMENT);
+
+      // The database is corrupt, backup and replace it with a new one.
+      nsCOMPtr<nsIFile> fileToBeReplaced;
+      bool fileExists = false;
+      if (NS_SUCCEEDED(profileDir->Clone(getter_AddRefs(fileToBeReplaced))) &&
+          NS_SUCCEEDED(fileToBeReplaced->Exists(&fileExists)) &&
+         fileExists) {
+        rv = BackupAndReplaceDatabaseFile(storage, corruptDbName, true, false);
+        NS_ENSURE_SUCCESS(rv, rv);
+      }
+    }
+
+    // Open the database file.  If it does not exist a new one will be created.
+    // Use an unshared connection, it will consume more memory but avoid shared
+    // cache contentions across threads.
+    rv = storage->OpenUnsharedDatabase(databaseFile, getter_AddRefs(mMainConn));
+    if (NS_SUCCEEDED(rv) && !databaseExisted) {
       mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CREATE;
     }
     else if (rv == NS_ERROR_FILE_CORRUPTED) {
       // The database is corrupt, backup and replace it with a new one.
-      mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CORRUPT;
-      rv = BackupAndReplaceDatabaseFile(storage, true);
+      rv = BackupAndReplaceDatabaseFile(storage, DATABASE_FILENAME, true, true);
       // Fallback to catch-all handler.
     }
     NS_ENSURE_SUCCESS(rv, rv);
 
-    // Ensure the icons database exists.
-    rv = EnsureFaviconsDatabaseFile(storage);
-    NS_ENSURE_SUCCESS(rv, rv);
-
     // Initialize the database schema.  In case of failure the existing schema is
     // is corrupt or incoherent, thus the database should be replaced.
     bool databaseMigrated = false;
     rv = SetupDatabaseConnection(storage);
     bool shouldTryToCloneDb = true;
     if (NS_SUCCEEDED(rv)) {
       // Failing to initialize the schema may indicate a corruption.
       rv = InitSchema(&databaseMigrated);
@@ -629,17 +656,20 @@ Database::EnsureConnection()
     }
     if (NS_WARN_IF(NS_FAILED(rv))) {
       if (rv != NS_ERROR_FILE_IS_LOCKED) {
         mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CORRUPT;
       }
       // Some errors may not indicate a database corruption, for those cases we
       // just bail out without throwing away a possibly valid places.sqlite.
       if (rv == NS_ERROR_FILE_CORRUPTED) {
-        rv = BackupAndReplaceDatabaseFile(storage, shouldTryToCloneDb);
+        // Since we don't know which database is corrupt, we must replace both.
+        rv = BackupAndReplaceDatabaseFile(storage, DATABASE_FAVICONS_FILENAME, false, false);
+        NS_ENSURE_SUCCESS(rv, rv);
+        rv = BackupAndReplaceDatabaseFile(storage, DATABASE_FILENAME, shouldTryToCloneDb, true);
         NS_ENSURE_SUCCESS(rv, rv);
         // Try to initialize the new database again.
         rv = SetupDatabaseConnection(storage);
         NS_ENSURE_SUCCESS(rv, rv);
         rv = InitSchema(&databaseMigrated);
       }
       // Bail out if we couldn't fix the database.
       NS_ENSURE_SUCCESS(rv, rv);
@@ -662,47 +692,48 @@ Database::EnsureConnection()
     initSucceeded = true;
   }
   return NS_OK;
 }
 
 nsresult
 Database::NotifyConnectionInitalized()
 {
+  MOZ_ASSERT(NS_IsMainThread());
   // Notify about Places initialization.
   nsCOMArray<nsIObserver> entries;
   mCacheObservers.GetEntries(entries);
   for (int32_t idx = 0; idx < entries.Count(); ++idx) {
     MOZ_ALWAYS_SUCCEEDS(entries[idx]->Observe(nullptr, TOPIC_PLACES_INIT_COMPLETE, nullptr));
   }
   nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
   if (obs) {
     MOZ_ALWAYS_SUCCEEDS(obs->NotifyObservers(nullptr, TOPIC_PLACES_INIT_COMPLETE, nullptr));
   }
   return NS_OK;
 }
 
 nsresult
-Database::EnsureFaviconsDatabaseFile(nsCOMPtr<mozIStorageService>& aStorage)
+Database::EnsureFaviconsDatabaseAttached(const nsCOMPtr<mozIStorageService>& aStorage)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   nsCOMPtr<nsIFile> databaseFile;
-  nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
-                                       getter_AddRefs(databaseFile));
+  NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(databaseFile));
+  NS_ENSURE_STATE(databaseFile);
+  nsresult rv = databaseFile->Append(DATABASE_FAVICONS_FILENAME);
   NS_ENSURE_SUCCESS(rv, rv);
-  rv = databaseFile->Append(DATABASE_FAVICONS_FILENAME);
+  nsString iconsPath;
+  rv = databaseFile->GetPath(iconsPath);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  bool databaseFileExists = false;
-  rv = databaseFile->Exists(&databaseFileExists);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  if (databaseFileExists) {
-    return NS_OK;
+  bool fileExists = false;
+  if (NS_SUCCEEDED(databaseFile->Exists(&fileExists)) && fileExists) {
+    return AttachDatabase(mMainConn, NS_ConvertUTF16toUTF8(iconsPath),
+                          NS_LITERAL_CSTRING("favicons"));
   }
 
   // Open the database file, this will also create it.
   nsCOMPtr<mozIStorageConnection> conn;
   rv = aStorage->OpenUnsharedDatabase(databaseFile, getter_AddRefs(conn));
   NS_ENSURE_SUCCESS(rv, rv);
 
   {
@@ -742,81 +773,77 @@ Database::EnsureFaviconsDatabaseFile(nsC
     rv = conn->ExecuteSimpleSQL(CREATE_MOZ_ICONS_TO_PAGES);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = transaction.Commit();
     NS_ENSURE_SUCCESS(rv, rv);
 
     // The scope exit will take care of closing the connection.
   }
 
+  rv = AttachDatabase(mMainConn, NS_ConvertUTF16toUTF8(iconsPath),
+                      NS_LITERAL_CSTRING("favicons"));
+  NS_ENSURE_SUCCESS(rv, rv);
+
   return NS_OK;
 }
 
-nsresult
-Database::InitDatabaseFile(nsCOMPtr<mozIStorageService>& aStorage,
-                           bool* aNewDatabaseCreated)
-{
-  MOZ_ASSERT(NS_IsMainThread());
-  *aNewDatabaseCreated = false;
-
-  nsCOMPtr<nsIFile> databaseFile;
-  nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
-                                       getter_AddRefs(databaseFile));
-  NS_ENSURE_SUCCESS(rv, rv);
-  rv = databaseFile->Append(DATABASE_FILENAME);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  bool databaseFileExists = false;
-  rv = databaseFile->Exists(&databaseFileExists);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  if (databaseFileExists &&
-      Preferences::GetBool(PREF_FORCE_DATABASE_REPLACEMENT, false)) {
-    // If this pref is set, Maintenance required a database replacement, due to
-    // integrity corruption.
-    // Be sure to clear the pref to avoid handling it more than once.
-    (void)Preferences::ClearUser(PREF_FORCE_DATABASE_REPLACEMENT);
-
-    return NS_ERROR_FILE_CORRUPTED;
-  }
-
-  // Open the database file.  If it does not exist a new one will be created.
-  // Use an unshared connection, it will consume more memory but avoid shared
-  // cache contentions across threads.
-  rv = aStorage->OpenUnsharedDatabase(databaseFile, getter_AddRefs(mMainConn));
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  *aNewDatabaseCreated = !databaseFileExists;
-  return NS_OK;
-}
 
 nsresult
 Database::BackupAndReplaceDatabaseFile(nsCOMPtr<mozIStorageService>& aStorage,
-                                       bool aTryToClone)
+                                       const nsString& aDbFilename,
+                                       bool aTryToClone,
+                                       bool aReopenConnection)
 {
   MOZ_ASSERT(NS_IsMainThread());
+
+  if (aDbFilename.Equals(DATABASE_FILENAME)) {
+    mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CORRUPT;
+  } else {
+    // Due to OS file lockings, attached databases can't be cloned properly,
+    // otherwise trying to reattach them later would fail.
+    aTryToClone = false;
+  }
+
   nsCOMPtr<nsIFile> profDir;
   nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
                                        getter_AddRefs(profDir));
   NS_ENSURE_SUCCESS(rv, rv);
   nsCOMPtr<nsIFile> databaseFile;
   rv = profDir->Clone(getter_AddRefs(databaseFile));
   NS_ENSURE_SUCCESS(rv, rv);
-  rv = databaseFile->Append(DATABASE_FILENAME);
+  rv = databaseFile->Append(aDbFilename);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // If we have
-  // already failed in the last 24 hours avoid to create another corrupt file,
+  // If we already failed in the last 24 hours avoid to create another corrupt file,
   // since doing so, in some situation, could cause us to create a new corrupt
   // file at every try to access any Places service.  That is bad because it
   // would quickly fill the user's disk space without any notice.
-  if (!hasRecentCorruptDB()) {
+  nsCOMPtr<nsIFile> corruptFile;
+  rv = profDir->Clone(getter_AddRefs(corruptFile));
+  NS_ENSURE_SUCCESS(rv, rv);
+  nsString corruptFilename = getCorruptFilename(aDbFilename);
+  rv = corruptFile->Append(corruptFilename);
+  NS_ENSURE_SUCCESS(rv, rv);
+  if (!isRecentCorruptFile(corruptFile)) {
+    // Ensure we never create more than one corrupt file.
+    nsCOMPtr<nsIFile> corruptFile;
+    rv = profDir->Clone(getter_AddRefs(corruptFile));
+    NS_ENSURE_SUCCESS(rv, rv);
+    nsString corruptFilename = getCorruptFilename(aDbFilename);
+    rv = corruptFile->Append(corruptFilename);
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = corruptFile->Remove(false);
+    if (NS_FAILED(rv) && rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST &&
+                         rv != NS_ERROR_FILE_NOT_FOUND) {
+      return rv;
+    }
+
     nsCOMPtr<nsIFile> backup;
-    (void)aStorage->BackupDatabaseFile(databaseFile, DATABASE_CORRUPT_FILENAME,
-                                       profDir, getter_AddRefs(backup));
+    Unused << aStorage->BackupDatabaseFile(databaseFile, corruptFilename,
+                                           profDir, getter_AddRefs(backup));
   }
 
   // If anything fails from this point on, we have a stale connection or
   // database file, and there's not much more we can do.
   // The only thing we can try to do is to replace the database on the next
   // startup, and report the problem through telemetry.
   {
     enum eCorruptDBReplaceStage : int8_t {
@@ -830,81 +857,87 @@ Database::BackupAndReplaceDatabaseFile(n
     eCorruptDBReplaceStage stage = stage_closing;
     auto guard = MakeScopeExit([&]() {
       if (stage != stage_replaced) {
         // Reaching this point means the database is corrupt and we failed to
         // replace it.  For this session part of the application related to
         // bookmarks and history will misbehave.  The frontend may show a
         // "locked" notification to the user though.
         // Set up a pref to try replacing the database at the next startup.
-        Preferences::SetBool(PREF_FORCE_DATABASE_REPLACEMENT, true);
+        Preferences::SetString(PREF_FORCE_DATABASE_REPLACEMENT, aDbFilename);
       }
       // Report the corruption through telemetry.
       Telemetry::Accumulate(Telemetry::PLACES_DATABASE_CORRUPTION_HANDLING_STAGE,
                             static_cast<int8_t>(stage));
     });
 
     // Close database connection if open.
     if (mMainConn) {
       rv = mMainConn->SpinningSynchronousClose();
       NS_ENSURE_SUCCESS(rv, rv);
+      mMainConn = nullptr;
     }
 
     // Remove the broken database.
     stage = stage_removing;
     rv = databaseFile->Remove(false);
-    if (NS_FAILED(rv) && rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
+    if (NS_FAILED(rv) && rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST &&
+                         rv != NS_ERROR_FILE_NOT_FOUND) {
       return rv;
     }
 
     // Create a new database file and try to clone tables from the corrupt one.
     bool cloned = false;
     if (aTryToClone && Preferences::GetBool(PREF_DATABASE_CLONEONCORRUPTION, true)) {
       stage = stage_cloning;
-      rv = TryToCloneTablesFromCorruptDatabase(aStorage);
+      rv = TryToCloneTablesFromCorruptDatabase(aStorage, databaseFile);
       if (NS_SUCCEEDED(rv)) {
+        // If we cloned successfully, we should not consider the database
+        // corrupt anymore, otherwise we could reimport default bookmarks.
         mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_OK;
         cloned = true;
       }
     }
 
-    // Use an unshared connection, it will consume more memory but avoid shared
-    // cache contentions across threads.
-    stage = stage_reopening;
-    rv = aStorage->OpenUnsharedDatabase(databaseFile, getter_AddRefs(mMainConn));
-    NS_ENSURE_SUCCESS(rv, rv);
+    if (aReopenConnection) {
+      // Use an unshared connection, it will consume more memory but avoid shared
+      // cache contentions across threads.
+      stage = stage_reopening;
+      rv = aStorage->OpenUnsharedDatabase(databaseFile, getter_AddRefs(mMainConn));
+      NS_ENSURE_SUCCESS(rv, rv);
+    }
 
     stage = cloned ? stage_cloned : stage_replaced;
   }
 
   return NS_OK;
 }
 
 nsresult
-Database::TryToCloneTablesFromCorruptDatabase(nsCOMPtr<mozIStorageService>& aStorage)
+Database::TryToCloneTablesFromCorruptDatabase(const nsCOMPtr<mozIStorageService>& aStorage,
+                                              const nsCOMPtr<nsIFile>& aDatabaseFile)
 {
   MOZ_ASSERT(NS_IsMainThread());
-  nsCOMPtr<nsIFile> profDir;
-  nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
-                                       getter_AddRefs(profDir));
-  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsAutoString filename;
+  nsresult rv = aDatabaseFile->GetLeafName(filename);
 
   nsCOMPtr<nsIFile> corruptFile;
-  rv = profDir->Clone(getter_AddRefs(corruptFile));
+  rv = aDatabaseFile->Clone(getter_AddRefs(corruptFile));
   NS_ENSURE_SUCCESS(rv, rv);
-  rv = corruptFile->Append(DATABASE_CORRUPT_FILENAME);
+  rv = corruptFile->SetLeafName(getCorruptFilename(filename));
   NS_ENSURE_SUCCESS(rv, rv);
   nsAutoString path;
   rv = corruptFile->GetPath(path);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsIFile> recoverFile;
-  rv = profDir->Clone(getter_AddRefs(recoverFile));
+  rv = aDatabaseFile->Clone(getter_AddRefs(recoverFile));
   NS_ENSURE_SUCCESS(rv, rv);
-  rv = recoverFile->Append(DATABASE_RECOVER_FILENAME);
+  rv = recoverFile->SetLeafName(getRecoverFilename(filename));
   NS_ENSURE_SUCCESS(rv, rv);
   // Ensure there's no previous recover file.
   rv = recoverFile->Remove(false);
   if (NS_FAILED(rv) && rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST &&
                        rv != NS_ERROR_FILE_NOT_FOUND) {
     return rv;
   }
 
@@ -979,17 +1012,17 @@ Database::TryToCloneTablesFromCorruptDat
   rv = stmt->Finalize();
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
   Unused << conn->Close();
   conn = nullptr;
-  rv = recoverFile->RenameTo(profDir, DATABASE_FILENAME);
+  rv = recoverFile->RenameTo(nullptr, filename);
   NS_ENSURE_SUCCESS(rv, rv);
   Unused << corruptFile->Remove(false);
 
   guard.release();
   return NS_OK;
 }
 
 nsresult
@@ -1054,35 +1087,31 @@ Database::SetupDatabaseConnection(nsCOMP
     if (stmt && NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
       int32_t fkState = stmt->AsInt32(0);
       MOZ_ASSERT(fkState, "Foreign keys should be enabled");
     }
   }
 #endif
 
   // Attach the favicons database to the main connection.
-  nsCOMPtr<nsIFile> iconsFile;
-  rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
-                              getter_AddRefs(iconsFile));
-  NS_ENSURE_SUCCESS(rv, rv);
-  rv = iconsFile->Append(DATABASE_FAVICONS_FILENAME);
-  NS_ENSURE_SUCCESS(rv, rv);
-  nsString iconsPath;
-  rv = iconsFile->GetPath(iconsPath);
-  NS_ENSURE_SUCCESS(rv, rv);
-  rv = AttachDatabase(mMainConn, NS_ConvertUTF16toUTF8(iconsPath),
-                      NS_LITERAL_CSTRING("favicons"));
+  rv = EnsureFaviconsDatabaseAttached(aStorage);
   if (NS_FAILED(rv)) {
     // The favicons database may be corrupt. Try to replace and reattach it.
-    rv = iconsFile->Remove(true);
+    nsCOMPtr<nsIFile> iconsFile;
+    rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+                                getter_AddRefs(iconsFile));
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = iconsFile->Append(DATABASE_FAVICONS_FILENAME);
     NS_ENSURE_SUCCESS(rv, rv);
-    rv = EnsureFaviconsDatabaseFile(aStorage);
-    NS_ENSURE_SUCCESS(rv, rv);
-    rv = AttachDatabase(mMainConn, NS_ConvertUTF16toUTF8(iconsPath),
-                        NS_LITERAL_CSTRING("favicons"));
+    rv = iconsFile->Remove(false);
+    if (NS_FAILED(rv) && rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST &&
+                         rv != NS_ERROR_FILE_NOT_FOUND) {
+      return rv;
+    }
+    rv = EnsureFaviconsDatabaseAttached(aStorage);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   // Create favicons temp entities.
   rv = mMainConn->ExecuteSimpleSQL(CREATE_ICONS_AFTERINSERT_TRIGGER);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // We use our functions during migration, so initialize them now.
--- a/toolkit/components/places/Database.h
+++ b/toolkit/components/places/Database.h
@@ -235,55 +235,51 @@ protected:
    * Finalizes the cached statements and closes the database connection.
    * A TOPIC_PLACES_CONNECTION_CLOSED notification is fired when done.
    */
   void Shutdown();
 
   bool IsShutdownStarted() const;
 
   /**
-   * Initializes the database file.  If the database does not exist or is
-   * corrupt, a new one is created.  In case of corruption it also creates a
-   * backup copy of the database.
-   *
-   * @param aStorage
-   *        mozStorage service instance.
-   * @param aNewDatabaseCreated
-   *        whether a new database file has been created.
-   */
-  nsresult InitDatabaseFile(nsCOMPtr<mozIStorageService>& aStorage,
-                            bool* aNewDatabaseCreated);
-
-  /**
    * Ensure the favicons database file exists.
    *
    * @param aStorage
    *        mozStorage service instance.
    */
-  nsresult EnsureFaviconsDatabaseFile(nsCOMPtr<mozIStorageService>& aStorage);
+  nsresult EnsureFaviconsDatabaseAttached(const nsCOMPtr<mozIStorageService>& aStorage);
 
   /**
    * Creates a database backup and replaces the original file with a new
    * one.
    *
    * @param aStorage
    *        mozStorage service instance.
+   * @param aDbfilename
+   *        the database file name to replace.
    * @param aTryToClone
    *        whether we should try to clone a corrupt database.
+   * @param aReopenConnection
+   *        whether we should open a new connection to the replaced database.
    */
   nsresult BackupAndReplaceDatabaseFile(nsCOMPtr<mozIStorageService>& aStorage,
-                                        bool aTryToClone);
+                                        const nsString& aDbFilename,
+                                        bool aTryToClone,
+                                        bool aReopenConnection);
 
   /**
    * Tries to recover tables and their contents from a corrupt database.
    *
    * @param aStorage
    *        mozStorage service instance.
+   * @param aDatabaseFile
+   *        nsIFile pointing to the places.sqlite file considered corrupt.
    */
-  nsresult TryToCloneTablesFromCorruptDatabase(nsCOMPtr<mozIStorageService>& aStorage);
+  nsresult TryToCloneTablesFromCorruptDatabase(const nsCOMPtr<mozIStorageService>& aStorage,
+                                               const nsCOMPtr<nsIFile>& aDatabaseFile);
 
   /**
    * Set up the connection environment through PRAGMAs.
    * Will return NS_ERROR_FILE_CORRUPTED if any critical setting fails.
    *
    * @param aStorage
    *        mozStorage service instance.
    */
--- a/toolkit/components/places/PlacesDBUtils.jsm
+++ b/toolkit/components/places/PlacesDBUtils.jsm
@@ -6,16 +6,17 @@
 
 const BYTES_PER_MEBIBYTE = 1048576;
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetters(this, {
   Services: "resource://gre/modules/Services.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+  Sqlite: "resource://gre/modules/Sqlite.jsm",
 });
 
 var EXPORTED_SYMBOLS = [ "PlacesDBUtils" ];
 
 var PlacesDBUtils = {
   _isShuttingDown: false,
   shutdown() {
     PlacesDBUtils._isShuttingDown = true;
@@ -100,54 +101,34 @@ var PlacesDBUtils = {
    *
    * @return {Promise} resolves if database is sane or is made sane.
    * @resolves to an array of logs for this task.
    * @rejects if we're unable to fix corruption or unable to check status.
    */
   async checkIntegrity() {
     let logs = [];
 
-    async function integrity(db) {
-      let row;
-      await db.execute("PRAGMA integrity_check", null, (r, cancel) => {
-        row = r;
-        cancel();
-      });
-      return row.getResultByIndex(0) === "ok";
-    }
-    try {
-      // Run a integrity check, but stop at the first error.
-      await PlacesUtils.withConnectionWrapper("PlacesDBUtils: check the integrity", async db => {
-        let isOk = await integrity(db);
-        if (isOk) {
-          logs.push("The database is sane");
-        } else {
-          // We stopped due to an integrity corruption, try to fix if possible.
-          logs.push("The database is corrupt");
-          // Try to reindex, this often fixes simple indices corruption.
-          await db.execute("REINDEX");
-          logs.push("The database has been REINDEXed");
-          isOk = await integrity(db);
-          if (isOk) {
-            logs.push("The database is now sane");
-          } else {
-            logs.push("The database is still corrupt");
-            Services.prefs.setBoolPref("places.database.replaceOnStartup", true);
-            PlacesDBUtils.clearPendingTasks();
-            throw new Error("Unable to fix corruption, database will be replaced on next startup");
-          }
+    async function check(dbName) {
+      try {
+        await integrity(dbName);
+        logs.push(`The ${dbName} database is sane`);
+      } catch (ex) {
+        PlacesDBUtils.clearPendingTasks();
+        if (ex.status == Cr.NS_ERROR_FILE_CORRUPTED) {
+          logs.push(`The ${dbName} database is corrupt`);
+          Services.prefs.setCharPref("places.database.replaceDatabaseOnStartup", dbName);
+          throw new Error(`Unable to fix corruption, ${dbName} will be replaced on next startup`);
         }
-      });
-    } catch (ex) {
-      if (ex.message.indexOf("Unable to fix corruption") !== 0) {
-        // There was some other error, so throw.
-        PlacesDBUtils.clearPendingTasks();
-        throw new Error("Unable to check database integrity");
+        throw new Error(`Unable to check ${dbName} integrity: ${ex}`);
       }
     }
+
+    await check("places.sqlite");
+    await check("favicons.sqlite");
+
     return logs;
   },
 
   invalidateCaches() {
     let logs = [];
     return PlacesUtils.withConnectionWrapper("PlacesDBUtils: invalidate caches", async db => {
       let idsWithInvalidGuidsRows = await db.execute(`
         SELECT id FROM moz_bookmarks
@@ -1081,8 +1062,50 @@ var PlacesDBUtils = {
       let result =
           await task().then(logs => { return { succeeded: true, logs }; })
                       .catch(err => { return { succeeded: false, logs: [err.message] }; });
       tasksMap.set(task.name, result);
     }
     return tasksMap;
   }
 };
+
+async function integrity(dbName) {
+  async function check(db) {
+    let row;
+    await db.execute("PRAGMA integrity_check", null, (r, cancel) => {
+      row = r;
+      cancel();
+    });
+    return row.getResultByIndex(0) === "ok";
+  }
+
+  // Create a new connection for this check, so we can operate independently
+  // from a broken Places service.
+  // openConnection returns an exception with .status == Cr.NS_ERROR_FILE_CORRUPTED,
+  // we should do the same everywhere we want maintenance to try replacing the
+  // database on next startup.
+  let path = OS.Path.join(OS.Constants.Path.profileDir, dbName);
+  let db = await Sqlite.openConnection({ path });
+  try {
+    if (await check(db))
+      return;
+
+    // We stopped due to an integrity corruption, try to fix it if possible.
+    // First, try to reindex, this often fixes simple indices problems.
+    try {
+      await db.execute("REINDEX");
+    } catch (ex) {
+      let error = new Error("Impossible to reindex database");
+      error.status = Cr.NS_ERROR_FILE_CORRUPTED;
+      throw error;
+    }
+
+    // Check again.
+    if (!await check(db)) {
+      let error = new Error("The database is still corrupt");
+      error.status = Cr.NS_ERROR_FILE_CORRUPTED;
+      throw error;
+    }
+  } finally {
+    await db.close();
+  }
+}
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -918,23 +918,24 @@ function mapItemIdToInternalRootName(aIt
 const DB_FILENAME = "places.sqlite";
 
 /**
  * Sets the database to use for the given test.  This should be the very first
  * thing in the test, otherwise this database will not be used!
  *
  * @param aFileName
  *        The filename of the database to use.  This database must exist in
- *        toolkit/components/places/tests/migration!
- * @return {Promise}
+ *        the test folder.
+ * @return {Promise} the final path to the database
  */
-var setupPlacesDatabase = async function(aFileName, aDestFileName = DB_FILENAME) {
+async function setupPlacesDatabase(aFileName, aDestFileName = DB_FILENAME) {
   let currentDir = await OS.File.getCurrentDirectory();
 
   let src = OS.Path.join(currentDir, aFileName);
   Assert.ok((await OS.File.exists(src)), "Database file found");
 
   // Ensure that our database doesn't already exist.
   let dest = OS.Path.join(OS.Constants.Path.profileDir, aDestFileName);
   Assert.ok(!(await OS.File.exists(dest)), "Database file should not exist yet");
 
   await OS.File.copy(src, dest);
-};
+  return dest;
+}
rename from toolkit/components/places/tests/unit/corruptDB.sqlite
rename to toolkit/components/places/tests/maintenance/corruptDB.sqlite
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/maintenance/head.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+// Import common head.
+{
+  /* import-globals-from ../head_common.js */
+  let commonFile = do_get_file("../head_common.js", false);
+  let uri = Services.io.newFileURI(commonFile);
+  Services.scriptloader.loadSubScript(uri.spec, this);
+}
+
+// Put any other stuff relative to this test folder below.
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  PlacesDBUtils: "resource://gre/modules/PlacesDBUtils.jsm",
+});
+
+async function createCorruptDb(filename) {
+  let path = OS.Path.join(OS.Constants.Path.profileDir, filename);
+  await OS.File.remove(path, {ignoreAbsent: true});
+  // Create a corrupt database.
+  let dir = await OS.File.getCurrentDirectory();
+  let src = OS.Path.join(dir, "corruptDB.sqlite");
+  await OS.File.copy(src, path);
+}
+
+/**
+ * Used in _replaceOnStartup_ tests as common test code. It checks whether we
+ * are properly cloning or replacing a corrupt database.
+ *
+ * @param {string} src
+ *        Path to a test database, relative to this test folder
+ * @param {string} filename
+ *        Database file name
+ * @param {boolean} shouldClone
+ *        Whether we expect the database to be cloned
+ * @param {boolean} dbStatus
+ *        The expected final database status
+ */
+async function test_database_replacement(src, filename, shouldClone, dbStatus) {
+  registerCleanupFunction(() => {
+    Services.prefs.clearUserPref("places.database.cloneOnCorruption");
+  });
+  Services.prefs.setBoolPref("places.database.cloneOnCorruption", shouldClone);
+
+  // Only the main database file (places.sqlite) will be cloned, because
+  // attached databased would break due to OS file lockings.
+  let willClone = shouldClone && filename == DB_FILENAME;
+
+  // Ensure that our databases don't exist yet.
+  let dest = OS.Path.join(OS.Constants.Path.profileDir, filename);
+  Assert.ok(!await OS.File.exists(dest), `"${filename} should not exist initially`);
+  let corrupt = OS.Path.join(OS.Constants.Path.profileDir, `${filename}.corrupt`);
+  Assert.ok(!await OS.File.exists(corrupt), `${filename}.corrupt should not exist initially`);
+
+  let dir = await OS.File.getCurrentDirectory();
+  src = OS.Path.join(dir, src);
+  await OS.File.copy(src, dest);
+
+  // Create some unique stuff to check later.
+  let db = await Sqlite.openConnection({ path: dest });
+  await db.execute(`CREATE TABLE moz_cloned (id INTEGER PRIMARY KEY)`);
+  await db.execute(`CREATE TABLE not_cloned (id INTEGER PRIMARY KEY)`);
+  await db.execute(`DELETE FROM moz_cloned`); // Shouldn't throw.
+  await db.close();
+
+  // Open the database with Places.
+  Services.prefs.setCharPref("places.database.replaceDatabaseOnStartup", filename);
+  Assert.equal(PlacesUtils.history.databaseStatus, dbStatus);
+
+  Assert.ok(await OS.File.exists(dest), "The database should exist");
+
+  // Check the new database still contains our special data.
+  db = await Sqlite.openConnection({ path: dest });
+  if (willClone) {
+    await db.execute(`DELETE FROM moz_cloned`); // Shouldn't throw.
+  }
+
+  // Check the new database is really a new one.
+  await Assert.rejects(db.execute(`DELETE FROM not_cloned`),
+                       /no such table/,
+                       "The database should have been replaced");
+  await db.close();
+
+  if (willClone) {
+    Assert.ok(!await OS.File.exists(corrupt), "The corrupt db should not exist");
+  } else {
+    Assert.ok(await OS.File.exists(corrupt), "The corrupt db should exist");
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/maintenance/test_corrupt_favicons.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that history initialization correctly handles a corrupt favicons file
+// that can't be opened.
+
+add_task(async function() {
+  await createCorruptDb("favicons.sqlite");
+
+  Assert.equal(PlacesUtils.history.databaseStatus,
+               PlacesUtils.history.DATABASE_STATUS_CREATE);
+  let db = await PlacesUtils.promiseDBConnection();
+  await db.execute("SELECT * FROM moz_icons"); // Should not fail.
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/maintenance/test_corrupt_favicons_schema.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that history initialization correctly handles a corrupt places schema.
+
+add_task(async function() {
+  let path = await setupPlacesDatabase("../migration/favicons_v41.sqlite");
+
+  // Ensure the database will go through a migration that depends on moz_places
+  // and break the schema by dropping that table.
+  let db = await Sqlite.openConnection({ path });
+  await db.setSchemaVersion(38);
+  await db.execute("DROP TABLE moz_icons");
+  await db.close();
+
+  Assert.equal(PlacesUtils.history.databaseStatus,
+               PlacesUtils.history.DATABASE_STATUS_CORRUPT);
+  db = await PlacesUtils.promiseDBConnection();
+  let rows = await db.execute("SELECT 1 FROM moz_icons");
+  Assert.equal(rows.length, 0, "Found no icons");
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/maintenance/test_corrupt_places_schema.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that history initialization correctly handles a corrupt places schema.
+
+add_task(async function() {
+  let path = await setupPlacesDatabase("../migration/places_v38.sqlite");
+
+  // Ensure the database will go through a migration that depends on moz_places
+  // and break the schema by dropping that table.
+  let db = await Sqlite.openConnection({ path });
+  await db.setSchemaVersion(38);
+  await db.execute("DROP TABLE moz_places");
+  await db.close();
+
+  Assert.equal(PlacesUtils.history.databaseStatus,
+               PlacesUtils.history.DATABASE_STATUS_CORRUPT);
+  db = await PlacesUtils.promiseDBConnection();
+  await db.execute("SELECT * FROM moz_places LIMIT 1"); // Should not fail.
+});
rename from toolkit/components/places/tests/unit/test_corrupt_telemetry.js
rename to toolkit/components/places/tests/maintenance/test_corrupt_telemetry.js
--- a/toolkit/components/places/tests/unit/test_corrupt_telemetry.js
+++ b/toolkit/components/places/tests/maintenance/test_corrupt_telemetry.js
@@ -1,23 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that history initialization correctly handles a request to forcibly
 // replace the current database.
 
 add_task(async function() {
-  let profileDBPath = await OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite");
-  await OS.File.remove(profileDBPath, {ignoreAbsent: true});
-  // Ensure that our database doesn't already exist.
-  Assert.ok(!(await OS.File.exists(profileDBPath)), "places.sqlite shouldn't exist");
-  let dir = await OS.File.getCurrentDirectory();
-  let src = OS.Path.join(dir, "corruptDB.sqlite");
-  await OS.File.copy(src, profileDBPath);
-  Assert.ok(await OS.File.exists(profileDBPath), "places.sqlite should exist");
+  await createCorruptDb("places.sqlite");
 
   let count = Services.telemetry
                       .getHistogramById("PLACES_DATABASE_CORRUPTION_HANDLING_STAGE")
                       .snapshot()
                       .counts[3];
   Assert.equal(count, 0, "There should be no telemetry");
 
   Assert.equal(PlacesUtils.history.databaseStatus,
rename from toolkit/components/places/tests/unit/test_database_replaceOnStartup.js
rename to toolkit/components/places/tests/maintenance/test_favicons_replaceOnStartup.js
--- a/toolkit/components/places/tests/unit/test_database_replaceOnStartup.js
+++ b/toolkit/components/places/tests/maintenance/test_favicons_replaceOnStartup.js
@@ -1,60 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that history initialization correctly handles a request to forcibly
 // replace the current database.
 
 add_task(async function() {
-  registerCleanupFunction(function() {
-    Services.prefs.clearUserPref("places.database.cloneOnCorruption");
-  });
-  test_replacement(true);
-  test_replacement(false);
+  await test_database_replacement("../migration/favicons_v41.sqlite",
+                                  "favicons.sqlite",
+                                  false,
+                                  PlacesUtils.history.DATABASE_STATUS_CREATE);
 });
 
-async function test_replacement(shouldClone) {
-  Services.prefs.setBoolPref("places.database.cloneOnCorruption", shouldClone);
-  // Ensure that our databases don't exist yet.
-  let sane = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite");
-  Assert.ok(!await OS.File.exists(sane), "The db should not exist initially");
-  let corrupt = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite.corrupt");
-  Assert.ok(!await OS.File.exists(corrupt), "The corrupt db should not exist initially");
 
-  let file = do_get_file("../migration/places_v38.sqlite");
-  file.copyToFollowingLinks(gProfD, "places.sqlite");
-  file = gProfD.clone();
-  file.append("places.sqlite");
-
-  // Create some unique stuff to check later.
-  let db = Services.storage.openUnsharedDatabase(file);
-  db.executeSimpleSQL("CREATE TABLE moz_cloned(id INTEGER PRIMARY KEY)");
-  db.executeSimpleSQL("CREATE TABLE not_cloned (id INTEGER PRIMARY KEY)");
-  db.executeSimpleSQL("DELETE FROM moz_cloned"); // Shouldn't throw.
-  db.close();
-
-  // Open the database with Places.
-  Services.prefs.setBoolPref("places.database.replaceOnStartup", true);
-  let expectedStatus = shouldClone ? PlacesUtils.history.DATABASE_STATUS_UPGRADED
-                                   : PlacesUtils.history.DATABASE_STATUS_CORRUPT;
-  Assert.equal(PlacesUtils.history.databaseStatus, expectedStatus);
-
-  Assert.ok(await OS.File.exists(sane), "The database should exist");
-
-  // Check the new database still contains our special data.
-  db = Services.storage.openUnsharedDatabase(file);
-  if (shouldClone) {
-    db.executeSimpleSQL("DELETE FROM moz_cloned"); // Shouldn't throw.
-  }
-
-  // Check the new database is really a new one.
-  Assert.throws(() => {
-    db.executeSimpleSQL("DELETE FROM not_cloned");
-  }, "The database should have been replaced");
-  db.close();
-
-  if (shouldClone) {
-    Assert.ok(!await OS.File.exists(corrupt), "The corrupt db should not exist");
-  } else {
-    Assert.ok(await OS.File.exists(corrupt), "The corrupt db should exist");
-  }
-}
copy from toolkit/components/places/tests/unit/test_database_replaceOnStartup.js
copy to toolkit/components/places/tests/maintenance/test_favicons_replaceOnStartup_clone.js
--- a/toolkit/components/places/tests/unit/test_database_replaceOnStartup.js
+++ b/toolkit/components/places/tests/maintenance/test_favicons_replaceOnStartup_clone.js
@@ -1,60 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that history initialization correctly handles a request to forcibly
 // replace the current database.
 
 add_task(async function() {
-  registerCleanupFunction(function() {
-    Services.prefs.clearUserPref("places.database.cloneOnCorruption");
-  });
-  test_replacement(true);
-  test_replacement(false);
+  // In reality, this won't try to clone the database, because attached
+  // databases cannot be supported when cloning. This test also verifies that.
+  await test_database_replacement("../migration/favicons_v41.sqlite",
+                                  "favicons.sqlite",
+                                  true,
+                                  PlacesUtils.history.DATABASE_STATUS_CREATE);
 });
-
-async function test_replacement(shouldClone) {
-  Services.prefs.setBoolPref("places.database.cloneOnCorruption", shouldClone);
-  // Ensure that our databases don't exist yet.
-  let sane = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite");
-  Assert.ok(!await OS.File.exists(sane), "The db should not exist initially");
-  let corrupt = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite.corrupt");
-  Assert.ok(!await OS.File.exists(corrupt), "The corrupt db should not exist initially");
-
-  let file = do_get_file("../migration/places_v38.sqlite");
-  file.copyToFollowingLinks(gProfD, "places.sqlite");
-  file = gProfD.clone();
-  file.append("places.sqlite");
-
-  // Create some unique stuff to check later.
-  let db = Services.storage.openUnsharedDatabase(file);
-  db.executeSimpleSQL("CREATE TABLE moz_cloned(id INTEGER PRIMARY KEY)");
-  db.executeSimpleSQL("CREATE TABLE not_cloned (id INTEGER PRIMARY KEY)");
-  db.executeSimpleSQL("DELETE FROM moz_cloned"); // Shouldn't throw.
-  db.close();
-
-  // Open the database with Places.
-  Services.prefs.setBoolPref("places.database.replaceOnStartup", true);
-  let expectedStatus = shouldClone ? PlacesUtils.history.DATABASE_STATUS_UPGRADED
-                                   : PlacesUtils.history.DATABASE_STATUS_CORRUPT;
-  Assert.equal(PlacesUtils.history.databaseStatus, expectedStatus);
-
-  Assert.ok(await OS.File.exists(sane), "The database should exist");
-
-  // Check the new database still contains our special data.
-  db = Services.storage.openUnsharedDatabase(file);
-  if (shouldClone) {
-    db.executeSimpleSQL("DELETE FROM moz_cloned"); // Shouldn't throw.
-  }
-
-  // Check the new database is really a new one.
-  Assert.throws(() => {
-    db.executeSimpleSQL("DELETE FROM not_cloned");
-  }, "The database should have been replaced");
-  db.close();
-
-  if (shouldClone) {
-    Assert.ok(!await OS.File.exists(corrupt), "The corrupt db should not exist");
-  } else {
-    Assert.ok(await OS.File.exists(corrupt), "The corrupt db should exist");
-  }
-}
copy from toolkit/components/places/tests/unit/test_database_replaceOnStartup.js
copy to toolkit/components/places/tests/maintenance/test_places_replaceOnStartup.js
--- a/toolkit/components/places/tests/unit/test_database_replaceOnStartup.js
+++ b/toolkit/components/places/tests/maintenance/test_places_replaceOnStartup.js
@@ -1,60 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that history initialization correctly handles a request to forcibly
 // replace the current database.
 
 add_task(async function() {
-  registerCleanupFunction(function() {
-    Services.prefs.clearUserPref("places.database.cloneOnCorruption");
-  });
-  test_replacement(true);
-  test_replacement(false);
+  await test_database_replacement("../migration/places_v43.sqlite",
+                                  "places.sqlite",
+                                  false,
+                                  PlacesUtils.history.DATABASE_STATUS_CORRUPT);
 });
-
-async function test_replacement(shouldClone) {
-  Services.prefs.setBoolPref("places.database.cloneOnCorruption", shouldClone);
-  // Ensure that our databases don't exist yet.
-  let sane = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite");
-  Assert.ok(!await OS.File.exists(sane), "The db should not exist initially");
-  let corrupt = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite.corrupt");
-  Assert.ok(!await OS.File.exists(corrupt), "The corrupt db should not exist initially");
-
-  let file = do_get_file("../migration/places_v38.sqlite");
-  file.copyToFollowingLinks(gProfD, "places.sqlite");
-  file = gProfD.clone();
-  file.append("places.sqlite");
-
-  // Create some unique stuff to check later.
-  let db = Services.storage.openUnsharedDatabase(file);
-  db.executeSimpleSQL("CREATE TABLE moz_cloned(id INTEGER PRIMARY KEY)");
-  db.executeSimpleSQL("CREATE TABLE not_cloned (id INTEGER PRIMARY KEY)");
-  db.executeSimpleSQL("DELETE FROM moz_cloned"); // Shouldn't throw.
-  db.close();
-
-  // Open the database with Places.
-  Services.prefs.setBoolPref("places.database.replaceOnStartup", true);
-  let expectedStatus = shouldClone ? PlacesUtils.history.DATABASE_STATUS_UPGRADED
-                                   : PlacesUtils.history.DATABASE_STATUS_CORRUPT;
-  Assert.equal(PlacesUtils.history.databaseStatus, expectedStatus);
-
-  Assert.ok(await OS.File.exists(sane), "The database should exist");
-
-  // Check the new database still contains our special data.
-  db = Services.storage.openUnsharedDatabase(file);
-  if (shouldClone) {
-    db.executeSimpleSQL("DELETE FROM moz_cloned"); // Shouldn't throw.
-  }
-
-  // Check the new database is really a new one.
-  Assert.throws(() => {
-    db.executeSimpleSQL("DELETE FROM not_cloned");
-  }, "The database should have been replaced");
-  db.close();
-
-  if (shouldClone) {
-    Assert.ok(!await OS.File.exists(corrupt), "The corrupt db should not exist");
-  } else {
-    Assert.ok(await OS.File.exists(corrupt), "The corrupt db should exist");
-  }
-}
copy from toolkit/components/places/tests/unit/test_database_replaceOnStartup.js
copy to toolkit/components/places/tests/maintenance/test_places_replaceOnStartup_clone.js
--- a/toolkit/components/places/tests/unit/test_database_replaceOnStartup.js
+++ b/toolkit/components/places/tests/maintenance/test_places_replaceOnStartup_clone.js
@@ -1,60 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that history initialization correctly handles a request to forcibly
 // replace the current database.
 
 add_task(async function() {
-  registerCleanupFunction(function() {
-    Services.prefs.clearUserPref("places.database.cloneOnCorruption");
-  });
-  test_replacement(true);
-  test_replacement(false);
+  await test_database_replacement("../migration/places_v43.sqlite",
+                                  "places.sqlite",
+                                  true,
+                                  PlacesUtils.history.DATABASE_STATUS_UPGRADED);
 });
-
-async function test_replacement(shouldClone) {
-  Services.prefs.setBoolPref("places.database.cloneOnCorruption", shouldClone);
-  // Ensure that our databases don't exist yet.
-  let sane = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite");
-  Assert.ok(!await OS.File.exists(sane), "The db should not exist initially");
-  let corrupt = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite.corrupt");
-  Assert.ok(!await OS.File.exists(corrupt), "The corrupt db should not exist initially");
-
-  let file = do_get_file("../migration/places_v38.sqlite");
-  file.copyToFollowingLinks(gProfD, "places.sqlite");
-  file = gProfD.clone();
-  file.append("places.sqlite");
-
-  // Create some unique stuff to check later.
-  let db = Services.storage.openUnsharedDatabase(file);
-  db.executeSimpleSQL("CREATE TABLE moz_cloned(id INTEGER PRIMARY KEY)");
-  db.executeSimpleSQL("CREATE TABLE not_cloned (id INTEGER PRIMARY KEY)");
-  db.executeSimpleSQL("DELETE FROM moz_cloned"); // Shouldn't throw.
-  db.close();
-
-  // Open the database with Places.
-  Services.prefs.setBoolPref("places.database.replaceOnStartup", true);
-  let expectedStatus = shouldClone ? PlacesUtils.history.DATABASE_STATUS_UPGRADED
-                                   : PlacesUtils.history.DATABASE_STATUS_CORRUPT;
-  Assert.equal(PlacesUtils.history.databaseStatus, expectedStatus);
-
-  Assert.ok(await OS.File.exists(sane), "The database should exist");
-
-  // Check the new database still contains our special data.
-  db = Services.storage.openUnsharedDatabase(file);
-  if (shouldClone) {
-    db.executeSimpleSQL("DELETE FROM moz_cloned"); // Shouldn't throw.
-  }
-
-  // Check the new database is really a new one.
-  Assert.throws(() => {
-    db.executeSimpleSQL("DELETE FROM not_cloned");
-  }, "The database should have been replaced");
-  db.close();
-
-  if (shouldClone) {
-    Assert.ok(!await OS.File.exists(corrupt), "The corrupt db should not exist");
-  } else {
-    Assert.ok(await OS.File.exists(corrupt), "The corrupt db should exist");
-  }
-}
rename from toolkit/components/places/tests/unit/test_preventive_maintenance.js
rename to toolkit/components/places/tests/maintenance/test_preventive_maintenance.js
--- a/toolkit/components/places/tests/unit/test_preventive_maintenance.js
+++ b/toolkit/components/places/tests/maintenance/test_preventive_maintenance.js
@@ -5,19 +5,16 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
  /**
   * Test preventive maintenance
   * For every maintenance query create an uncoherent db and check that we take
   * correct fix steps, without polluting valid data.
   */
 
-// Include PlacesDBUtils module
-ChromeUtils.import("resource://gre/modules/PlacesDBUtils.jsm");
-
 // Get services and database connection
 var hs = PlacesUtils.history;
 var bs = PlacesUtils.bookmarks;
 var ts = PlacesUtils.tagging;
 var as = PlacesUtils.annotations;
 var fs = PlacesUtils.favicons;
 
 var mDBConn = hs.DBConnection;
rename from toolkit/components/places/tests/unit/test_preventive_maintenance_checkAndFixDatabase.js
rename to toolkit/components/places/tests/maintenance/test_preventive_maintenance_checkAndFixDatabase.js
--- a/toolkit/components/places/tests/unit/test_preventive_maintenance_checkAndFixDatabase.js
+++ b/toolkit/components/places/tests/maintenance/test_preventive_maintenance_checkAndFixDatabase.js
@@ -3,20 +3,21 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
  /**
   * Test preventive maintenance checkAndFixDatabase.
   */
 
-// Include PlacesDBUtils module.
-ChromeUtils.import("resource://gre/modules/PlacesDBUtils.jsm");
+add_task(async function() {
+  // We must initialize places first, or we won't have a db to check.
+  Assert.equal(PlacesUtils.history.databaseStatus,
+               PlacesUtils.history.DATABASE_STATUS_CREATE);
 
-add_task(async function() {
   let tasksStatusMap = await PlacesDBUtils.checkAndFixDatabase();
   let numberOfTasksRun = tasksStatusMap.size;
     let successfulTasks = [];
     let failedTasks = [];
     tasksStatusMap.forEach(val => {
       if (val.succeeded) {
         successfulTasks.push(val);
       } else {
rename from toolkit/components/places/tests/unit/test_preventive_maintenance_runTasks.js
rename to toolkit/components/places/tests/maintenance/test_preventive_maintenance_runTasks.js
--- a/toolkit/components/places/tests/unit/test_preventive_maintenance_runTasks.js
+++ b/toolkit/components/places/tests/maintenance/test_preventive_maintenance_runTasks.js
@@ -1,19 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
  /**
   * Test preventive maintenance runTasks.
   */
 
-// Include PlacesDBUtils module.
-ChromeUtils.import("resource://gre/modules/PlacesDBUtils.jsm");
-
 add_task(async function() {
   let tasksStatusMap = await PlacesDBUtils.runTasks([PlacesDBUtils.invalidateCaches]);
   let numberOfTasksRun = tasksStatusMap.size;
   let successfulTasks = [];
   let failedTasks = [];
   tasksStatusMap.forEach(val => {
     if (val.succeeded) {
       successfulTasks.push(val);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/maintenance/xpcshell.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+head = head.js
+firefox-appdir = browser
+support-files =
+  corruptDB.sqlite
+
+[test_corrupt_favicons.js]
+[test_corrupt_favicons_schema.js]
+[test_corrupt_places_schema.js]
+[test_corrupt_telemetry.js]
+[test_favicons_replaceOnStartup.js]
+[test_favicons_replaceOnStartup_clone.js]
+[test_places_replaceOnStartup.js]
+[test_places_replaceOnStartup_clone.js]
+[test_preventive_maintenance.js]
+[test_preventive_maintenance_checkAndFixDatabase.js]
+[test_preventive_maintenance_runTasks.js]
--- a/toolkit/components/places/tests/moz.build
+++ b/toolkit/components/places/tests/moz.build
@@ -11,16 +11,17 @@ TESTING_JS_MODULES += [
 ]
 
 XPCSHELL_TESTS_MANIFESTS += [
     'bookmarks/xpcshell.ini',
     'expiration/xpcshell.ini',
     'favicons/xpcshell.ini',
     'history/xpcshell.ini',
     'legacy/xpcshell.ini',
+    'maintenance/xpcshell.ini',
     'migration/xpcshell.ini',
     'queries/xpcshell.ini',
     'sync/xpcshell.ini',
     'unifiedcomplete/xpcshell.ini',
     'unit/xpcshell.ini',
 ]
 
 BROWSER_CHROME_MANIFESTS += ['browser/browser.ini']
--- a/toolkit/components/places/tests/unit/xpcshell.ini
+++ b/toolkit/components/places/tests/unit/xpcshell.ini
@@ -2,17 +2,16 @@
 head = head_bookmarks.js
 firefox-appdir = browser
 support-files =
   bookmarks.corrupt.html
   bookmarks.json
   bookmarks_corrupt.json
   bookmarks.preplaces.html
   bookmarks_html_singleframe.html
-  corruptDB.sqlite
   livemark.xml
   mobile_bookmarks_folder_import.json
   mobile_bookmarks_folder_merge.json
   mobile_bookmarks_multiple_folders.json
   mobile_bookmarks_root_import.json
   mobile_bookmarks_root_merge.json
   places.sparse.sqlite
 
@@ -58,18 +57,16 @@ skip-if = (os == "win" && os_version == 
 [test_bookmarks_html_escape_entities.js]
 [test_bookmarks_html_import_tags.js]
 [test_bookmarks_html_singleframe.js]
 [test_bookmarks_restore_notification.js]
 [test_broken_folderShortcut_result.js]
 [test_browserhistory.js]
 [test_bug636917_isLivemark.js]
 [test_childlessTags.js]
-[test_corrupt_telemetry.js]
-[test_database_replaceOnStartup.js]
 [test_download_history.js]
 [test_frecency.js]
 [test_frecency_decay.js]
 [test_frecency_stats.js]
 [test_frecency_zero_updated.js]
 [test_getChildIndex.js]
 [test_hash.js]
 [test_history.js]
@@ -96,19 +93,16 @@ support-files = noRoot.sqlite
 [test_onItemChanged_tags.js]
 [test_origins.js]
 [test_pageGuid_bookmarkGuid.js]
 [test_frecency_observers.js]
 [test_placeURIs.js]
 [test_PlacesUtils_annotations.js]
 [test_PlacesUtils_invalidateCachedGuidFor.js]
 [test_PlacesUtils_isRootItem.js]
-[test_preventive_maintenance.js]
-[test_preventive_maintenance_checkAndFixDatabase.js]
-[test_preventive_maintenance_runTasks.js]
 [test_promiseBookmarksTree.js]
 [test_resolveNullBookmarkTitles.js]
 [test_result_sort.js]
 [test_resultsAsVisit_details.js]
 [test_sql_function_origin.js]
 [test_sql_guid_functions.js]
 [test_tag_autocomplete_search.js]
 [test_tagging.js]