Bug 1434414 - Locking the 'sanitize on shutdown' pref causes sanitization to happen at every startup. r=johann draft
authorMarco Bonardo <mbonardo@mozilla.com>
Thu, 15 Feb 2018 00:42:58 +0100 (2018-02-14)
changeset 758532 033636ed8b2d38acde08eff9845393799c68d9b7
parent 758396 ea3da643422c58d65335f1778dd6c89c09911585
push id100086
push usermak77@bonardo.net
push dateThu, 22 Feb 2018 16:27:16 +0000 (2018-02-22)
reviewersjohann
bugs1434414
milestone60.0a1
Bug 1434414 - Locking the 'sanitize on shutdown' pref causes sanitization to happen at every startup. r=johann MozReview-Commit-ID: 6PvRFmaZsBC
browser/base/content/sanitize.xul
browser/base/content/sanitizeDialog.js
browser/components/nsBrowserGlue.js
browser/components/places/tests/unit/test_clearHistory_shutdown.js
browser/modules/Sanitizer.jsm
browser/modules/test/unit/test_Sanitizer_interrupted.js
browser/modules/test/unit/xpcshell.ini
--- a/browser/base/content/sanitize.xul
+++ b/browser/base/content/sanitize.xul
@@ -36,19 +36,16 @@
         onload="gSanitizePromptDialog.init();"
         ondialogaccept="return gSanitizePromptDialog.sanitize();">
 
   <vbox id="SanitizeDialogPane" class="prefpane">
     <stringbundle id="bundleBrowser"
                   src="chrome://browser/locale/browser.properties"/>
 
     <script type="application/javascript"
-            src="chrome://browser/content/sanitize.js"/>
-
-    <script type="application/javascript"
             src="chrome://global/content/preferencesBindings.js"/>
     <script type="application/javascript"
             src="chrome://browser/content/sanitizeDialog.js"/>
 
     <hbox id="SanitizeDurationBox" align="center">
       <label value="&clearTimeDuration.label;"
              accesskey="&clearTimeDuration.accesskey;"
              control="sanitizeDurationChoice"
--- a/browser/base/content/sanitizeDialog.js
+++ b/browser/base/content/sanitizeDialog.js
@@ -94,17 +94,16 @@ var gSanitizePromptDialog = {
     acceptButton.disabled = true;
     acceptButton.setAttribute("label",
                               this.bundleBrowser.getString("sanitizeButtonClearing"));
     docElt.getButton("cancel").disabled = true;
 
     try {
       let range = Sanitizer.getClearRange(this.selectedTimespan);
       let options = {
-        prefDomain: "privacy.cpd.",
         ignoreTimespan: !range,
         range,
       };
       Sanitizer.sanitize(null, options)
         .catch(Components.utils.reportError)
         .then(() => window.close())
         .catch(Components.utils.reportError);
       return false;
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -551,19 +551,16 @@ BrowserGlue.prototype = {
           for (let addon of addons) {
             if (addon.type != "experiment") {
               this._notifyUnsignedAddonsDisabled();
               break;
             }
           }
         });
         break;
-      case "test-initialize-sanitizer":
-        Sanitizer.onStartup();
-        break;
       case "sync-ui-state:update":
         this._updateFxaBadges();
         break;
       case "handlersvc-store-initialized":
         // Initialize PdfJs when running in-process and remote. This only
         // happens once since PdfJs registers global hooks. If the PdfJs
         // extension is installed the init method below will be overridden
         // leaving initialization to the extension.
--- a/browser/components/places/tests/unit/test_clearHistory_shutdown.js
+++ b/browser/components/places/tests/unit/test_clearHistory_shutdown.js
@@ -24,16 +24,18 @@ var EXPECTED_NOTIFICATIONS = [
 ];
 
 const UNEXPECTED_NOTIFICATIONS = [
   "xpcom-shutdown"
 ];
 
 const FTP_URL = "ftp://localhost/clearHistoryOnShutdown/";
 
+ChromeUtils.import("resource:///modules/Sanitizer.jsm");
+
 // Send the profile-after-change notification to the form history component to ensure
 // that it has been initialized.
 var formHistoryStartup = Cc["@mozilla.org/satchel/form-history-startup;1"].
                          getService(Ci.nsIObserver);
 formHistoryStartup.observe(null, "profile-after-change", null);
 ChromeUtils.defineModuleGetter(this, "FormHistory",
                                "resource://gre/modules/FormHistory.jsm");
 
@@ -41,30 +43,29 @@ var timeInMicroseconds = Date.now() * 10
 
 add_task(async function test_execute() {
   info("Initialize browserglue before Places");
 
   // Avoid default bookmarks import.
   let glue = Cc["@mozilla.org/browser/browserglue;1"].
              getService(Ci.nsIObserver);
   glue.observe(null, "initial-migration-will-import-default-bookmarks", null);
-  glue.observe(null, "test-initialize-sanitizer", null);
-
+  Sanitizer.onStartup();
 
-  Services.prefs.setBoolPref("privacy.clearOnShutdown.cache", true);
-  Services.prefs.setBoolPref("privacy.clearOnShutdown.cookies", true);
-  Services.prefs.setBoolPref("privacy.clearOnShutdown.offlineApps", true);
-  Services.prefs.setBoolPref("privacy.clearOnShutdown.history", true);
-  Services.prefs.setBoolPref("privacy.clearOnShutdown.downloads", true);
-  Services.prefs.setBoolPref("privacy.clearOnShutdown.cookies", true);
-  Services.prefs.setBoolPref("privacy.clearOnShutdown.formData", true);
-  Services.prefs.setBoolPref("privacy.clearOnShutdown.sessions", true);
-  Services.prefs.setBoolPref("privacy.clearOnShutdown.siteSettings", true);
+  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "cache", true);
+  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "cookies", true);
+  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "offlineApps", true);
+  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "history", true);
+  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "downloads", true);
+  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "cookies", true);
+  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "formData", true);
+  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "sessions", true);
+  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "siteSettings", true);
 
