Bug 1432583 - Better corruption handling for favicons.sqlite. r=standard8 draft
authorMarco Bonardo <mbonardo@mozilla.com>
Wed, 09 May 2018 14:53:59 +0200
changeset 796596 91e9875f9e4fc7a0d0c72af1b52ddaca80a52175
parent 795256 cf3ee14023483cbbb57129479537c713e22c1980
push id110300
push usermak77@bonardo.net
push dateThu, 17 May 2018 21:37:25 +0000
reviewersstandard8
bugs1432583
milestone62.0a1
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]