-  Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", true);
+  Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, true);
 
   info("Add visits.");
   for (let aUrl of URIS) {
     await PlacesTestUtils.addVisits({
       uri: uri(aUrl), visitDate: timeInMicroseconds++,
       transition: PlacesUtils.history.TRANSITION_TYPED
     });
   }
--- a/browser/modules/Sanitizer.jsm
+++ b/browser/modules/Sanitizer.jsm
@@ -21,43 +21,44 @@ XPCOMUtils.defineLazyModuleGetters(this,
 
 XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager",
                                    "@mozilla.org/serviceworkers/manager;1",
                                    "nsIServiceWorkerManager");
 XPCOMUtils.defineLazyServiceGetter(this, "quotaManagerService",
                                    "@mozilla.org/dom/quota-manager-service;1",
                                    "nsIQuotaManagerService");
 
+// Used as unique id for pending sanitizations.
+var gPendingSanitizationSerial = 0;
+
 /**
  * A number of iterations after which to yield time back
  * to the system.
  */
 const YIELD_PERIOD = 10;
 
 var Sanitizer = {
   /**
    * Whether we should sanitize on shutdown.
    */
   PREF_SANITIZE_ON_SHUTDOWN: "privacy.sanitize.sanitizeOnShutdown",
 
   /**
-   * During a sanitization this is set to a json containing the array of items
-   * being sanitized, then cleared once the sanitization is complete.
-   * This allows to retry a sanitization on startup in case it was interrupted
-   * by a crash.
+   * During a sanitization this is set to a JSON containing an array of the
+   * pending sanitizations. This allows to retry sanitizations on startup in
+   * case they dind't run or were interrupted by a crash.
+   * Use addPendingSanitization and removePendingSanitization to manage it.
    */
-  PREF_SANITIZE_IN_PROGRESS: "privacy.sanitize.sanitizeInProgress",
+  PREF_PENDING_SANITIZATIONS: "privacy.sanitize.pending",
 
   /**
-   * Whether the previous shutdown sanitization completed successfully.
-   * This is used to detect cases where we were supposed to sanitize on shutdown
-   * but due to a crash we were unable to.  In such cases there may not be any
-   * sanitization in progress, cause we didn't have a chance to start it yet.
+   * Pref branches to fetch sanitization options from.
    */
-  PREF_SANITIZE_DID_SHUTDOWN: "privacy.sanitize.didShutdownSanitize",
+  PREF_CPD_BRANCH: "privacy.cpd.",
+  PREF_SHUTDOWN_BRANCH: "privacy.clearOnShutdown.",
 
   /**
    * The fallback timestamp used when no argument is given to
    * Sanitizer.getClearRange.
    */
   PREF_TIMESPAN: "privacy.sanitize.timeSpan",
 
   /**
@@ -68,16 +69,24 @@ var Sanitizer = {
   TIMESPAN_HOUR:       1,
   TIMESPAN_2HOURS:     2,
   TIMESPAN_4HOURS:     3,
   TIMESPAN_TODAY:      4,
   TIMESPAN_5MIN:       5,
   TIMESPAN_24HOURS:    6,
 
   /**
+   * Whether we should sanitize on shutdown.
+   * When this is set, a pending sanitization should also be added and removed
+   * when shutdown sanitization is complete. This allows to retry incomplete
+   * sanitizations on startup.
+   */
+  shouldSanitizeOnShutdown: false,
+
+  /**
    * Shows a sanitization dialog to the user.
    *
    * @param [optional] parentWindow the window to use as
    *                   parent for the created dialog.
    */
   showUI(parentWindow) {
     let win = AppConstants.platform == "macosx" ?
       null : // make this an app-modal window on Mac
@@ -86,59 +95,59 @@ var Sanitizer = {
                            "chrome://browser/content/sanitize.xul",
                            "Sanitize",
                            "chrome,titlebar,dialog,centerscreen,modal",
                            null);
   },
 
   /**
    * Performs startup tasks:
-   *  - Checks if sanitization was interrupted during last shutdown.
+   *  - Checks if sanitizations were not completed during the last session.
    *  - Registers sanitize-on-shutdown.
    */
   async onStartup() {
-    // Check if we were interrupted during the last shutdown sanitization.
-    let shutdownSanitizationWasInterrupted =
-      Services.prefs.getBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, false) &&
-      Services.prefs.getPrefType(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN) == Ci.nsIPrefBranch.PREF_INVALID;
+    // First, collect pending sanitizations from the last session, before we
+    // add pending sanitizations for this session.
+    let pendingSanitizations = getAndClearPendingSanitizations();
 
-    if (Services.prefs.prefHasUserValue(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN)) {
-      // Reset the pref, so that if we crash before having a chance to
-      // sanitize on shutdown, we will do at the next startup.
-      // Flushing prefs has a cost, so do this only if necessary.
-      Services.prefs.clearUserPref(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN);
-      Services.prefs.savePrefFile(null);
+    // Check if we should sanitize on shutdown.
+    this.shouldSanitizeOnShutdown =
+      Services.prefs.getBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, false);
+    Services.prefs.addObserver(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, this, true);
+    // Add a pending shutdown sanitization, if necessary.
+    if (this.shouldSanitizeOnShutdown) {
+      let itemsToClear = getItemsToClearFromPrefBranch(Sanitizer.PREF_SHUTDOWN_BRANCH);
+      addPendingSanitization("shutdown", itemsToClear, {});
     }
+    // Shutdown sanitization is always pending, but the user may change the
+    // sanitize on shutdown prefs during the session. Then the pending
+    // sanitization would become stale and must be updated.
+    Services.prefs.addObserver(Sanitizer.PREF_SHUTDOWN_BRANCH, this, true);
 
     // Make sure that we are triggered during shutdown.
     let shutdownClient = PlacesUtils.history.shutdownClient.jsclient;
     // We need to pass to sanitize() (through sanitizeOnShutdown) a state object
     // that tracks the status of the shutdown blocker. This `progress` object
     // will be updated during sanitization and reported with the crash in case of
     // a shutdown timeout.
     // We use the `options` argument to pass the `progress` object to sanitize().
     let progress = { isShutdown: true };
     shutdownClient.addBlocker("sanitize.js: Sanitize on shutdown",
-      () => sanitizeOnShutdown({ progress }),
-      {
-        fetchState: () => ({ progress })
-      }
+      () => sanitizeOnShutdown(progress),
+      {fetchState: () => ({ progress })}
     );
 
-    // Check if Firefox crashed during a sanitization.
-    let lastInterruptedSanitization = Services.prefs.getStringPref(Sanitizer.PREF_SANITIZE_IN_PROGRESS, "");
-    if (lastInterruptedSanitization) {
-      // If the json is invalid this will just throw and reject the Task.
-      let {itemsToClear, options} = JSON.parse(lastInterruptedSanitization);
-      await this.sanitize(itemsToClear, options);
-    } else if (shutdownSanitizationWasInterrupted) {
-      // Otherwise, could be we were supposed to sanitize on shutdown but we
-      // didn't have a chance, due to an earlier crash.
-      // In such a case, just redo a shutdown sanitize now, during startup.
-      await sanitizeOnShutdown();
+    // Finally, run the sanitizations that were left pending, because we crashed
+    // before completing them.
+    for (let {itemsToClear, options} of pendingSanitizations) {
+      try {
+        await this.sanitize(itemsToClear, options);
+      } catch (ex) {
+        Cu.reportError("A previously pending sanitization failed: " + itemsToClear + "\n" + ex);
+      }
     }
   },
 
   /**
    * Returns a 2 element array representing the start and end times,
    * in the uSec-since-epoch format that PRTime likes. If we should
    * clear everything, this function returns null.
    *
@@ -201,23 +210,23 @@ var Sanitizer = {
    *        Object whose properties are options for this sanitization:
    *         - ignoreTimespan (default: true): Time span only makes sense in
    *           certain cases.  Consumers who want to only clear some private
    *           data can opt in by setting this to false, and can optionally
    *           specify a specific range.
    *           If timespan is not ignored, and range is not set, sanitize() will
    *           use the value of the timespan pref to determine a range.
    *         - range (default: null)
-   *         - prefDomain (default: "privacy.cpd."): indicates the preferences
-   *           branch to collect the list of items to sanitize from.
    *         - privateStateForNewWindow (default: "non-private"): when clearing
    *           open windows, defines the private state for the newly opened window.
    */
   async sanitize(itemsToClear = null, options = {}) {
     let progress = options.progress || {};
+    if (!itemsToClear)
+      itemsToClear = getItemsToClearFromPrefBranch(this.PREF_CPD_BRANCH);
     let promise = sanitizeInternal(this.items, itemsToClear, progress, options);
 
     // Depending on preferences, the sanitizer may perform asynchronous
     // work before it starts cleaning up the Places database (e.g. closing
     // windows). We need to make sure that the connection to that database
     // hasn't been closed by the time we use it.
     // Though, if this is a sanitize on shutdown, we already have a blocker.
     if (!progress.isShutdown) {
@@ -235,16 +244,41 @@ var Sanitizer = {
 
     try {
       await promise;
     } finally {
       Services.obs.notifyObservers(null, "sanitizer-sanitization-complete");
     }
   },
 
+  observe(subject, topic, data) {
+    if (topic == "nsPref:changed") {
+      if (data.startsWith(this.PREF_SHUTDOWN_BRANCH) &&
+          this.shouldSanitizeOnShutdown) {
+        // Update the pending shutdown sanitization.
+        removePendingSanitization("shutdown");
+        let itemsToClear = getItemsToClearFromPrefBranch(Sanitizer.PREF_SHUTDOWN_BRANCH);
+        addPendingSanitization("shutdown", itemsToClear, {});
+      } else if (data == this.PREF_SANITIZE_ON_SHUTDOWN) {
+        this.shouldSanitizeOnShutdown =
+          Services.prefs.getBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, false);
+        removePendingSanitization("shutdown");
+        if (this.shouldSanitizeOnShutdown) {
+          let itemsToClear = getItemsToClearFromPrefBranch(Sanitizer.PREF_SHUTDOWN_BRANCH);
+          addPendingSanitization("shutdown", itemsToClear, {});
+        }
+      }
+    }
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsiObserver,
+    Ci.nsISupportsWeakReference
+  ]),
+
   items: {
     cache: {
       async clear(range) {
         let seenException;
         let refObj = {};
         TelemetryStopwatch.start("FX_SANITIZE_CACHE", refObj);
 
         try {
@@ -737,38 +771,29 @@ var Sanitizer = {
       async clear(range) {
         await clearPluginData(range);
       },
     },
   },
 };
 
 async function sanitizeInternal(items, aItemsToClear, progress, options = {}) {
-  let { prefDomain = "privacy.cpd.", ignoreTimespan = true, range } = options;
+  let { ignoreTimespan = true, range } = options;
   let seenError = false;
-  let itemsToClear;
-  if (Array.isArray(aItemsToClear)) {
-    // Shallow copy the array, as we are going to modify
-    // it in place later.
-    itemsToClear = [...aItemsToClear];
-  } else {
-    let branch = Services.prefs.getBranch(prefDomain);
-    itemsToClear = Object.keys(items).filter(itemName => {
-      try {
-        return branch.getBoolPref(itemName);
-      } catch (ex) {
-        return false;
-      }
-    });
-  }
+  // Shallow copy the array, as we are going to modify it in place later.
+  if (!Array.isArray(aItemsToClear))
+    throw new Error("Must pass an array of items to clear.");
+  let itemsToClear = [...aItemsToClear];
 
   // Store the list of items to clear, in case we are killed before we
   // get a chance to complete.
-  Services.prefs.setStringPref(Sanitizer.PREF_SANITIZE_IN_PROGRESS,
-                               JSON.stringify({itemsToClear, options}));
+  let uid = gPendingSanitizationSerial++;
+  // Shutdown sanitization is managed outside.
+  if (!progress.isShutdown)
+    addPendingSanitization(uid, itemsToClear, options);
 
   // Store the list of items to clear, for debugging/forensics purposes
   for (let k of itemsToClear) {
     progress[k] = "ready";
   }
 
   // Ensure open windows get cleared first, if they're in our list, so that
   // they don't stick around in the recently closed windows list, and so we
@@ -821,19 +846,18 @@ async function sanitizeInternal(items, a
   }
   for (let handle of handles) {
     progress[handle.name] = "blocking";
     await handle.promise;
   }
 
   // Sanitization is complete.
   TelemetryStopwatch.finish("FX_SANITIZE_TOTAL", refObj);
-  // Reset the inProgress preference since we were not killed during
-  // sanitization.
-  Services.prefs.clearUserPref(Sanitizer.PREF_SANITIZE_IN_PROGRESS);
+  if (!progress.isShutdown)
+    removePendingSanitization(uid);
   progress = {};
   if (seenError) {
     throw new Error("Error sanitizing");
   }
 }
 
 async function clearPluginData(range) {
   // Clear plugin data.
@@ -894,20 +918,73 @@ async function clearPluginData(range) {
     // to do anything about it.
   });
 
   if (seenException) {
     throw seenException;
   }
 }
 
-async function sanitizeOnShutdown(options = {}) {
-  if (!Services.prefs.getBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN)) {
+async function sanitizeOnShutdown(progress) {
+  if (!Sanitizer.shouldSanitizeOnShutdown) {
     return;
   }
   // Need to sanitize upon shutdown
-  options.prefDomain = "privacy.clearOnShutdown.";
-  await Sanitizer.sanitize(null, options);
+  let itemsToClear = getItemsToClearFromPrefBranch(Sanitizer.PREF_SHUTDOWN_BRANCH);
+  await Sanitizer.sanitize(itemsToClear, { progress });
   // We didn't crash during shutdown sanitization, so annotate it to avoid
   // sanitizing again on startup.
-  Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN, true);
+  removePendingSanitization("shutdown");
   Services.prefs.savePrefFile(null);
 }
+
+/**
+ * Gets an array of items to clear from the given pref branch.
+ * @param branch The pref branch to fetch.
+ * @return Array of items to clear
+ */
+function getItemsToClearFromPrefBranch(branch) {
+  branch = Services.prefs.getBranch(branch);
+  return Object.keys(Sanitizer.items).filter(itemName => {
+    try {
+      return branch.getBoolPref(itemName);
+    } catch (ex) {
+      return false;
+    }
+  });
+}
+
+/**
+ * These functions are used to track pending sanitization on the next startup
+ * in case of a crash before a sanitization could happen.
+ * @param id A unique id identifying the sanitization
+ * @param itemsToClear The items to clear
+ * @param options The Sanitize options
+ */
+function addPendingSanitization(id, itemsToClear, options) {
+  let pendingSanitizations = safeGetPendingSanitizations();
+  pendingSanitizations.push({id, itemsToClear, options});
+  Services.prefs.setStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS,
+                               JSON.stringify(pendingSanitizations));
+}
+function removePendingSanitization(id) {
+  let pendingSanitizations = safeGetPendingSanitizations();
+  let i = pendingSanitizations.findIndex(s => s.id == id);
+  let [s] = pendingSanitizations.splice(i, 1);
+  Services.prefs.setStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS,
+    JSON.stringify(pendingSanitizations));
+  return s;
+}
+function getAndClearPendingSanitizations() {
+  let pendingSanitizations = safeGetPendingSanitizations();
+  if (pendingSanitizations.length)
+    Services.prefs.clearUserPref(Sanitizer.PREF_PENDING_SANITIZATIONS);
+  return pendingSanitizations;
+}
+function safeGetPendingSanitizations() {
+  try {
+    return JSON.parse(
+      Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]"));
+  } catch (ex) {
+    Cu.reportError("Invalid JSON value for pending sanitizations: " + ex);
+    return [];
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/unit/test_Sanitizer_interrupted.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+do_get_profile();
+
+// Test that interrupted sanitizations are properly tracked.
+
+add_task(async function() {
+  ChromeUtils.import("resource:///modules/Sanitizer.jsm");
+  registerCleanupFunction(() => {
+    Services.prefs.clearUserPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN);
+    Services.prefs.clearUserPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "formdata");
+  });
+  Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, true);
+  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "formdata", true);
+
+  await Sanitizer.onStartup();
+  Assert.ok(Sanitizer.shouldSanitizeOnShutdown, "Should sanitize on shutdown");
+
+  let pendingSanitizations = JSON.parse(
+    Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]"));
+  Assert.equal(pendingSanitizations.length, 1, "Should have 1 pending sanitization");
+  Assert.equal(pendingSanitizations[0].id, "shutdown", "Should be the shutdown sanitization");
+  Assert.ok(pendingSanitizations[0].itemsToClear.includes("formdata"), "Pref has been setup");
+  Assert.ok(!pendingSanitizations[0].options.isShutdown, "Shutdown option is not present");
+
+  // Check the preference listeners.
+  Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, false);
+  pendingSanitizations = JSON.parse(
+    Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]"));
+  Assert.equal(pendingSanitizations.length, 0, "Should not have pending sanitizations");
+  Assert.ok(!Sanitizer.shouldSanitizeOnShutdown, "Should not sanitize on shutdown");
+  Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, true);
+  pendingSanitizations = JSON.parse(
+    Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]"));
+  Assert.equal(pendingSanitizations.length, 1, "Should have 1 pending sanitization");
+  Assert.equal(pendingSanitizations[0].id, "shutdown", "Should be the shutdown sanitization");
+
+  Assert.ok(pendingSanitizations[0].itemsToClear.includes("formdata"),
+            "Pending sanitizations should include formdata");
+  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "formdata", false);
+  pendingSanitizations = JSON.parse(
+    Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]"));
+  Assert.equal(pendingSanitizations.length, 1, "Should have 1 pending sanitization");
+  Assert.ok(!pendingSanitizations[0].itemsToClear.includes("formdata"),
+            "Pending sanitizations should have been updated");
+
+  // Check a sanitization properly rebuilds the pref.
+  await Sanitizer.sanitize(["formdata"]);
+  pendingSanitizations = JSON.parse(
+    Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]"));
+  Assert.equal(pendingSanitizations.length, 1, "Should have 1 pending sanitization");
+  Assert.equal(pendingSanitizations[0].id, "shutdown", "Should be the shutdown sanitization");
+
+  // Startup should run the pending one and setup a new shutdown sanitization.
+  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "formdata", false);
+  await Sanitizer.onStartup();
+  pendingSanitizations = JSON.parse(
+    Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]"));
+  Assert.equal(pendingSanitizations.length, 1, "Should have 1 pending sanitization");
+  Assert.equal(pendingSanitizations[0].id, "shutdown", "Should be the shutdown sanitization");
+  Assert.ok(!pendingSanitizations[0].itemsToClear.includes("formdata"), "Pref has been setup");
+});
--- a/browser/modules/test/unit/xpcshell.ini
+++ b/browser/modules/test/unit/xpcshell.ini
@@ -2,10 +2,11 @@
 head =
 firefox-appdir = browser
 skip-if = toolkit == 'android'
 
 [test_AttributionCode.js]
 skip-if = os != 'win'
 [test_DirectoryLinksProvider.js]
 [test_E10SUtils_nested_URIs.js]
+[test_Sanitizer_interrupted.js]
 [test_SitePermissions.js]
 [test_LaterRun.js]