Merge mozilla-central to mozilla-inbound. CLOSED TREE
authorCsoregi Natalia <ncsoregi@mozilla.com>
Wed, 17 Oct 2018 13:08:33 +0300
changeset 489963 1a9ecfe4c2d4aae717bdb8cdbef9391a1e1c1ba6
parent 489937 eed147a8bd4197ebc04eb58bebe29356e65395b0 (current diff)
parent 489962 9c943fa07f723ebf4fa3a885b69fe2cb3c5da2d7 (diff)
child 489964 a729ef8fab784e20eeccececa5e69cc5dc153c09
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
milestone64.0a1
Merge mozilla-central to mozilla-inbound. CLOSED TREE
browser/extensions/formautofill/MasterPassword.jsm
browser/extensions/formautofill/test/browser/browser_creditCard_fill_master_password.js
browser/extensions/formautofill/test/unit/test_masterPassword.js
dom/canvas/CacheMap.cpp
dom/canvas/CacheMap.h
dom/canvas/WebGLFramebufferAttachable.cpp
dom/canvas/WebGLFramebufferAttachable.h
testing/web-platform/meta/html/semantics/forms/the-select-element/common-HTMLOptionsCollection.html.ini
testing/web-platform/meta/html/semantics/forms/the-select-element/select-remove.html.ini
--- a/Makefile.in
+++ b/Makefile.in
@@ -40,56 +40,36 @@ endif
 # Additionally, provide a dummy target during tests, because
 # faster/rules.mk will expect these targets to exist.
 ifdef TEST_MOZBUILD
 source-repo.h: $(MDDEPDIR)/source-repo.h.stub
 buildid.h: $(MDDEPDIR)/buildid.h.stub
 endif
 endif
 
-ifdef JS_STANDALONE
-configure_dir = $(topsrcdir)/js/src
-else
-configure_dir = $(topsrcdir)
-endif
-
 BUILD_BACKEND_FILES := $(addprefix backend.,$(addsuffix Backend,$(BUILD_BACKENDS)))
 
 ifndef TEST_MOZBUILD
 ifndef MOZ_PROFILE_USE
 # We need to explicitly put BUILD_BACKEND_FILES here otherwise the rule in
 # rules.mk doesn't run early enough.
-$(TIERS) binaries:: CLOBBER $(configure_dir)/configure config.status $(BUILD_BACKEND_FILES)
-ifdef MOZ_WIDGET_TOOLKIT
-ifdef COMPILE_ENVIRONMENT
-$(TIERS) binaries:: $(topsrcdir)/js/src/configure js/src/config.status
-endif
-endif
+$(TIERS) binaries:: CLOBBER $(BUILD_BACKEND_FILES)
 endif
 endif
 
 ifdef JS_STANDALONE
 .PHONY: CLOBBER
 CLOBBER:
 else
 CLOBBER: $(topsrcdir)/CLOBBER
 	@echo 'STOP!  The CLOBBER file has changed.'
 	@echo 'Please run the build through "mach build".'
 	@exit 1
 endif
 
-config.status: $(configure_dir)/configure $(configure_dir)/old-configure
-js/src/config.status: $(topsrcdir)/js/src/configure $(topsrcdir)/js/src/old-configure
-config.status js/src/config.status:
-	@echo 'STOP!  $? has changed and needs to be run again.'
-	@echo 'Please rerun it.'
-	@echo 'To ignore this message, touch "$(CURDIR)/$@",'
-	@echo 'but your build might not succeed.'
-	@exit 1
-
 # Regenerate the build backend if it is out of date. We only have this rule in
 # this main make file because having it in rules.mk and applied to partial tree
 # builds resulted in a world of hurt. Gory details are in bug 877308.
 #
 # The mach build driver will ensure the backend is up to date for partial tree
 # builds. This cleanly avoids most of the pain.
 
 ifndef TEST_MOZBUILD
@@ -113,30 +93,19 @@ install_manifests := \
 # FasterMake/RecursiveMake backend. This is a hack until bug 1241744 moves
 # xpidl handling to FasterMake in that case, mechanically making the dist/bin
 # install manifest non-existent (non-existent manifests being skipped)
 ifeq (,$(filter FasterMake+RecursiveMake,$(BUILD_BACKENDS)))
 install_manifests += dist/bin
 endif
 install_manifest_depends = \
   CLOBBER \
-  $(configure_dir)/configure \
-  config.status \
   $(BUILD_BACKEND_FILES) \
   $(NULL)
 
-ifdef MOZ_WIDGET_TOOLKIT
-ifdef COMPILE_ENVIRONMENT
-install_manifest_depends += \
-  $(topsrcdir)/js/src/configure \
-  js/src/config.status \
-  $(NULL)
-endif
-endif
-
 .PHONY: install-manifests
 install-manifests: $(addprefix install-,$(install_manifests))
 
 # If we're using the hybrid FasterMake/RecursiveMake backend, we want
 # to recurse in the faster/ directory in parallel of install manifests.
 ifneq (,$(filter FasterMake+RecursiveMake,$(BUILD_BACKENDS)))
 install-manifests: faster
 .PHONY: faster
--- a/browser/components/payments/content/paymentDialogWrapper.js
+++ b/browser/components/payments/content/paymentDialogWrapper.js
@@ -11,18 +11,18 @@
 
 const paymentSrv = Cc["@mozilla.org/dom/payments/payment-request-service;1"]
                      .getService(Ci.nsIPaymentRequestService);
 
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-ChromeUtils.defineModuleGetter(this, "MasterPassword",
-                               "resource://formautofill/MasterPassword.jsm");
+ChromeUtils.defineModuleGetter(this, "OSKeyStore",
+                               "resource://formautofill/OSKeyStore.jsm");
 ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
                                "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "formAutofillStorage", () => {
   let storage;
   try {
     storage = ChromeUtils.import("resource://formautofill/FormAutofillStorage.jsm", {})
                          .formAutofillStorage;
@@ -144,30 +144,30 @@ var paymentDialogWrapper = {
     });
 
     return address;
   },
 
   /**
    * @param {string} guid The GUID of the basic card record from storage.
    * @param {string} cardSecurityCode The associated card security code (CVV/CCV/etc.)
-   * @throws if the user cancels entering their master password or an error decrypting
+   * @throws If there is an error decrypting
    * @returns {nsIBasicCardResponseData?} returns response data or null (if the
    *                                      master password dialog was cancelled);
    */
   async _convertProfileBasicCardToPaymentMethodData(guid, cardSecurityCode) {
     let cardData = this.temporaryStore.creditCards.get(guid) ||
                    await formAutofillStorage.creditCards.get(guid);
     if (!cardData) {
       throw new Error(`Basic card not found in storage: ${guid}`);
     }
 
     let cardNumber;
     try {
-      cardNumber = await MasterPassword.decrypt(cardData["cc-number-encrypted"], true);
+      cardNumber = await OSKeyStore.decrypt(cardData["cc-number-encrypted"], true);
     } catch (ex) {
       if (ex.result != Cr.NS_ERROR_ABORT) {
         throw ex;
       }
       // User canceled master password entry
       return null;
     }
 
@@ -498,18 +498,26 @@ var paymentDialogWrapper = {
     window.close();
   },
 
   async onPay({
     selectedPayerAddressGUID: payerGUID,
     selectedPaymentCardGUID: paymentCardGUID,
     selectedPaymentCardSecurityCode: cardSecurityCode,
   }) {
-    let methodData = await this._convertProfileBasicCardToPaymentMethodData(paymentCardGUID,
-                                                                            cardSecurityCode);
+    let methodData;
+    try {
+      methodData = await this._convertProfileBasicCardToPaymentMethodData(paymentCardGUID,
+                                                                          cardSecurityCode);
+    } catch (ex) {
+      // TODO (Bug 1498403): Some kind of "credit card storage error" here, perhaps asking user
+      // to re-enter credit card # from management UI.
+      Cu.reportError(ex);
+      return;
+    }
 
     if (!methodData) {
       // TODO (Bug 1429265/Bug 1429205): Handle when a user hits cancel on the
       // Master Password dialog.
       Cu.reportError("Bug 1429265/Bug 1429205: User canceled master password entry");
       return;
     }
 
--- a/browser/components/payments/res/debugging.js
+++ b/browser/components/payments/res/debugging.js
@@ -313,17 +313,17 @@ let DUPED_ADDRESSES = {
 };
 
 let BASIC_CARDS_1 = {
   "53f9d009aed2": {
     billingAddressGUID: "68gjdh354j",
     methodName: "basic-card",
     "cc-number": "************5461",
     "guid": "53f9d009aed2",
-    "version": 1,
+    "version": 2,
     "timeCreated": 1505240896213,
     "timeLastModified": 1515609524588,
     "timeLastUsed": 0,
     "timesUsed": 0,
     "cc-name": "John Smith",
     "cc-exp-month": 6,
     "cc-exp-year": 2024,
     "cc-type": "visa",
@@ -331,17 +331,17 @@ let BASIC_CARDS_1 = {
     "cc-additional-name": "",
     "cc-family-name": "Smith",
     "cc-exp": "2024-06",
   },
   "9h5d4h6f4d1s": {
     methodName: "basic-card",
     "cc-number": "************0954",
     "guid": "9h5d4h6f4d1s",
-    "version": 1,
+    "version": 2,
     "timeCreated": 1517890536491,
     "timeLastModified": 1517890564518,
     "timeLastUsed": 0,
     "timesUsed": 0,
     "cc-name": "Jane Doe",
     "cc-exp-month": 5,
     "cc-exp-year": 2023,
     "cc-type": "mastercard",
@@ -349,17 +349,17 @@ let BASIC_CARDS_1 = {
     "cc-additional-name": "",
     "cc-family-name": "Doe",
     "cc-exp": "2023-05",
   },
   "123456789abc": {
     methodName: "basic-card",
     "cc-number": "************1234",
     "guid": "123456789abc",
-    "version": 1,
+    "version": 2,
     "timeCreated": 1517890536491,
     "timeLastModified": 1517890564518,
     "timeLastUsed": 0,
     "timesUsed": 0,
     "cc-name": "Jane Fields",
     "cc-given-name": "Jane",
     "cc-additional-name": "",
     "cc-family-name": "Fields",
@@ -383,17 +383,17 @@ let BASIC_CARDS_1 = {
     "cc-exp-month": 6,
     "cc-exp-year": 2023,
     "cc-exp": "2023-06",
   },
   "missing-cc-name": {
     methodName: "basic-card",
     "cc-number": "************8563",
     "guid": "missing-cc-name",
-    "version": 1,
+    "version": 2,
     "timeCreated": 1517890536491,
     "timeLastModified": 1517890564518,
     "timeLastUsed": 0,
     "timesUsed": 0,
     "cc-exp-month": 8,
     "cc-exp-year": 2024,
     "cc-exp": "2024-08",
   },
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -1784,17 +1784,17 @@
                                                            [brandName, target.getAttribute("uri")], 2);
               Services.prompt.QueryInterface(Ci.nsIPromptFactory);
               let prompt = Services.prompt.getPrompt(gBrowser.contentWindow, Ci.nsIPrompt);
               prompt.QueryInterface(Ci.nsIWritablePropertyBag2);
               prompt.setPropertyAsBool("allowTabModal", true);
               prompt.alert(title, text);
             },
           };
-          Services.search.addEngine(target.getAttribute("uri"), null,
+          Services.search.addEngine(target.getAttribute("uri"),
                                     target.getAttribute("image"), false,
                                     installCallback);
         }
         let anonid = target.getAttribute("anonid");
         if (anonid == "search-one-offs-context-open-in-new-tab") {
           // Select the context-clicked button so that consumers can easily
           // tell which button was acted on.
           this.selectedButton = this._buttonForEngine(this._contextEngine);
--- a/browser/extensions/formautofill/FormAutofillParent.jsm
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -37,17 +37,17 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://formautofill/FormAutofill.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
   CreditCard: "resource://gre/modules/CreditCard.jsm",
   FormAutofillPreferences: "resource://formautofill/FormAutofillPreferences.jsm",
   FormAutofillDoorhanger: "resource://formautofill/FormAutofillDoorhanger.jsm",
   FormAutofillUtils: "resource://formautofill/FormAutofillUtils.jsm",
-  MasterPassword: "resource://formautofill/MasterPassword.jsm",
+  OSKeyStore: "resource://formautofill/OSKeyStore.jsm",
 });
 
 this.log = null;
 FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]);
 
 const {
   ENABLED_AUTOFILL_ADDRESSES_PREF,
   ENABLED_AUTOFILL_CREDITCARDS_PREF,
@@ -221,18 +221,18 @@ FormAutofillParent.prototype = {
         if (data.guid) {
           await this.formAutofillStorage.addresses.update(data.guid, data.address);
         } else {
           await this.formAutofillStorage.addresses.add(data.address);
         }
         break;
       }
       case "FormAutofill:SaveCreditCard": {
-        if (!await MasterPassword.ensureLoggedIn()) {
-          log.warn("User canceled master password entry");
+        if (!await OSKeyStore.ensureLoggedIn()) {
+          log.warn("User canceled encryption login");
           return;
         }
         await this.formAutofillStorage.creditCards.add(data.creditcard);
         break;
       }
       case "FormAutofill:RemoveAddresses": {
         data.guids.forEach(guid => this.formAutofillStorage.addresses.remove(guid));
         break;
@@ -249,22 +249,22 @@ FormAutofillParent.prototype = {
         const win = BrowserWindowTracker.getTopWindow();
         win.openPreferences("privacy-form-autofill", {origin: "autofillFooter"});
         break;
       }
       case "FormAutofill:GetDecryptedString": {
         let {cipherText, reauth} = data;
         let string;
         try {
-          string = await MasterPassword.decrypt(cipherText, reauth);
+          string = await OSKeyStore.decrypt(cipherText, reauth);
         } catch (e) {
           if (e.result != Cr.NS_ERROR_ABORT) {
             throw e;
           }
-          log.warn("User canceled master password entry");
+          log.warn("User canceled encryption login");
         }
         target.sendAsyncMessage("FormAutofill:DecryptedString", string);
         break;
       }
     }
   },
 
   /**
@@ -288,17 +288,17 @@ FormAutofillParent.prototype = {
       Services.ppmm.removeMessageListener("FormAutofill:GetDecryptedString", this);
       Services.prefs.removeObserver(ENABLED_AUTOFILL_CREDITCARDS_PREF, this);
     }
   },
 
   /**
    * Get the records from profile store and return results back to content
    * process. It will decrypt the credit card number and append
-   * "cc-number-decrypted" to each record if MasterPassword isn't set.
+   * "cc-number-decrypted" to each record if OSKeyStore isn't set.
    *
    * @private
    * @param  {string} data.collectionName
    *         The name used to specify which collection to retrieve records.
    * @param  {string} data.searchString
    *         The typed string for filtering out the matched records.
    * @param  {string} data.info
    *         The input autocomplete property's information.
@@ -313,44 +313,33 @@ FormAutofillParent.prototype = {
     }
 
     let recordsInCollection = await collection.getAll();
     if (!info || !info.fieldName || !recordsInCollection.length) {
       target.sendAsyncMessage("FormAutofill:Records", recordsInCollection);
       return;
     }
 
-    let isCCAndMPEnabled = collectionName == CREDITCARDS_COLLECTION_NAME && MasterPassword.isEnabled;
-    // We don't filter "cc-number" when MasterPassword is set.
-    if (isCCAndMPEnabled && info.fieldName == "cc-number") {
+    let isCC = collectionName == CREDITCARDS_COLLECTION_NAME;
+    // We don't filter "cc-number"
+    if (isCC && info.fieldName == "cc-number") {
       recordsInCollection = recordsInCollection.filter(record => !!record["cc-number"]);
       target.sendAsyncMessage("FormAutofill:Records", recordsInCollection);
       return;
     }
 
     let records = [];
     let lcSearchString = searchString.toLowerCase();
 
     for (let record of recordsInCollection) {
       let fieldValue = record[info.fieldName];
       if (!fieldValue) {
         continue;
       }
 
-      // Cache the decrypted "cc-number" in each record for content to preview
-      // when MasterPassword isn't set.
-      if (!isCCAndMPEnabled && record["cc-number-encrypted"]) {
-        record["cc-number-decrypted"] = await MasterPassword.decrypt(record["cc-number-encrypted"]);
-      }
-
-      // Filter "cc-number" based on the decrypted one.
-      if (info.fieldName == "cc-number") {
-        fieldValue = record["cc-number-decrypted"];
-      }
-
       if (collectionName == ADDRESSES_COLLECTION_NAME && record.country
           && !FormAutofill.supportedCountries.includes(record.country)) {
         // Address autofill isn't supported for the record's country so we don't
         // want to attempt to potentially incorrectly fill the address fields.
         continue;
       }
 
       if (lcSearchString && !String(fieldValue).toLowerCase().startsWith(lcSearchString)) {
@@ -534,18 +523,18 @@ FormAutofillParent.prototype = {
         return;
       }
 
       if (state == "disable") {
         Services.prefs.setBoolPref("extensions.formautofill.creditCards.enabled", false);
         return;
       }
 
-      if (!await MasterPassword.ensureLoggedIn()) {
-        log.warn("User canceled master password entry");
+      if (!await OSKeyStore.ensureLoggedIn()) {
+        log.warn("User canceled encryption login");
         return;
       }
 
       let changedGUIDs = [];
       if (creditCard.guid) {
         if (state == "update") {
           await this.formAutofillStorage.creditCards.update(creditCard.guid, creditCard.record, true);
           changedGUIDs.push(creditCard.guid);
--- a/browser/extensions/formautofill/FormAutofillStorage.jsm
+++ b/browser/extensions/formautofill/FormAutofillStorage.jsm
@@ -137,33 +137,36 @@ ChromeUtils.import("resource://formautof
 ChromeUtils.defineModuleGetter(this, "CreditCard",
                                "resource://gre/modules/CreditCard.jsm");
 ChromeUtils.defineModuleGetter(this, "JSONFile",
                                "resource://gre/modules/JSONFile.jsm");
 ChromeUtils.defineModuleGetter(this, "FormAutofillNameUtils",
                                "resource://formautofill/FormAutofillNameUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "FormAutofillUtils",
                                "resource://formautofill/FormAutofillUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "MasterPassword",
-                               "resource://formautofill/MasterPassword.jsm");
+ChromeUtils.defineModuleGetter(this, "OSKeyStore",
+                               "resource://formautofill/OSKeyStore.jsm");
 ChromeUtils.defineModuleGetter(this, "PhoneNumber",
                                "resource://formautofill/phonenumberutils/PhoneNumber.jsm");
 
+XPCOMUtils.defineLazyServiceGetter(this, "cryptoSDR",
+                                   "@mozilla.org/login-manager/crypto/SDR;1",
+                                   Ci.nsILoginManagerCrypto);
 XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
 const CryptoHash = Components.Constructor("@mozilla.org/security/hash;1",
                                           "nsICryptoHash", "initWithString");
 
 const PROFILE_JSON_FILE_NAME = "autofill-profiles.json";
 
 const STORAGE_SCHEMA_VERSION = 1;
 const ADDRESS_SCHEMA_VERSION = 1;
-const CREDIT_CARD_SCHEMA_VERSION = 1;
+const CREDIT_CARD_SCHEMA_VERSION = 2;
 
 const VALID_ADDRESS_FIELDS = [
   "given-name",
   "additional-name",
   "family-name",
   "organization",
   "street-address",
   "address-level3",
@@ -259,23 +262,24 @@ class AutofillRecords {
 
     this.VALID_FIELDS = validFields;
     this.VALID_COMPUTED_FIELDS = validComputedFields;
 
     this._store = store;
     this._collectionName = collectionName;
     this._schemaVersion = schemaVersion;
 
-    Promise.all(this._data.map(record => this._migrateRecord(record)))
-      .then(hasChangesArr => {
-        let dataHasChanges = hasChangesArr.find(hasChanges => hasChanges);
-        if (dataHasChanges) {
-          this._store.saveSoon();
-        }
-      });
+    this._initializePromise =
+      Promise.all(this._data.map(async (record, index) => this._migrateRecord(record, index)))
+        .then(hasChangesArr => {
+          let dataHasChanges = hasChangesArr.includes(true);
+          if (dataHasChanges) {
+            this._store.saveSoon();
+          }
+        });
   }
 
   /**
    * Gets the schema version number.
    *
    * @returns {number}
    *          The current schema version number.
    */
@@ -299,16 +303,24 @@ class AutofillRecords {
   _ensureMatchingVersion(record) {
     if (record.version != this.version) {
       throw new Error(`Got unknown record version ${
         record.version}; want ${this.version}`);
     }
   }
 
   /**
+   * Initialize the records in the collection, resolves when the migration completes.
+   * @returns {Promise}
+   */
+  initialize() {
+    return this._initializePromise;
+  }
+
+  /**
    * Adds a new record.
    *
    * @param {Object} record
    *        The new record for saving.
    * @param {boolean} [options.sourceSync = false]
    *        Did sync generate this addition?
    * @returns {Promise<string>}
    *          The GUID of the newly added item..
@@ -1171,36 +1183,47 @@ class AutofillRecords {
   }
 
   _findIndexByGUID(guid, {includeDeleted = false} = {}) {
     return this._data.findIndex(record => {
       return record.guid == guid && (!record.deleted || includeDeleted);
     });
   }
 
-  async _migrateRecord(record) {
+  async _migrateRecord(record, index) {
     let hasChanges = false;
 
     if (record.deleted) {
       return hasChanges;
     }
 
     if (!record.version || isNaN(record.version) || record.version < 1) {
       this.log.warn("Invalid record version:", record.version);
 
       // Force to run the migration.
       record.version = 0;
     }
 
     if (record.version < this.version) {
       hasChanges = true;
-      record.version = this.version;
+
+      record = await this._computeMigratedRecord(record);
 
-      // Force to recompute fields if we upgrade the schema.
-      await this._stripComputedFields(record);
+      if (record.deleted) {
+        // record is deleted by _computeMigratedRecord(),
+        // go ahead and put it in the store.
+        this._data[index] = record;
+        return hasChanges;
+      }
+
+      // Compute the computed fields before putting it to store.
+      await this.computeFields(record);
+      this._data[index] = record;
+
+      return hasChanges;
     }
 
     hasChanges |= await this.computeFields(record);
     return hasChanges;
   }
 
   _normalizeRecord(record, preserveEmptyFields = false) {
     this._normalizeFields(record);
@@ -1251,16 +1274,34 @@ class AutofillRecords {
     this._store.data[this._collectionName] = [];
     this._store.saveSoon();
     Services.obs.notifyObservers({wrappedJSObject: {
       sourceSync,
       collectionName: this._collectionName,
     }}, "formautofill-storage-changed", "removeAll");
   }
 
+  /**
+   * Strip the computed fields based on the record version.
+   * @param   {Object} record      The record to migrate
+   * @returns {Object}             Migrated record.
+   *                               Record is always cloned, with version updated,
+   *                               with computed fields stripped.
+   *                               Could be a tombstone record, if the record
+   *                               should be discorded.
+   */
+  async _computeMigratedRecord(record) {
+    if (!record.deleted) {
+      record = this._clone(record);
+      await this._stripComputedFields(record);
+      record.version = this.version;
+    }
+    return record;
+  }
+
   async _stripComputedFields(record) {
     this.VALID_COMPUTED_FIELDS.forEach(field => delete record[field]);
   }
 
   // An interface to be inherited.
   _recordReadProcessor(record) {}
 
   // An interface to be inherited.
@@ -1599,28 +1640,82 @@ class CreditCards extends AutofillRecord
       hasNewComputedFields = true;
     }
 
     // Encrypt credit card number
     if (!("cc-number-encrypted" in creditCard)) {
       if ("cc-number" in creditCard) {
         let ccNumber = creditCard["cc-number"];
         creditCard["cc-number"] = CreditCard.getLongMaskedNumber(ccNumber);
-        creditCard["cc-number-encrypted"] = await MasterPassword.encrypt(ccNumber);
+        creditCard["cc-number-encrypted"] = await OSKeyStore.encrypt(ccNumber);
       } else {
         creditCard["cc-number-encrypted"] = "";
       }
     }
 
     return hasNewComputedFields;
   }
 
+  async _computeMigratedRecord(creditCard) {
+    if (creditCard["cc-number-encrypted"]) {
+      switch (creditCard.version) {
+        case 1: {
+          if (!cryptoSDR.isLoggedIn) {
+            // We cannot decrypt the data, so silently remove the record for
+            // the user.
+            if (creditCard.deleted) {
+              break;
+            }
+
+            this.log.warn("Removing version 1 credit card record to migrate to new encryption:", creditCard.guid);
+
+            // Replace the record with a tombstone record here,
+            // regardless of existence of sync metadata.
+            let existingSync = this._getSyncMetaData(creditCard);
+            creditCard = {
+              guid: creditCard.guid,
+              timeLastModified: Date.now(),
+              deleted: true,
+            };
+
+            if (existingSync) {
+              creditCard._sync = existingSync;
+              existingSync.changeCounter++;
+            }
+            break;
+          }
+
+          creditCard = this._clone(creditCard);
+
+          // Decrypt the cc-number using version 1 encryption.
+          let ccNumber = cryptoSDR.decrypt(creditCard["cc-number-encrypted"]);
+          // Re-encrypt the cc-number with version 2 encryption.
+          creditCard["cc-number-encrypted"] = await OSKeyStore.encrypt(ccNumber);
+          break;
+        }
+
+        default:
+          throw new Error("Unknown credit card version to migrate: " + creditCard.version);
+      }
+    }
+    return super._computeMigratedRecord(creditCard);
+  }
+
   async _stripComputedFields(creditCard) {
     if (creditCard["cc-number-encrypted"]) {
-      creditCard["cc-number"] = await MasterPassword.decrypt(creditCard["cc-number-encrypted"]);
+      try {
+        creditCard["cc-number"] = await OSKeyStore.decrypt(creditCard["cc-number-encrypted"]);
+      } catch (ex) {
+        if (ex.result == Cr.NS_ERROR_ABORT) {
+          throw ex;
+        }
+        // Quietly recover from encryption error,
+        // so existing credit card entry with undecryptable number
+        // can be updated.
+      }
     }
     await super._stripComputedFields(creditCard);
   }
 
   _normalizeFields(creditCard) {
     this._normalizeCCName(creditCard);
     this._normalizeCCNumber(creditCard);
     this._normalizeCCExpirationDate(creditCard);
@@ -1671,38 +1766,62 @@ class CreditCards extends AutofillRecord
   }
 
   _validateFields(creditCard) {
     if (!creditCard["cc-number"]) {
       throw new Error("Missing/invalid cc-number");
     }
   }
 
+  _ensureMatchingVersion(record) {
+    if (!record.version || isNaN(record.version) || record.version < 1) {
+      throw new Error(`Got invalid record version ${
+        record.version}; want ${this.version}`);
+    }
+
+    if (record.version < this.version) {
+      switch (record.version) {
+        case 1:
+          // The difference between version 1 and 2 is only about the encryption
+          // method used for the cc-number-encrypted field.
+          // As long as the record is already decrypted, it is safe to bump the
+          // version directly.
+          if (!record["cc-number-encrypted"]) {
+            record.version = this.version;
+          } else {
+            throw new Error("Unexpected record migration path.");
+          }
+          break;
+        default:
+          throw new Error("Unknown credit card version to match: " + record.version);
+      }
+    }
+
+    return super._ensureMatchingVersion(record);
+  }
+
   /**
    * Normalize the given record and return the first matched guid if storage has the same record.
    * @param {Object} targetCreditCard
    *        The credit card for duplication checking.
    * @returns {Promise<string|null>}
    *          Return the first guid if storage has the same credit card and null otherwise.
    */
   async getDuplicateGuid(targetCreditCard) {
     let clonedTargetCreditCard = this._clone(targetCreditCard);
     this._normalizeRecord(clonedTargetCreditCard);
     for (let creditCard of this._data) {
       let isDuplicate = await Promise.all(this.VALID_FIELDS.map(async field => {
         if (!clonedTargetCreditCard[field]) {
           return !creditCard[field];
         }
         if (field == "cc-number" && creditCard[field]) {
-          if (MasterPassword.isEnabled) {
-            // Compare the masked numbers instead when the master password is
-            // enabled because we don't want to leak the credit card number.
-            return CreditCard.getLongMaskedNumber(clonedTargetCreditCard[field]) == creditCard[field];
-          }
-          return (clonedTargetCreditCard[field] == await MasterPassword.decrypt(creditCard["cc-number-encrypted"]));
+          // Compare the masked numbers instead when decryption requires a password
+          // because we don't want to leak the credit card number.
+          return CreditCard.getLongMaskedNumber(clonedTargetCreditCard[field]) == creditCard[field];
         }
         return clonedTargetCreditCard[field] == creditCard[field];
       })).then(fieldResults => fieldResults.every(result => result));
       if (isDuplicate) {
         return creditCard.guid;
       }
     }
     return null;
@@ -1800,17 +1919,20 @@ FormAutofillStorage.prototype = {
    * @rejects  JavaScript exception.
    */
   initialize() {
     if (!this._initializePromise) {
       this._store = new JSONFile({
         path: this._path,
         dataPostProcessor: this._dataPostProcessor.bind(this),
       });
-      this._initializePromise = this._store.load();
+      this._initializePromise = this._store.load()
+        .then(() => Promise.all([
+          this.addresses.initialize(),
+          this.creditCards.initialize()]));
     }
     return this._initializePromise;
   },
 
   _dataPostProcessor(data) {
     data.version = this.version;
     if (!data.addresses) {
       data.addresses = [];
--- a/browser/extensions/formautofill/FormAutofillUtils.jsm
+++ b/browser/extensions/formautofill/FormAutofillUtils.jsm
@@ -12,17 +12,17 @@ const ADDRESS_REFERENCES_EXT = "addressR
 
 const ADDRESSES_COLLECTION_NAME = "addresses";
 const CREDITCARDS_COLLECTION_NAME = "creditCards";
 const MANAGE_ADDRESSES_KEYWORDS = ["manageAddressesTitle", "addNewAddressTitle"];
 const EDIT_ADDRESS_KEYWORDS = [
   "givenName", "additionalName", "familyName", "organization2", "streetAddress",
   "state", "province", "city", "country", "zip", "postalCode", "email", "tel",
 ];
-const MANAGE_CREDITCARDS_KEYWORDS = ["manageCreditCardsTitle", "addNewCreditCardTitle", "showCreditCardsBtnLabel"];
+const MANAGE_CREDITCARDS_KEYWORDS = ["manageCreditCardsTitle", "addNewCreditCardTitle"];
 const EDIT_CREDITCARD_KEYWORDS = ["cardNumber", "nameOnCard", "cardExpiresMonth", "cardExpiresYear", "cardNetwork"];
 const FIELD_STATES = {
   NORMAL: "NORMAL",
   AUTO_FILLED: "AUTO_FILLED",
   PREVIEW: "PREVIEW",
 };
 const SECTION_TYPES = {
   ADDRESS: "address",
rename from browser/extensions/formautofill/MasterPassword.jsm
rename to browser/extensions/formautofill/OSKeyStore.jsm
--- a/browser/extensions/formautofill/MasterPassword.jsm
+++ b/browser/extensions/formautofill/OSKeyStore.jsm
@@ -1,184 +1,251 @@
 /* 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/. */
 
 /**
- * Helpers for the Master Password Dialog.
- * In the future the Master Password implementation may move here.
+ * Helpers for using OS Key Store.
  */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = [
-  "MasterPassword",
+  "OSKeyStore",
 ];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyServiceGetter(this, "cryptoSDR",
-                                   "@mozilla.org/login-manager/crypto/SDR;1",
-                                   Ci.nsILoginManagerCrypto);
+ChromeUtils.defineModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "nativeOSKeyStore",
+                                   "@mozilla.org/security/oskeystore;1", Ci.nsIOSKeyStore);
+
+// Skip reauth during tests, only works in non-official builds.
+const TEST_ONLY_REAUTH = "extensions.formautofill.osKeyStore.unofficialBuildOnlyLogin";
+
+var OSKeyStore = {
+  /**
+   * On macOS this becomes part of the name label visible on Keychain Acesss as
+   * "org.mozilla.nss.keystore.firefox" (where "firefox" is the MOZ_APP_NAME).
+   */
+  STORE_LABEL: AppConstants.MOZ_APP_NAME,
 
-var MasterPassword = {
-  get _token() {
-    let tokendb = Cc["@mozilla.org/security/pk11tokendb;1"].createInstance(Ci.nsIPK11TokenDB);
-    return tokendb.getInternalKeyToken();
+  /**
+   * Consider the module is initialized as locked. OS might unlock without a
+   * prompt.
+   * @type {Boolean}
+   */
+  _isLocked: true,
+
+  _pendingUnlockPromise: null,
+
+  /**
+   * @returns {boolean} True if logged in (i.e. decrypt(reauth = false) will
+   *                    not retrigger a dialog) and false if not.
+   *                    User might log out elsewhere in the OS, so even if this
+   *                    is true a prompt might still pop up.
+   */
+  get isLoggedIn() {
+    return !this._isLocked;
   },
 
   /**
-   * @returns {boolean} True if a master password is set and false otherwise.
+   * @returns {boolean} True if there is another login dialog existing and false
+   *                    otherwise.
    */
-  get isEnabled() {
-    return this._token.hasPassword;
+  get isUIBusy() {
+    return !!this._pendingUnlockPromise;
   },
 
   /**
-   * @returns {boolean} True if master password is logged in and false if not.
+   * If the test pref exist and applicable,
+   * this method will dispatch a observer message and return
+   * to simulate successful reauth, or throw to simulate
+   * failed reauth.
+   *
+   * @returns {boolean} True when reauth should NOT be skipped,
+   *                    false when reauth has been skipped.
+   * @throws            If it needs to simulate reauth login failure.
    */
-  get isLoggedIn() {
-    return Services.logins.isLoggedIn;
+  _maybeSkipReauthForTest() {
+    // Don't take test reauth pref in the following configurations.
+    if (nativeOSKeyStore.isNSSKeyStore ||
+        AppConstants.MOZILLA_OFFICIAL ||
+        !this._testReauth) {
+      return true;
+    }
+
+    // Skip this reauth because there is no way to mock the
+    // native dialog in the testing environment, for now.
+    log.debug("_ensureReauth: _testReauth: ", this._testReauth);
+    switch (this._testReauth) {
+      case "pass":
+        Services.obs.notifyObservers(null, "oskeystore-testonly-reauth", "pass");
+        return false;
+      case "cancel":
+        Services.obs.notifyObservers(null, "oskeystore-testonly-reauth", "cancel");
+        throw new Components.Exception("Simulating user cancelling login dialog", Cr.NS_ERROR_FAILURE);
+      default:
+        throw new Components.Exception("Unknown test pref value", Cr.NS_ERROR_FAILURE);
+    }
   },
 
   /**
-   * @returns {boolean} True if there is another master password login dialog
-   *                    existing and false otherwise.
-   */
-  get isUIBusy() {
-    return Services.logins.uiBusy;
-  },
-
-  /**
-   * Ensure the master password is logged in. It will display the master password
-   * login prompt or do nothing if it's logged in already. If an existing MP
+   * Ensure the store in use is logged in. It will display the OS login
+   * login prompt or do nothing if it's logged in already. If an existing login
    * prompt is already prompted, the result from it will be used instead.
    *
+   * Note: This method must set _pendingUnlockPromise before returning the
+   * promise (i.e. the first |await|), otherwise we'll risk re-entry.
+   * This is why there aren't an |await| in the method. The method is marked as
+   * |async| to communicate that it's async.
+   *
    * @param   {boolean} reauth Prompt the login dialog no matter it's logged in
    *                           or not if it's set to true.
    * @returns {Promise<boolean>} True if it's logged in or no password is set
    *                             and false if it's still not logged in (prompt
    *                             canceled or other error).
    */
   async ensureLoggedIn(reauth = false) {
-    if (!this.isEnabled) {
-      return true;
+    if (this._pendingUnlockPromise) {
+      log.debug("ensureLoggedIn: Has a pending unlock operation");
+      return this._pendingUnlockPromise;
     }
+    log.debug("ensureLoggedIn: Creating new pending unlock promise. reauth: ", reauth);
+
+    // TODO: Implementing re-auth by passing this value to the native implementation
+    // in some way. Set this to false for now to ignore the reauth request (bug 1429265).
+    reauth = false;
+
+    let unlockPromise = Promise.resolve().then(async () => {
+      if (reauth) {
+        reauth = this._maybeSkipReauthForTest();
+      }
 
-    if (this.isLoggedIn && !reauth) {
-      return true;
-    }
+      if (!await nativeOSKeyStore.asyncSecretAvailable(this.STORE_LABEL)) {
+        log.debug("ensureLoggedIn: Secret unavailable, attempt to generate new secret.");
+        let recoveryPhrase = await nativeOSKeyStore.asyncGenerateSecret(this.STORE_LABEL);
+        // TODO We should somehow have a dialog to ask the user to write this down,
+        // and another dialog somewhere for the user to restore the secret with it.
+        // (Intentionally not printing it out in the console)
+        log.debug("ensureLoggedIn: Secret generated. Recovery phrase length: " + recoveryPhrase.length);
+      }
+    });
 
-    // If a prompt is already showing then wait for and focus it.
-    if (this.isUIBusy) {
-      return this.waitForExistingDialog();
+    if (nativeOSKeyStore.isNSSKeyStore) {
+      // Workaround bug 1492305: NSS-implemented methods don't reject when user cancels.
+      unlockPromise = unlockPromise.then(() => {
+        log.debug("ensureLoggedIn: isNSSKeyStore: ", reauth, Services.logins.isLoggedIn);
+        // User has hit the cancel button on the master password prompt.
+        // We must reject the promise chain here.
+        if (!Services.logins.isLoggedIn) {
+          throw Components.Exception("User canceled OS unlock entry (Workaround)", Cr.NS_ERROR_FAILURE);
+        }
+      });
     }
 
-    let token = this._token;
-    try {
-      // 'true' means always prompt for token password. User will be prompted until
-      // clicking 'Cancel' or entering the correct password.
-      token.login(true);
-    } catch (e) {
-      // An exception will be thrown if the user cancels the login prompt dialog.
-      // User is also logged out.
-    }
+    unlockPromise = unlockPromise.then(() => {
+      log.debug("ensureLoggedIn: Logged in");
+      this._pendingUnlockPromise = null;
+      this._isLocked = false;
 
-    // If we triggered a master password prompt, notify observers.
-    if (token.isLoggedIn()) {
-      Services.obs.notifyObservers(null, "passwordmgr-crypto-login");
-    } else {
-      Services.obs.notifyObservers(null, "passwordmgr-crypto-loginCanceled");
-    }
+      return true;
+    }, (err) => {
+      log.debug("ensureLoggedIn: Not logged in", err);
+      this._pendingUnlockPromise = null;
+      this._isLocked = true;
 
-    return token.isLoggedIn();
+      return false;
+    });
+
+    this._pendingUnlockPromise = unlockPromise;
+
+    return this._pendingUnlockPromise;
   },
 
   /**
    * Decrypts cipherText.
    *
+   * Note: In the event of an rejection, check the result property of the Exception
+   *       object. Handles NS_ERROR_ABORT as user has cancelled the action (e.g.,
+   *       don't show that dialog), apart from other errors (e.g., gracefully
+   *       recover from that and still shows the dialog.)
+   *
    * @param   {string} cipherText Encrypted string including the algorithm details.
    * @param   {boolean} reauth True if we want to force the prompt to show up
    *                    even if the user is already logged in.
    * @returns {Promise<string>} resolves to the decrypted string, or rejects otherwise.
    */
   async decrypt(cipherText, reauth = false) {
     if (!await this.ensureLoggedIn(reauth)) {
-      throw Components.Exception("User canceled master password entry", Cr.NS_ERROR_ABORT);
+      throw Components.Exception("User canceled OS unlock entry", Cr.NS_ERROR_ABORT);
     }
-    return cryptoSDR.decrypt(cipherText);
+    let bytes = await nativeOSKeyStore.asyncDecryptBytes(this.STORE_LABEL, cipherText);
+    return String.fromCharCode.apply(String, bytes);
   },
 
   /**
    * Encrypts a string and returns cipher text containing algorithm information used for decryption.
    *
    * @param   {string} plainText Original string without encryption.
    * @returns {Promise<string>} resolves to the encrypted string (with algorithm), otherwise rejects.
    */
   async encrypt(plainText) {
     if (!await this.ensureLoggedIn()) {
-      throw Components.Exception("User canceled master password entry", Cr.NS_ERROR_ABORT);
+      throw Components.Exception("User canceled OS unlock entry", Cr.NS_ERROR_ABORT);
     }
 
-    return cryptoSDR.encrypt(plainText);
+    // Convert plain text into a UTF-8 binary string
+    plainText = unescape(encodeURIComponent(plainText));
+
+    // Convert it to an array
+    let textArr = [];
+    for (let char of plainText) {
+      textArr.push(char.charCodeAt(0));
+    }
+
+    let rawEncryptedText = await nativeOSKeyStore.asyncEncryptBytes(this.STORE_LABEL, textArr.length, textArr);
+
+    // Mark the output with a version number.
+    return rawEncryptedText;
   },
 
   /**
-   * Resolve when master password dialogs are closed, immediately if none are open.
+   * Resolve when the login dialogs are closed, immediately if none are open.
    *
    * An existing MP dialog will be focused and will request attention.
    *
    * @returns {Promise<boolean>}
    *          Resolves with whether the user is logged in to MP.
    */
   async waitForExistingDialog() {
-    if (!this.isUIBusy) {
-      log.debug("waitForExistingDialog: Dialog isn't showing. isLoggedIn:", this.isLoggedIn);
-      return this.isLoggedIn;
+    if (this.isUIBusy) {
+      return this._pendingUnlockPromise;
     }
-
-    return new Promise((resolve) => {
-      log.debug("waitForExistingDialog: Observing the open dialog");
-      let observer = {
-        QueryInterface: ChromeUtils.generateQI([
-          Ci.nsIObserver,
-          Ci.nsISupportsWeakReference,
-        ]),
+    return this.isLoggedIn;
+  },
 
-        observe(subject, topic, data) {
-          log.debug("waitForExistingDialog: Got notification:", topic);
-          // Only run observer once.
-          Services.obs.removeObserver(this, "passwordmgr-crypto-login");
-          Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled");
-          if (topic == "passwordmgr-crypto-loginCanceled") {
-            resolve(false);
-            return;
-          }
-
-          resolve(true);
-        },
-      };
+  /**
+   * Remove the store. For tests.
+   */
+  async cleanup() {
+    return nativeOSKeyStore.asyncDeleteSecret(this.STORE_LABEL);
+  },
 
-      // Possible leak: it's possible that neither of these notifications
-      // will fire, and if that happens, we'll leak the observer (and
-      // never return). We should guarantee that at least one of these
-      // will fire.
-      // See bug XXX.
-      Services.obs.addObserver(observer, "passwordmgr-crypto-login");
-      Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled");
-
-      // Focus and draw attention to the existing master password dialog for the
-      // occassions where it's not attached to the current window.
-      let promptWin = Services.wm.getMostRecentWindow("prompt:promptPassword");
-      promptWin.focus();
-      promptWin.getAttention();
-    });
+  /**
+   * Check if the implementation is using the NSS key store.
+   * If so, tests will be able to handle the reauth dialog.
+   */
+  get isNSSKeyStore() {
+    return nativeOSKeyStore.isNSSKeyStore;
   },
 };
 
 XPCOMUtils.defineLazyGetter(this, "log", () => {
   let ConsoleAPI = ChromeUtils.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
   return new ConsoleAPI({
-    maxLogLevelPref: "masterPassword.loglevel",
-    prefix: "Master Password",
+    maxLogLevelPref: "extensions.formautofill.loglevel",
+    prefix: "OSKeyStore",
   });
 });
+
+XPCOMUtils.defineLazyPreferenceGetter(OSKeyStore, "_testReauth", TEST_ONLY_REAUTH, "");
--- a/browser/extensions/formautofill/content/manageCreditCards.xhtml
+++ b/browser/extensions/formautofill/content/manageCreditCards.xhtml
@@ -15,29 +15,27 @@
 </head>
 <body dir="&locale.dir;">
   <fieldset>
     <legend data-localization="creditCardsListHeader"/>
     <select id="credit-cards" size="9" multiple="multiple"/>
   </fieldset>
   <div id="controls-container">
     <button id="remove" disabled="disabled" data-localization="removeBtnLabel"/>
-    <button id="show-hide-credit-cards" data-localization="showCreditCardsBtnLabel"/>
     <!-- Wrapper is used to properly compute the search tooltip position -->
     <div>
       <button id="add" data-localization="addBtnLabel"/>
     </div>
     <button id="edit" disabled="disabled" data-localization="editBtnLabel"/>
   </div>
   <script type="application/javascript">
     "use strict";
     /* global ManageCreditCards */
     new ManageCreditCards({
       records: document.getElementById("credit-cards"),
       controlsContainer: document.getElementById("controls-container"),
       remove: document.getElementById("remove"),
-      showHideCreditCards: document.getElementById("show-hide-credit-cards"),
       add: document.getElementById("add"),
       edit: document.getElementById("edit"),
     });
   </script>
 </body>
 </html>
--- a/browser/extensions/formautofill/content/manageDialog.js
+++ b/browser/extensions/formautofill/content/manageDialog.js
@@ -14,18 +14,18 @@ ChromeUtils.import("resource://formautof
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 
 ChromeUtils.defineModuleGetter(this, "CreditCard",
                                "resource://gre/modules/CreditCard.jsm");
 ChromeUtils.defineModuleGetter(this, "formAutofillStorage",
                                "resource://formautofill/FormAutofillStorage.jsm");
 ChromeUtils.defineModuleGetter(this, "FormAutofillUtils",
                                "resource://formautofill/FormAutofillUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "MasterPassword",
-                               "resource://formautofill/MasterPassword.jsm");
+ChromeUtils.defineModuleGetter(this, "OSKeyStore",
+                               "resource://formautofill/OSKeyStore.jsm");
 
 this.log = null;
 FormAutofill.defineLazyLogGetter(this, "manageAddresses");
 
 class ManageRecords {
   constructor(subStorageName, elements) {
     this._storageInitPromise = formAutofillStorage.initialize();
     this._subStorageName = subStorageName;
@@ -308,35 +308,43 @@ class ManageAddresses extends ManageReco
 }
 
 class ManageCreditCards extends ManageRecords {
   constructor(elements) {
     super("creditCards", elements);
     elements.add.setAttribute("searchkeywords", FormAutofillUtils.EDIT_CREDITCARD_KEYWORDS
                                                   .map(key => FormAutofillUtils.stringBundle.GetStringFromName(key))
                                                   .join("\n"));
-    this._hasMasterPassword = MasterPassword.isEnabled;
     this._isDecrypted = false;
-    if (this._hasMasterPassword) {
-      elements.showHideCreditCards.setAttribute("hidden", true);
-    }
   }
 
   /**
    * Open the edit address dialog to create/edit a credit card.
    *
    * @param  {object} creditCard [optional]
    */
   async openEditDialog(creditCard) {
-    // If master password is set, ask for password if user is trying to edit an
-    // existing credit card.
-    if (!creditCard || !this._hasMasterPassword || await MasterPassword.ensureLoggedIn(true)) {
+    // Ask for reauth if user is trying to edit an existing credit card.
+    if (!creditCard || await OSKeyStore.ensureLoggedIn(true)) {
       let decryptedCCNumObj = {};
       if (creditCard) {
-        decryptedCCNumObj["cc-number"] = await MasterPassword.decrypt(creditCard["cc-number-encrypted"]);
+        try {
+          decryptedCCNumObj["cc-number"] = await OSKeyStore.decrypt(creditCard["cc-number-encrypted"]);
+        } catch (ex) {
+          if (ex.result == Cr.NS_ERROR_ABORT) {
+            // User shouldn't be ask to reauth here, but it could happen.
+            // Return here and skip opening the dialog.
+            return;
+          }
+          // We've got ourselves a real error.
+          // Recover from encryption error so the user gets a chance to re-enter
+          // unencrypted credit card number.
+          decryptedCCNumObj["cc-number"] = "";
+          Cu.reportError(ex);
+        }
       }
       let decryptedCreditCard = Object.assign({}, creditCard, decryptedCCNumObj);
       this.prefWin.gSubDialog.open(EDIT_CREDIT_CARD_URL, "resizable=no", {
         record: decryptedCreditCard,
       });
     }
   }
 
@@ -354,22 +362,16 @@ class ManageCreditCards extends ManageRe
       encryptedNumber: creditCard["cc-number-encrypted"],
       number: creditCard["cc-number"],
       name: creditCard["cc-name"],
       network: creditCard["cc-type"],
     });
     return cardObj.getLabel({showNumbers: showCreditCards});
   }
 
-  async toggleShowHideCards(options) {
-    this._isDecrypted = !this._isDecrypted;
-    this.updateShowHideButtonState();
-    await this.updateLabels(options, this._isDecrypted);
-  }
-
   async updateLabels(options, isDecrypted) {
     for (let option of options) {
       option.text = await this.getLabel(option.record, isDecrypted);
     }
     // For testing only: Notify when credit cards labels have been updated
     this._elements.records.dispatchEvent(new CustomEvent("LabelsUpdated"));
   }
 
@@ -387,30 +389,15 @@ class ManageCreditCards extends ManageRe
         option.setAttribute("cc-type", record["cc-type"]);
       } else {
         option.removeAttribute("cc-type");
       }
     }
   }
 
   updateButtonsStates(selectedCount) {
-    this.updateShowHideButtonState();
     super.updateButtonsStates(selectedCount);
   }
 
-  updateShowHideButtonState() {
-    if (this._elements.records.length) {
-      this._elements.showHideCreditCards.removeAttribute("disabled");
-    } else {
-      this._elements.showHideCreditCards.setAttribute("disabled", true);
-    }
-    this._elements.showHideCreditCards.textContent =
-      this._isDecrypted ? FormAutofillUtils.stringBundle.GetStringFromName("hideCreditCardsBtnLabel") :
-                          FormAutofillUtils.stringBundle.GetStringFromName("showCreditCardsBtnLabel");
-  }
-
   handleClick(event) {
-    if (event.target == this._elements.showHideCreditCards) {
-      this.toggleShowHideCards(this._elements.records.options);
-    }
     super.handleClick(event);
   }
 }
--- a/browser/extensions/formautofill/locales/en-US/formautofill.properties
+++ b/browser/extensions/formautofill/locales/en-US/formautofill.properties
@@ -99,18 +99,16 @@ savedCreditCardsBtnLabel = Saved Credit Cards…
 # LOCALIZATION NOTE (manageAddressesTitle, manageCreditCardsTitle): The dialog title for the list of addresses or
 # credit cards in browser preferences.
 manageAddressesTitle = Saved Addresses
 manageCreditCardsTitle = Saved Credit Cards
 # LOCALIZATION NOTE (addressesListHeader, creditCardsListHeader): The header for the list of addresses or credit cards
 # in browser preferences.
 addressesListHeader = Addresses
 creditCardsListHeader = Credit Cards
-showCreditCardsBtnLabel = Show Credit Cards
-hideCreditCardsBtnLabel = Hide Credit Cards
 removeBtnLabel = Remove
 addBtnLabel = Add…
 editBtnLabel = Edit…
 # LOCALIZATION NOTE (manageDialogsWidth): This strings sets the default width for windows used to manage addresses and
 # credit cards.
 manageDialogsWidth = 560px
 
 # LOCALIZATION NOTE (addNewAddressTitle, editAddressTitle): The dialog title for creating or editing addresses
--- a/browser/extensions/formautofill/moz.build
+++ b/browser/extensions/formautofill/moz.build
@@ -27,16 +27,18 @@ elif CONFIG['OS_ARCH'] == 'Darwin':
         'skin/osx/editDialog.css',
     ]
 elif CONFIG['OS_ARCH'] == 'WINNT':
     FINAL_TARGET_FILES.features['formautofill@mozilla.org'].chrome.res += [
         'skin/windows/autocomplete-item.css',
         'skin/windows/editDialog.css',
     ]
 
+TESTING_JS_MODULES += ['test/fixtures/OSKeyStoreTestUtils.jsm']
+
 BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
 
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 
 MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini']
 
 JAR_MANIFESTS += ['jar.mn']
 
--- a/browser/extensions/formautofill/test/browser/browser.ini
+++ b/browser/extensions/formautofill/test/browser/browser.ini
@@ -10,17 +10,18 @@ support-files =
 [browser_autocomplete_footer.js]
 skip-if = verify
 [browser_autocomplete_marked_back_forward.js]
 [browser_autocomplete_marked_detached_tab.js]
 skip-if = (verify && (os == 'win' || os == 'mac'))
 [browser_check_installed.js]
 [browser_creditCard_doorhanger.js]
 skip-if = (os == "linux") || (os == "mac" && debug) || (os == "win") # bug 1425884
-[browser_creditCard_fill_master_password.js]
+[browser_creditCard_fill_cancel_login.js]
+skip-if = true # Re-auth is not implemented, cannot cancel OS key store login (bug 1429265)
 [browser_dropdown_layout.js]
 [browser_editAddressDialog.js]
 [browser_editCreditCardDialog.js]
 skip-if = (verify && (os == 'linux'))
 [browser_first_time_use_doorhanger.js]
 skip-if = verify
 [browser_insecure_form.js]
 skip-if = (os == 'linux' && !debug) || (os == 'win') # bug 1456284
--- a/browser/extensions/formautofill/test/browser/browser_creditCard_doorhanger.js
+++ b/browser/extensions/formautofill/test/browser/browser_creditCard_doorhanger.js
@@ -46,16 +46,17 @@ add_task(async function test_submit_cred
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
+      let onChanged = TestUtils.topicObserved("formautofill-storage-changed");
       await ContentTask.spawn(browser, null, async function() {
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
         name.focus();
         name.setUserInput("User 1");
 
         form.querySelector("#cc-number").setUserInput("5038146897157463");
         form.querySelector("#cc-exp-month").setUserInput("12");
@@ -64,54 +65,66 @@ add_task(async function test_submit_cred
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
+      await onChanged;
     }
   );
 
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
   is(creditCards[0]["cc-name"], "User 1", "Verify the name field");
   is(creditCards[0]["cc-type"], "mastercard", "Verify the cc-type field");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 2, "User has seen the doorhanger");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_submit_untouched_creditCard_form() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+    return;
+  }
+
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard(TEST_CREDIT_CARD_1);
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
+
+  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+  let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
+                                       (subject, data) => data == "notifyUsed");
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       await openPopupOn(browser, "form #cc-name");
       await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
       await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+      await osKeyStoreLoginShown;
       await ContentTask.spawn(browser, null, async function() {
         let form = content.document.getElementById("form");
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await sleep(1000);
       is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
     }
   );
+  await onUsed;
 
   creditCards = await getCreditCards();
   is(creditCards.length, 1, "Still 1 credit card");
   is(creditCards[0].timesUsed, 1, "timesUsed field set to 1");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 3, "User has used autofill");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
@@ -120,16 +133,19 @@ add_task(async function test_submit_chan
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard(TEST_CREDIT_CARD_1);
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
+
+  let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
+                                       (subject, data) => data == "notifyUsed");
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
       await ContentTask.spawn(browser, null, async function() {
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
 
@@ -144,16 +160,17 @@ add_task(async function test_submit_chan
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
     }
   );
+  await onUsed;
 
   creditCards = await getCreditCards();
   is(creditCards.length, 1, "Still 1 credit card in storage");
   is(creditCards[0]["cc-name"], TEST_CREDIT_CARD_1["cc-name"], "name field still exists");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 2, "User has seen the doorhanger");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
@@ -269,88 +286,16 @@ add_task(async function test_submit_cred
   let creditCardPref = SpecialPowers.getBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF);
   is(creditCards.length, 0, "No credit card in storage");
   is(creditCardPref, false, "Credit card is disabled");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 2, "User has seen the doorhanger");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   SpecialPowers.clearUserPref(ENABLED_AUTOFILL_CREDITCARDS_PREF);
 });
 
-add_task(async function test_submit_creditCard_saved_with_mp_enabled() {
-  LoginTestUtils.masterPassword.enable();
-  // Login with the masterPassword in LoginTestUtils.
-  let masterPasswordDialogShown = waitForMasterPasswordDialog(true);
-  await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
-    async function(browser) {
-      let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
-                                                       "popupshown");
-      await ContentTask.spawn(browser, null, async function() {
-        let form = content.document.getElementById("form");
-        let name = form.querySelector("#cc-name");
-        name.focus();
-        await new Promise(resolve => setTimeout(resolve, 1000));
-        name.setUserInput("User 0");
-
-        let number = form.querySelector("#cc-number");
-        number.setUserInput("6387060366272981");
-
-        // Wait 1000ms before submission to make sure the input value applied
-        await new Promise(resolve => setTimeout(resolve, 1000));
-        form.querySelector("input[type=submit]").click();
-      });
-
-      await promiseShown;
-      await clickDoorhangerButton(MAIN_BUTTON);
-      await masterPasswordDialogShown;
-      await TestUtils.topicObserved("formautofill-storage-changed");
-    }
-  );
-
-  let creditCards = await getCreditCards();
-  is(creditCards.length, 1, "1 credit card in storage");
-  is(creditCards[0]["cc-name"], "User 0", "Verify the name field");
-  is(creditCards[0]["cc-number"], "************2981", "Verify the card number field");
-  LoginTestUtils.masterPassword.disable();
-  await removeAllRecords();
-});
-
-add_task(async function test_submit_creditCard_saved_with_mp_enabled_but_canceled() {
-  LoginTestUtils.masterPassword.enable();
-  let masterPasswordDialogShown = waitForMasterPasswordDialog();
-  await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
-    async function(browser) {
-      let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
-                                                       "popupshown");
-      await ContentTask.spawn(browser, null, async function() {
-        let form = content.document.getElementById("form");
-        let name = form.querySelector("#cc-name");
-        name.focus();
-        await new Promise(resolve => setTimeout(resolve, 1000));
-        name.setUserInput("User 2");
-
-        let number = form.querySelector("#cc-number");
-        number.setUserInput("5471839082338112");
-
-        // Wait 1000ms before submission to make sure the input value applied
-        await new Promise(resolve => setTimeout(resolve, 1000));
-        form.querySelector("input[type=submit]").click();
-      });
-
-      await promiseShown;
-      await clickDoorhangerButton(MAIN_BUTTON);
-      await masterPasswordDialogShown;
-    }
-  );
-
-  await sleep(1000);
-  let creditCards = await getCreditCards();
-  is(creditCards.length, 0, "No credit cards in storage");
-  LoginTestUtils.masterPassword.disable();
-});
-
 add_task(async function test_submit_creditCard_with_sync_account() {
   await SpecialPowers.pushPrefEnv({
     "set": [
       [SYNC_USERNAME_PREF, "foo@bar.com"],
       [SYNC_CREDITCARDS_AVAILABLE_PREF, true],
     ],
   });
 
@@ -440,16 +385,18 @@ add_task(async function test_submit_manu
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard(TEST_CREDIT_CARD_3);
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
+  let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
+                                       (subject, data) => data == "notifyUsed");
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
       await ContentTask.spawn(browser, null, async function() {
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
         name.focus();
@@ -462,195 +409,255 @@ add_task(async function test_submit_manu
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
     }
   );
+  await onUsed;
 
   creditCards = await getCreditCards();
   is(creditCards.length, 1, "Still 1 credit card in storage");
   is(creditCards[0]["cc-name"], "User 3", "Verify the name field");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 2, "User has seen the doorhanger");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_update_autofill_form_name() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+    return;
+  }
+
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard(TEST_CREDIT_CARD_1);
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
+  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+  let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
+                                       (subject, data) => data == "notifyUsed");
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
       await openPopupOn(browser, "form #cc-name");
       await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
       await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+      await osKeyStoreLoginShown;
       await ContentTask.spawn(browser, null, async function() {
+        await ContentTaskUtils.waitForCondition(() => {
+          let form = content.document.getElementById("form");
+          let name = form.querySelector("#cc-name");
+          return name.value == "John Doe";
+        }, "Credit card detail never fills");
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
         name.setUserInput("User 1");
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
-
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
     }
   );
+  await onUsed;
 
   creditCards = await getCreditCards();
   is(creditCards.length, 1, "Still 1 credit card");
   is(creditCards[0]["cc-name"], "User 1", "cc-name field is updated");
   is(creditCards[0]["cc-number"], "************1111", "Verify the card number field");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 3, "User has used autofill");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_update_autofill_form_exp_date() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+    return;
+  }
+
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard(TEST_CREDIT_CARD_1);
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
+  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+  let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
+                                       (subject, data) => data == "notifyUsed");
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
       await openPopupOn(browser, "form #cc-name");
       await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
       await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
       await ContentTask.spawn(browser, null, async function() {
+        await ContentTaskUtils.waitForCondition(() => {
+          let form = content.document.getElementById("form");
+          let name = form.querySelector("#cc-name");
+          return name.value == "John Doe";
+        }, "Credit card detail never fills");
         let form = content.document.getElementById("form");
         let year = form.querySelector("#cc-exp-year");
         year.setUserInput("2020");
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
+      await osKeyStoreLoginShown;
     }
   );
+  await onUsed;
 
   creditCards = await getCreditCards();
   is(creditCards.length, 1, "Still 1 credit card");
   is(creditCards[0]["cc-exp-year"], "2020", "cc-exp-year field is updated");
   is(creditCards[0]["cc-number"], "************1111", "Verify the card number field");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 3, "User has used autofill");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_create_new_autofill_form() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+    return;
+  }
+
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard(TEST_CREDIT_CARD_1);
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
+  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
+      let onChanged = TestUtils.topicObserved("formautofill-storage-changed");
       await openPopupOn(browser, "form #cc-name");
       await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
       await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
       await ContentTask.spawn(browser, null, async function() {
+        await ContentTaskUtils.waitForCondition(() => {
+          let form = content.document.getElementById("form");
+          let name = form.querySelector("#cc-name");
+          return name.value == "John Doe";
+        }, "Credit card detail never fills");
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
         name.setUserInput("User 1");
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await promiseShown;
       await clickDoorhangerButton(SECONDARY_BUTTON);
+      await osKeyStoreLoginShown;
+      await onChanged;
     }
   );
 
   creditCards = await getCreditCards();
   is(creditCards.length, 2, "2 credit cards in storage");
   is(creditCards[0]["cc-name"], TEST_CREDIT_CARD_1["cc-name"],
      "Original record's cc-name field is unchanged");
   is(creditCards[1]["cc-name"], "User 1", "cc-name field in the new record");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 3, "User has used autofill");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_update_duplicate_autofill_form() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+    return;
+  }
+
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard({
     "cc-number": "6387060366272981",
   });
   await saveCreditCard({
     "cc-number": "5038146897157463",
   });
   let creditCards = await getCreditCards();
   is(creditCards.length, 2, "2 credit card in storage");
+  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+  let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
+                                       (subject, data) => data == "notifyUsed");
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       await openPopupOn(browser, "form #cc-number");
       await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
       await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
       await ContentTask.spawn(browser, null, async function() {
+        await ContentTaskUtils.waitForCondition(() => {
+          let form = content.document.getElementById("form");
+          let number = form.querySelector("#cc-number");
+          return number.value == "6387060366272981";
+        }, "Should be the first credit card number");
+
+        // Change number to the second credit card number
         let form = content.document.getElementById("form");
         let number = form.querySelector("#cc-number");
-        is(number.value, "6387060366272981", "Should be the first credit card number");
-        // Change number to the second credit card number
         number.setUserInput("5038146897157463");
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await sleep(1000);
       is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
+      await osKeyStoreLoginShown;
     }
   );
+  await onUsed;
 
   creditCards = await getCreditCards();
   is(creditCards.length, 2, "Still 2 credit card");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 1,
     "User neither sees the doorhanger nor uses autofill but somehow has a record in the storage");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_submit_creditCard_with_invalid_network() {
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
+      let onChanged = TestUtils.topicObserved("formautofill-storage-changed");
       await ContentTask.spawn(browser, null, async function() {
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
         name.focus();
         name.setUserInput("User 1");
 
         form.querySelector("#cc-number").setUserInput("5038146897157463");
         form.querySelector("#cc-exp-month").setUserInput("12");
@@ -659,16 +666,17 @@ add_task(async function test_submit_cred
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
+      await onChanged;
     }
   );
 
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
   is(creditCards[0]["cc-name"], "User 1", "Verify the name field");
   is(creditCards[0]["cc-type"], undefined, "Invalid network/cc-type was not saved");
 
rename from browser/extensions/formautofill/test/browser/browser_creditCard_fill_master_password.js
rename to browser/extensions/formautofill/test/browser/browser_creditCard_fill_cancel_login.js
--- a/browser/extensions/formautofill/test/browser/browser_creditCard_fill_master_password.js
+++ b/browser/extensions/formautofill/test/browser/browser_creditCard_fill_cancel_login.js
@@ -1,25 +1,25 @@
 "use strict";
 
-add_task(async function test_fill_creditCard_with_mp_enabled_but_canceled() {
+add_task(async function test_fill_creditCard_but_cancel_login() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+    return;
+  }
+
   await saveCreditCard(TEST_CREDIT_CARD_2);
 
-  LoginTestUtils.masterPassword.enable();
-  registerCleanupFunction(() => {
-    LoginTestUtils.masterPassword.disable();
-  });
-
-  let masterPasswordDialogShown = waitForMasterPasswordDialog(false); // cancel
+  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(false); // cancel
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       await openPopupOn(browser, "#cc-name");
       const ccItem = getDisplayedPopupItems(browser)[0];
       await EventUtils.synthesizeMouseAtCenter(ccItem, {});
-      await Promise.all([masterPasswordDialogShown, expectPopupClose(browser)]);
+      await Promise.all([osKeyStoreLoginShown, expectPopupClose(browser)]);
 
       await ContentTask.spawn(browser, {}, async function() {
         is(content.document.querySelector("#cc-name").value, "", "Check name");
         is(content.document.querySelector("#cc-number").value, "", "Check number");
       });
     }
   );
 });
--- a/browser/extensions/formautofill/test/browser/browser_manageCreditCardsDialog.js
+++ b/browser/extensions/formautofill/test/browser/browser_manageCreditCardsDialog.js
@@ -1,32 +1,29 @@
 "use strict";
 
 const TEST_SELECTORS = {
   selRecords: "#credit-cards",
   btnRemove: "#remove",
-  btnShowHideCreditCards: "#show-hide-credit-cards",
   btnAdd: "#add",
   btnEdit: "#edit",
 };
 
 const DIALOG_SIZE = "width=600,height=400";
 
 add_task(async function test_manageCreditCardsInitialState() {
   await BrowserTestUtils.withNewTab({gBrowser, url: MANAGE_CREDIT_CARDS_DIALOG_URL}, async function(browser) {
     await ContentTask.spawn(browser, TEST_SELECTORS, (args) => {
       let selRecords = content.document.querySelector(args.selRecords);
       let btnRemove = content.document.querySelector(args.btnRemove);
-      let btnShowHideCreditCards = content.document.querySelector(args.btnShowHideCreditCards);
       let btnAdd = content.document.querySelector(args.btnAdd);
       let btnEdit = content.document.querySelector(args.btnEdit);
 
       is(selRecords.length, 0, "No credit card");
       is(btnRemove.disabled, true, "Remove button disabled");
-      is(btnShowHideCreditCards.disabled, true, "Show Credit Cards button disabled");
       is(btnAdd.disabled, false, "Add button enabled");
       is(btnEdit.disabled, true, "Edit button disabled");
     });
   });
 });
 
 add_task(async function test_cancelManageCreditCardsDialogWithESC() {
   let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL);
@@ -102,67 +99,16 @@ add_task(async function test_creditCards
   is(selRecords.length, 1, "One credit card is shown");
 
   await removeCreditCards([selRecords.options[0].value]);
   await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
   is(selRecords.length, 0, "Credit card is removed");
   win.close();
 });
 
-add_task(async function test_showCreditCards() {
-  await SpecialPowers.pushPrefEnv({"set": [["privacy.reduceTimerPrecision", false]]});
-  await saveCreditCard(TEST_CREDIT_CARD_1);
-  await saveCreditCard(TEST_CREDIT_CARD_2);
-  await saveCreditCard(TEST_CREDIT_CARD_3);
-
-  let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL, null, DIALOG_SIZE);
-  await waitForFocusAndFormReady(win);
-
-  let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
-  let btnShowHideCreditCards = win.document.querySelector(TEST_SELECTORS.btnShowHideCreditCards);
-
-  is(btnShowHideCreditCards.disabled, false, "Show credit cards button enabled");
-  is(btnShowHideCreditCards.textContent, "Show Credit Cards", "Label should be 'Show Credit Cards'");
-
-  // Show credit card numbers
-  EventUtils.synthesizeMouseAtCenter(btnShowHideCreditCards, {}, win);
-  await BrowserTestUtils.waitForEvent(selRecords, "LabelsUpdated");
-  is(selRecords[0].text, "5103059495477870", "Decrypted credit card 3");
-  is(selRecords[1].text, "4929001587121045, Timothy Berners-Lee", "Decrypted credit card 2");
-  is(selRecords[2].text, "4111111111111111, John Doe", "Decrypted credit card 1");
-  is(btnShowHideCreditCards.textContent, "Hide Credit Cards", "Label should be 'Hide Credit Cards'");
-
-  // Hide credit card numbers
-  EventUtils.synthesizeMouseAtCenter(btnShowHideCreditCards, {}, win);
-  await BrowserTestUtils.waitForEvent(selRecords, "LabelsUpdated");
-  is(selRecords[0].text, "**** 7870", "Masked credit card 3");
-  is(selRecords[1].text, "**** 1045, Timothy Berners-Lee", "Masked credit card 2");
-  is(selRecords[2].text, "**** 1111, John Doe", "Masked credit card 1");
-  is(btnShowHideCreditCards.textContent, "Show Credit Cards", "Label should be 'Show Credit Cards'");
-
-  // Show credit card numbers again to test if they revert back to masked form when reloaded
-  EventUtils.synthesizeMouseAtCenter(btnShowHideCreditCards, {}, win);
-  await BrowserTestUtils.waitForEvent(selRecords, "LabelsUpdated");
-  // Ensure credit card numbers are shown again
-  is(selRecords[0].text, "5103059495477870", "Decrypted credit card 3");
-  // Remove a card to trigger reloading
-  await removeCreditCards([selRecords.options[2].value]);
-  await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
-  is(selRecords[0].text, "**** 7870", "Masked credit card 3");
-  is(selRecords[1].text, "**** 1045, Timothy Berners-Lee", "Masked credit card 2");
-
-  // Remove the rest of the cards
-  await removeCreditCards([selRecords.options[1].value]);
-  await removeCreditCards([selRecords.options[0].value]);
-  await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
-  is(btnShowHideCreditCards.disabled, true, "Show credit cards button is disabled when there is no card");
-
-  win.close();
-});
-
 add_task(async function test_showCreditCardIcons() {
   await SpecialPowers.pushPrefEnv({"set": [["privacy.reduceTimerPrecision", false]]});
   await saveCreditCard(TEST_CREDIT_CARD_1);
   let unknownCard = Object.assign({}, TEST_CREDIT_CARD_3, {"cc-type": "gringotts"});
   await saveCreditCard(unknownCard);
 
   let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL, null, DIALOG_SIZE);
   await waitForFocusAndFormReady(win);
@@ -188,45 +134,51 @@ add_task(async function test_showCreditC
   }
 
   await removeCreditCards([option0.value, option1.value]);
   await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
   is(selRecords.length, 0, "Credit card is removed");
   win.close();
 });
 
+add_task(async function test_hasEditLoginPrompt() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+    return;
+  }
 
-add_task(async function test_hasMasterPassword() {
   await saveCreditCard(TEST_CREDIT_CARD_1);
-  LoginTestUtils.masterPassword.enable();
 
   let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL, null, DIALOG_SIZE);
   await waitForFocusAndFormReady(win);
 
   let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
   let btnRemove = win.document.querySelector(TEST_SELECTORS.btnRemove);
-  let btnShowHideCreditCards = win.document.querySelector(TEST_SELECTORS.btnShowHideCreditCards);
   let btnAdd = win.document.querySelector(TEST_SELECTORS.btnAdd);
-  let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit);
-  let masterPasswordDialogShown = waitForMasterPasswordDialog();
+  // let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit);
 
-  is(btnShowHideCreditCards.hidden, true, "Show credit cards button is hidden");
+  EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
 
-  // Master password dialog should show when trying to edit a credit card record.
-  EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
+  // Login dialog should show when trying to edit a credit card record.
+  // TODO: test disabled because re-auth is not implemented yet (bug 1429265).
+  /*
+  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(); // cancel
   EventUtils.synthesizeMouseAtCenter(btnEdit, {}, win);
-  await masterPasswordDialogShown;
+  await osKeyStoreLoginShown;
+  await new Promise(resolve => waitForFocus(resolve, win));
+  await new Promise(resolve => executeSoon(resolve));
+  */
 
-  // Master password is not required for removing credit cards.
+  // Login is not required for removing credit cards.
   EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win);
   await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved");
   is(selRecords.length, 0, "Credit card is removed");
 
   // gSubDialog.open should be called when trying to add a credit card,
-  // no master password is required.
+  // no OS login dialog is required.
   window.gSubDialog = {
     open: url => is(url, EDIT_CREDIT_CARD_DIALOG_URL, "Edit credit card dialog is called"),
   };
   EventUtils.synthesizeMouseAtCenter(btnAdd, {}, win);
   delete window.gSubDialog;
 
   win.close();
 });
--- a/browser/extensions/formautofill/test/browser/head.js
+++ b/browser/extensions/formautofill/test/browser/head.js
@@ -3,32 +3,31 @@
             TEST_ADDRESS_IE_1,
             TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2, TEST_CREDIT_CARD_3, FORM_URL, CREDITCARD_FORM_URL,
             FTU_PREF, ENABLED_AUTOFILL_ADDRESSES_PREF, AUTOFILL_CREDITCARDS_AVAILABLE_PREF, ENABLED_AUTOFILL_CREDITCARDS_PREF,
             SUPPORTED_COUNTRIES_PREF,
             SYNC_USERNAME_PREF, SYNC_ADDRESSES_PREF, SYNC_CREDITCARDS_PREF, SYNC_CREDITCARDS_AVAILABLE_PREF, CREDITCARDS_USED_STATUS_PREF,
             DEFAULT_REGION_PREF,
             sleep, expectPopupOpen, openPopupOn, expectPopupClose, closePopup, clickDoorhangerButton,
             getAddresses, saveAddress, removeAddresses, saveCreditCard,
-            getDisplayedPopupItems, getDoorhangerCheckbox, waitForMasterPasswordDialog,
+            getDisplayedPopupItems, getDoorhangerCheckbox,
             getNotification, getDoorhangerButton, removeAllRecords, testDialog */
 
 "use strict";
 
-ChromeUtils.import("resource://testing-common/LoginTestUtils.jsm", this);
-ChromeUtils.import("resource://formautofill/MasterPassword.jsm", this);
+ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", this);
+ChromeUtils.import("resource://testing-common/OSKeyStoreTestUtils.jsm", this);
 
 const MANAGE_ADDRESSES_DIALOG_URL = "chrome://formautofill/content/manageAddresses.xhtml";
 const MANAGE_CREDIT_CARDS_DIALOG_URL = "chrome://formautofill/content/manageCreditCards.xhtml";
 const EDIT_ADDRESS_DIALOG_URL = "chrome://formautofill/content/editAddress.xhtml";
 const EDIT_CREDIT_CARD_DIALOG_URL = "chrome://formautofill/content/editCreditCard.xhtml";
 const BASE_URL = "http://mochi.test:8888/browser/browser/extensions/formautofill/test/browser/";
 const FORM_URL = "http://mochi.test:8888/browser/browser/extensions/formautofill/test/browser/autocomplete_basic.html";
-const CREDITCARD_FORM_URL =
-  "https://example.org/browser/browser/extensions/formautofill/test/browser/autocomplete_creditcard_basic.html";
+const CREDITCARD_FORM_URL = "https://example.org/browser/browser/extensions/formautofill/test/browser/autocomplete_creditcard_basic.html";
 const FTU_PREF = "extensions.formautofill.firstTimeUse";
 const CREDITCARDS_USED_STATUS_PREF = "extensions.formautofill.creditCards.used";
 const ENABLED_AUTOFILL_ADDRESSES_PREF = "extensions.formautofill.addresses.enabled";
 const AUTOFILL_CREDITCARDS_AVAILABLE_PREF = "extensions.formautofill.creditCards.available";
 const ENABLED_AUTOFILL_CREDITCARDS_PREF = "extensions.formautofill.creditCards.enabled";
 const SUPPORTED_COUNTRIES_PREF = "extensions.formautofill.supportedCountries";
 const SYNC_USERNAME_PREF = "services.sync.username";
 const SYNC_ADDRESSES_PREF = "services.sync.engine.addresses";
@@ -321,34 +320,16 @@ async function clickDoorhangerButton(but
 function getDoorhangerCheckbox() {
   return getNotification().checkbox;
 }
 
 function getDoorhangerButton(button) {
   return getNotification()[button];
 }
 
-
-// Wait for the master password dialog to popup and enter the password to log in
-// if "login" is "true" or dismiss it directly if otherwise.
-function waitForMasterPasswordDialog(login = false) {
-  info("expecting master password dialog loaded");
-  let dialogShown = TestUtils.topicObserved("common-dialog-loaded");
-  return dialogShown.then(([subject]) => {
-    let dialog = subject.Dialog;
-    is(dialog.args.title, "Password Required", "Master password dialog shown");
-    if (login) {
-      dialog.ui.password1Textbox.value = LoginTestUtils.masterPassword.masterPassword;
-      dialog.ui.button0.click();
-    } else {
-      dialog.ui.button1.click();
-    }
-  });
-}
-
 async function removeAllRecords() {
   let addresses = await getAddresses();
   if (addresses.length) {
     await removeAddresses(addresses.map(address => address.guid));
   }
 
   let creditCards = await getCreditCards();
   if (creditCards.length) {
@@ -361,19 +342,26 @@ async function waitForFocusAndFormReady(
     new Promise(resolve => waitForFocus(resolve, win)),
     BrowserTestUtils.waitForEvent(win, "FormReady"),
   ]);
 }
 
 async function testDialog(url, testFn, arg = undefined) {
   if (url == EDIT_CREDIT_CARD_DIALOG_URL && arg && arg.record) {
     arg.record = Object.assign({}, arg.record, {
-      "cc-number": await MasterPassword.decrypt(arg.record["cc-number-encrypted"]),
+      "cc-number": await OSKeyStore.decrypt(arg.record["cc-number-encrypted"]),
     });
   }
   let win = window.openDialog(url, null, "width=600,height=600", arg);
   await waitForFocusAndFormReady(win);
   let unloadPromise = BrowserTestUtils.waitForEvent(win, "unload");
   await testFn(win);
   return unloadPromise;
 }
 
+add_task(function setup() {
+  OSKeyStoreTestUtils.setup();
+});
+
 registerCleanupFunction(removeAllRecords);
+registerCleanupFunction(async () => {
+  await OSKeyStoreTestUtils.cleanup();
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/fixtures/OSKeyStoreTestUtils.jsm
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+  "OSKeyStoreTestUtils",
+];
+
+ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", this);
+// TODO: Consider AppConstants.MOZILLA_OFFICIAL to decide if we could test re-auth (bug 1429265).
+/*
+ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+*/
+ChromeUtils.import("resource://testing-common/LoginTestUtils.jsm", this);
+ChromeUtils.import("resource://testing-common/TestUtils.jsm");
+
+var OSKeyStoreTestUtils = {
+  /*
+  TEST_ONLY_REAUTH: "extensions.formautofill.osKeyStore.unofficialBuildOnlyLogin",
+  */
+
+  setup() {
+    // TODO: run tests with master password enabled to ensure NSS-implemented
+    // key store prompts on re-auth (bug 1429265)
+    /*
+    LoginTestUtils.masterPassword.enable();
+    */
+
+    this.ORIGINAL_STORE_LABEL = OSKeyStore.STORE_LABEL;
+    OSKeyStore.STORE_LABEL = "test-" + Math.random().toString(36).substr(2);
+  },
+
+  async cleanup() {
+    // TODO: run tests with master password enabled to ensure NSS-implemented
+    // key store prompts on re-auth (bug 1429265)
+    /*
+    LoginTestUtils.masterPassword.disable();
+    */
+
+    await OSKeyStore.cleanup();
+    OSKeyStore.STORE_LABEL = this.ORIGINAL_STORE_LABEL;
+  },
+
+  canTestOSKeyStoreLogin() {
+    // TODO: return true based on whether or not we could test the prompt on
+    // the platform (bug 1429265).
+    /*
+    return OSKeyStore.isNSSKeyStore || !AppConstants.MOZILLA_OFFICIAL;
+    */
+    return true;
+  },
+
+  // Wait for the master password dialog to popup and enter the password to log in
+  // if "login" is "true" or dismiss it directly if otherwise.
+  async waitForOSKeyStoreLogin(login = false) {
+    // TODO: Always resolves for now, because we are skipping re-auth on all
+    // platforms (bug 1429265).
+    /*
+    if (OSKeyStore.isNSSKeyStore) {
+      await this.waitForMasterPasswordDialog(login);
+      return;
+    }
+
+    const str = login ? "pass" : "cancel";
+
+    Services.prefs.setStringPref(this.TEST_ONLY_REAUTH, str);
+
+    await TestUtils.topicObserved("oskeystore-testonly-reauth",
+      (subject, data) => data == str);
+
+    Services.prefs.setStringPref(this.TEST_ONLY_REAUTH, "");
+    */
+  },
+
+  async waitForMasterPasswordDialog(login = false) {
+    let [subject] = await TestUtils.topicObserved("common-dialog-loaded");
+
+    let dialog = subject.Dialog;
+    if (dialog.args.title !== "Password Required") {
+      throw new Error("Incorrect master password dialog title");
+    }
+
+    if (login) {
+      dialog.ui.password1Textbox.value = LoginTestUtils.masterPassword.masterPassword;
+      dialog.ui.button0.click();
+    } else {
+      dialog.ui.button1.click();
+    }
+    await TestUtils.waitForTick();
+  },
+};
--- a/browser/extensions/formautofill/test/mochitest/formautofill_common.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
@@ -1,10 +1,11 @@
 /* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/SimpleTest.js */
 /* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/EventUtils.js */
+/* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/AddTask.js */
 /* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
 /* eslint-disable no-unused-vars */
 
 "use strict";
 
 let formFillChromeScript;
 let defaultTextColor;
 let expectingPopup = null;
@@ -196,16 +197,25 @@ async function cleanUpCreditCards() {
   return invokeAsyncChromeTask("FormAutofillTest:CleanUpCreditCards", "FormAutofillTest:CreditCardsCleanedUp");
 }
 
 async function cleanUpStorage() {
   await cleanUpAddresses();
   await cleanUpCreditCards();
 }
 
+async function canTestOSKeyStoreLogin() {
+  let {canTest} = await invokeAsyncChromeTask("FormAutofillTest:CanTestOSKeyStoreLogin", "FormAutofillTest:CanTestOSKeyStoreLoginResult");
+  return canTest;
+}
+
+async function waitForOSKeyStoreLogin(login = false) {
+  await invokeAsyncChromeTask("FormAutofillTest:OSKeyStoreLogin", "FormAutofillTest:OSKeyStoreLoggedIn", {login});
+}
+
 function patchRecordCCNumber(record) {
   const number = record["cc-number"];
   const ccNumberFmt = {
     affix: "****",
     label: number.substr(-4),
   };
 
   return Object.assign({}, record, {ccNumberFmt});
@@ -258,16 +268,22 @@ function formAutoFillCommonSetup() {
   formFillChromeScript = SpecialPowers.loadChromeScript(chromeURL);
   formFillChromeScript.addMessageListener("onpopupshown", ({results}) => {
     gLastAutoCompleteResults = results;
     if (gPopupShownListener) {
       gPopupShownListener({results});
     }
   });
 
+  add_task(async function setup() {
+    formFillChromeScript.sendAsyncMessage("setup");
+    info(`expecting the storage setup`);
+    await formFillChromeScript.promiseOneMessage("setup-finished");
+  });
+
   SimpleTest.registerCleanupFunction(async () => {
     formFillChromeScript.sendAsyncMessage("cleanup");
     info(`expecting the storage cleanup`);
     await formFillChromeScript.promiseOneMessage("cleanup-finished");
 
     formFillChromeScript.destroy();
     expectingPopup = null;
   });
--- a/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
@@ -1,16 +1,19 @@
 // assert is available to chrome scripts loaded via SpecialPowers.loadChromeScript.
 /* global assert */
 /* eslint-env mozilla/frame-script */
 
 "use strict";
 
+ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://formautofill/FormAutofillUtils.jsm");
+ChromeUtils.import("resource://formautofill/OSKeyStore.jsm");
+ChromeUtils.import("resource://testing-common/OSKeyStoreTestUtils.jsm");
 
 let {formAutofillStorage} = ChromeUtils.import("resource://formautofill/FormAutofillStorage.jsm", {});
 
 const {ADDRESSES_COLLECTION_NAME, CREDITCARDS_COLLECTION_NAME} = FormAutofillUtils;
 
 var ParentUtils = {
   async _getRecords(collectionName) {
     return new Promise(resolve => {
@@ -109,19 +112,25 @@ var ParentUtils = {
     if (guids.length == 0) {
       sendAsyncMessage("FormAutofillTest:CreditCardsCleanedUp");
       return;
     }
 
     await this.operateCreditCard("remove", {guids}, "FormAutofillTest:CreditCardsCleanedUp");
   },
 
+  setup() {
+    OSKeyStoreTestUtils.setup();
+  },
+
   async cleanup() {
     await this.cleanUpAddresses();
     await this.cleanUpCreditCards();
+    await OSKeyStoreTestUtils.cleanup();
+
     Services.obs.removeObserver(this, "formautofill-storage-changed");
   },
 
   _areRecordsMatching(recordA, recordB, collectionName) {
     for (let field of formAutofillStorage[collectionName].VALID_FIELDS) {
       if (recordA[field] !== recordB[field]) {
         return false;
       }
@@ -210,13 +219,28 @@ addMessageListener("FormAutofillTest:Rem
 addMessageListener("FormAutofillTest:CheckCreditCards", (msg) => {
   ParentUtils.checkCreditCards(msg);
 });
 
 addMessageListener("FormAutofillTest:CleanUpCreditCards", (msg) => {
   ParentUtils.cleanUpCreditCards();
 });
 
-addMessageListener("cleanup", () => {
-  ParentUtils.cleanup().then(() => {
-    sendAsyncMessage("cleanup-finished", {});
-  });
+addMessageListener("FormAutofillTest:CanTestOSKeyStoreLogin", (msg) => {
+  sendAsyncMessage("FormAutofillTest:CanTestOSKeyStoreLoginResult",
+    {canTest: OSKeyStore.isNSSKeyStore || !AppConstants.MOZILLA_OFFICIAL});
+});
+
+addMessageListener("FormAutofillTest:OSKeyStoreLogin", async (msg) => {
+  await OSKeyStoreTestUtils.waitForOSKeyStoreLogin(msg.login);
+  sendAsyncMessage("FormAutofillTest:OSKeyStoreLoggedIn");
 });
+
+addMessageListener("setup", async () => {
+  ParentUtils.setup();
+  sendAsyncMessage("setup-finished", {});
+});
+
+addMessageListener("cleanup", async () => {
+  await ParentUtils.cleanup();
+
+  sendAsyncMessage("cleanup-finished", {});
+});
--- a/browser/extensions/formautofill/test/mochitest/test_basic_creditcard_autocomplete_form.html
+++ b/browser/extensions/formautofill/test/mochitest/test_basic_creditcard_autocomplete_form.html
@@ -145,41 +145,60 @@ add_task(async function check_search_res
   await setInput("#cc-name", "");
   synthesizeKey("KEY_ArrowDown");
   await expectPopup();
   checkMenuEntries(["John Smith"], false);
 
   await SpecialPowers.popPrefEnv();
 });
 
+let canTest;
+
 // Autofill the credit card from dropdown menu.
 add_task(async function check_fields_after_form_autofill() {
+  canTest = await canTestOSKeyStoreLogin();
+  if (!canTest) {
+    todo(canTest, "Cannot test OS key store login on official builds.");
+    return;
+  }
+
   await setInput("#cc-exp-year", 202);
 
   synthesizeKey("KEY_ArrowDown");
   await expectPopup();
   checkMenuEntries(MOCK_STORAGE.slice(1).map(patchRecordCCNumber).map(cc => JSON.stringify({
     primary: cc["cc-exp-year"],
     secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
   })));
 
   synthesizeKey("KEY_ArrowDown");
+  let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+  await new Promise(resolve => SimpleTest.executeSoon(resolve));
   await triggerAutofillAndCheckProfile(MOCK_STORAGE[1]);
+  await osKeyStoreLoginShown;
 });
 
 // Fallback to history search after autofill address.
 add_task(async function check_fallback_after_form_autofill() {
+  if (!canTest) {
+    return;
+  }
+
   await setInput("#cc-name", "", true);
   synthesizeKey("KEY_ArrowDown");
   await expectPopup();
   checkMenuEntries(["John Smith"], false);
 });
 
 // Resume form autofill once all the autofilled fileds are changed.
 add_task(async function check_form_autofill_resume() {
+  if (!canTest) {
+    return;
+  }
+
   document.querySelector("#cc-name").blur();
   document.querySelector("#form1").reset();
 
   await setInput("#cc-name", "");
   synthesizeKey("KEY_ArrowDown");
   await expectPopup();
   checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
     primary: cc["cc-name"],
--- a/browser/extensions/formautofill/test/mochitest/test_clear_form.html
+++ b/browser/extensions/formautofill/test/mochitest/test_clear_form.html
@@ -98,19 +98,26 @@ add_task(async function clear_modified_f
   await setInput("#tel", "+1111111111", true);
 
   await triggerPopupAndHoverItem("#street-address", 0);
   await confirmClear("#street-address");
   checkIsFormCleared({tel: "+1111111111"});
 });
 
 add_task(async function clear_distinct_section() {
+  if (!(await canTestOSKeyStoreLogin())) {
+    todo(false, "Cannot test OS key store login on official builds.");
+    return;
+  }
+
   document.getElementById("form1").reset();
   await triggerPopupAndHoverItem("#cc-name", 0);
+  let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
   await triggerAutofillAndCheckProfile(MOCK_CC_STORAGE[0]);
+  await osKeyStoreLoginShown;
 
   await triggerPopupAndHoverItem("#organization", 0);
   await triggerAutofillAndCheckProfile(MOCK_ADDR_STORAGE[0]);
   await triggerPopupAndHoverItem("#street-address", 0);
   await confirmClear("#street-address");
 
   for (const [id, val] of Object.entries(MOCK_CC_STORAGE[0])) {
     const element = document.getElementById(id);
--- a/browser/extensions/formautofill/test/unit/head.js
+++ b/browser/extensions/formautofill/test/unit/head.js
@@ -94,17 +94,17 @@ async function initProfileStorage(fileNa
 
   let onChanged = TestUtils.topicObserved(
     "formautofill-storage-changed",
     (subject, data) =>
       data == "add" &&
       subject.wrappedJSObject.collectionName == collectionName
   );
   for (let record of records) {
-    Assert.ok(profileStorage[collectionName].add(record));
+    Assert.ok(await profileStorage[collectionName].add(record));
     await onChanged;
   }
   await profileStorage._saveImmediately();
   return profileStorage;
 }
 
 function verifySectionFieldDetails(sections, expectedResults) {
   Assert.equal(sections.length, expectedResults.length, "Expected section count.");
@@ -218,8 +218,18 @@ add_task(async function head_initialize(
     Services.prefs.clearUserPref("extensions.formautofill.creditCards.available");
     Services.prefs.clearUserPref("extensions.formautofill.heuristics.enabled");
     Services.prefs.clearUserPref("extensions.formautofill.section.enabled");
     Services.prefs.clearUserPref("dom.forms.autocomplete.formautofill");
   });
 
   await loadExtension();
 });
+
+let OSKeyStoreTestUtils;
+add_task(async function os_key_store_setup() {
+  ({OSKeyStoreTestUtils} =
+    ChromeUtils.import("resource://testing-common/OSKeyStoreTestUtils.jsm", {}));
+  OSKeyStoreTestUtils.setup();
+  registerCleanupFunction(async function cleanup() {
+    await OSKeyStoreTestUtils.cleanup();
+  });
+});
--- a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
@@ -1,19 +1,18 @@
 /*
  * Test for form auto fill content helper fill all inputs function.
  */
 /* eslint-disable mozilla/no-arbitrary-setTimeout */
 
 "use strict";
 
-let MasterPassword;
 add_task(async function setup() {
   ChromeUtils.import("resource://formautofill/FormAutofillHandler.jsm");
-  ({MasterPassword} = ChromeUtils.import("resource://formautofill/MasterPassword.jsm", {}));
+  ChromeUtils.import("resource://formautofill/OSKeyStore.jsm");
 });
 
 const TESTCASES = [
   {
     description: "Form without autocomplete property",
     document: `<form><input id="given-name"><input id="family-name">
                <input id="street-addr"><input id="city"><select id="country"></select>
                <input id='email'><input id="tel"></form>`,
@@ -472,38 +471,31 @@ const TESTCASES_FILL_SELECT = [
 function do_test(testcases, testFn) {
   for (let tc of testcases) {
     (function() {
       let testcase = tc;
       add_task(async function() {
         info("Starting testcase: " + testcase.description);
         let ccNumber = testcase.profileData["cc-number"];
         if (ccNumber) {
-          testcase.profileData["cc-number-encrypted"] = await MasterPassword.encrypt(ccNumber);
+          testcase.profileData["cc-number-encrypted"] = await OSKeyStore.encrypt(ccNumber);
           delete testcase.profileData["cc-number"];
         }
 
         let doc = MockDocument.createTestDocument("http://localhost:8080/test/",
                                                   testcase.document);
         let form = doc.querySelector("form");
         let formLike = FormLikeFactory.createFromForm(form);
         let handler = new FormAutofillHandler(formLike);
         let promises = [];
-        // Replace the interal decrypt method with MasterPassword API
+        // Replace the internal decrypt method with OSKeyStore API,
+        // but don't pass the reauth parameter to avoid triggering
+        // reauth login dialog in these tests.
         let decryptHelper = async (cipherText, reauth) => {
-          let string;
-          try {
-            string = await MasterPassword.decrypt(cipherText, reauth);
-          } catch (e) {
-            if (e.result != Cr.NS_ERROR_ABORT) {
-              throw e;
-            }
-            info("User canceled master password entry");
-          }
-          return string;
+          return OSKeyStore.decrypt(cipherText, false);
         };
 
         handler.collectFormFields();
 
         let focusedInput = doc.getElementById(testcase.focusedInputId);
         handler.focusedInput = focusedInput;
 
         for (let section of handler.sections) {
--- a/browser/extensions/formautofill/test/unit/test_creditCardRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_creditCardRecords.js
@@ -311,17 +311,17 @@ add_task(async function test_add() {
   let creditCards = await profileStorage.creditCards.getAll();
 
   Assert.equal(creditCards.length, 2);
 
   do_check_credit_card_matches(creditCards[0], TEST_CREDIT_CARD_1);
   do_check_credit_card_matches(creditCards[1], TEST_CREDIT_CARD_2);
 
   Assert.notEqual(creditCards[0].guid, undefined);
-  Assert.equal(creditCards[0].version, 1);
+  Assert.equal(creditCards[0].version, 2);
   Assert.notEqual(creditCards[0].timeCreated, undefined);
   Assert.equal(creditCards[0].timeLastModified, creditCards[0].timeCreated);
   Assert.equal(creditCards[0].timeLastUsed, 0);
   Assert.equal(creditCards[0].timesUsed, 0);
 
   // Empty string should be deleted before saving.
   await profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_EMPTY_FIELD);
   let creditCard = profileStorage.creditCards._data[2];
@@ -417,16 +417,24 @@ add_task(async function test_update() {
   creditCard = profileStorage.creditCards._data[0];
   Assert.equal(creditCard["cc-number"],
     CreditCard.getLongMaskedNumber(TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"]));
   await profileStorage.creditCards.update(profileStorage.creditCards._data[1].guid, TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD, true);
   creditCard = profileStorage.creditCards._data[1];
   Assert.equal(creditCard["cc-number"],
     CreditCard.getLongMaskedNumber(TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"]));
 
+  // Decryption failure of existing record should not prevent it from being updated.
+  creditCard = profileStorage.creditCards._data[0];
+  creditCard["cc-number-encrypted"] = "INVALID";
+  await profileStorage.creditCards.update(profileStorage.creditCards._data[0].guid, TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD, false);
+  creditCard = profileStorage.creditCards._data[0];
+  Assert.equal(creditCard["cc-number"],
+    CreditCard.getLongMaskedNumber(TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"]));
+
   await Assert.rejects(profileStorage.creditCards.update("INVALID_GUID", TEST_CREDIT_CARD_3),
     /No matching record\./
   );
 
   await Assert.rejects(profileStorage.creditCards.update(guid, TEST_CREDIT_CARD_WITH_INVALID_FIELD),
     /"invalidField" is not a valid field\./
   );
 
@@ -650,23 +658,17 @@ add_task(async function test_getDuplicat
   Assert.equal(await profileStorage.creditCards.getDuplicateGuid(record), null);
 
   // Numbers with the same last 4 digits shouldn't be treated as a duplicate.
   record = Object.assign({}, TEST_CREDIT_CARD_3);
   let last4Digits = record["cc-number"].substr(-4);
   // This number differs from TEST_CREDIT_CARD_3 by swapping the order of the
   // 09 and 90 adjacent digits, which is still a valid credit card number.
   record["cc-number"] = "358999378390" + last4Digits;
-  Assert.equal(await profileStorage.creditCards.getDuplicateGuid(record), null);
 
-  // ... However, we treat numbers with the same last 4 digits as a duplicate if
-  // the master password is enabled.
-  let tokendb = Cc["@mozilla.org/security/pk11tokendb;1"].createInstance(Ci.nsIPK11TokenDB);
-  let token = tokendb.getInternalKeyToken();
-  token.reset();
-  token.initPassword("password");
+  // We treat numbers with the same last 4 digits as a duplicate.
   Assert.equal(await profileStorage.creditCards.getDuplicateGuid(record), guid);
 
-  // ... Even though the master password is enabled and the last 4 digits are the
-  // same, an invalid credit card number should never be treated as a duplicate.
+  // Even though the last 4 digits are the same, an invalid credit card number
+  // should never be treated as a duplicate.
   record["cc-number"] = "************" + last4Digits;
   Assert.equal(await profileStorage.creditCards.getDuplicateGuid(record), null);
 });
--- a/browser/extensions/formautofill/test/unit/test_getRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_getRecords.js
@@ -2,19 +2,20 @@
  * Test for make sure getRecords can retrieve right collection from storage.
  */
 
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/CreditCard.jsm");
 
 let FormAutofillParent;
+let OSKeyStore;
 add_task(async function setup() {
   ({FormAutofillParent} = ChromeUtils.import("resource://formautofill/FormAutofillParent.jsm", {}));
-  ChromeUtils.import("resource://formautofill/MasterPassword.jsm");
+  ({OSKeyStore} = ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", {}));
 });
 
 const TEST_ADDRESS_1 = {
   "given-name": "Timothy",
   "additional-name": "John",
   "family-name": "Berners-Lee",
   organization: "World Wide Web Consortium",
   "street-address": "32 Vassar Street\nMIT Room 32-G524",
@@ -171,84 +172,63 @@ add_task(async function test_getRecords_
   let formAutofillParent = new FormAutofillParent();
 
   await formAutofillParent.init();
   await formAutofillParent.formAutofillStorage.initialize();
   let collection = formAutofillParent.formAutofillStorage.creditCards;
   let encryptedCCRecords = await Promise.all([TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2].map(async record => {
     let clonedRecord = Object.assign({}, record);
     clonedRecord["cc-number"] = CreditCard.getLongMaskedNumber(record["cc-number"]);
-    clonedRecord["cc-number-encrypted"] = await MasterPassword.encrypt(record["cc-number"]);
+    clonedRecord["cc-number-encrypted"] = await OSKeyStore.encrypt(record["cc-number"]);
     return clonedRecord;
   }));
   sinon.stub(collection, "getAll", () =>
     Promise.resolve([Object.assign({}, encryptedCCRecords[0]), Object.assign({}, encryptedCCRecords[1])]));
-  let CreditCardsWithDecryptedNumber = [
-    Object.assign({}, encryptedCCRecords[0], {"cc-number-decrypted": TEST_CREDIT_CARD_1["cc-number"]}),
-    Object.assign({}, encryptedCCRecords[1], {"cc-number-decrypted": TEST_CREDIT_CARD_2["cc-number"]}),
-  ];
 
   let testCases = [
     {
-      description: "If the search string could match 1 creditCard (without masterpassword)",
-      filter: {
-        collectionName: "creditCards",
-        info: {fieldName: "cc-name"},
-        searchString: "John Doe",
-      },
-      expectedResult: CreditCardsWithDecryptedNumber.slice(0, 1),
-    },
-    {
-      description: "If the search string could match multiple creditCards (without masterpassword)",
+      description: "If the search string could match multiple creditCards",
       filter: {
         collectionName: "creditCards",
         info: {fieldName: "cc-name"},
         searchString: "John",
       },
-      expectedResult: CreditCardsWithDecryptedNumber,
+      expectedResult: encryptedCCRecords,
     },
     {
-      description: "If the search string could not match any creditCard (without masterpassword)",
+      description: "If the search string could not match any creditCard",
       filter: {
         collectionName: "creditCards",
         info: {fieldName: "cc-name"},
         searchString: "T",
       },
       expectedResult: [],
     },
     {
-      description: "If the search number string could match 1 creditCard (without masterpassword)",
-      filter: {
-        collectionName: "creditCards",
-        info: {fieldName: "cc-number"},
-        searchString: "411",
-      },
-      expectedResult: CreditCardsWithDecryptedNumber.slice(0, 1),
-    },
-    {
-      description: "If the search string could match multiple creditCards (without masterpassword)",
+      description: "Return all creditCards if focused field is cc number; " +
+        "if the search string could match multiple creditCards",
       filter: {
         collectionName: "creditCards",
         info: {fieldName: "cc-number"},
         searchString: "4",
       },
-      expectedResult: CreditCardsWithDecryptedNumber,
+      expectedResult: encryptedCCRecords,
     },
     {
-      description: "If the search string could match 1 creditCard (with masterpassword)",
+      description: "If the search string could match 1 creditCard",
       filter: {
         collectionName: "creditCards",
         info: {fieldName: "cc-name"},
         searchString: "John Doe",
       },
       mpEnabled: true,
       expectedResult: encryptedCCRecords.slice(0, 1),
     },
     {
-      description: "Return all creditCards if focused field is cc number (with masterpassword)",
+      description: "Return all creditCards if focused field is cc number",
       filter: {
         collectionName: "creditCards",
         info: {fieldName: "cc-number"},
         searchString: "411",
       },
       mpEnabled: true,
       expectedResult: encryptedCCRecords,
     },
deleted file mode 100644
--- a/browser/extensions/formautofill/test/unit/test_masterPassword.js
+++ /dev/null
@@ -1,119 +0,0 @@
-/**
- * Tests of MasterPassword.jsm
- */
-
-"use strict";
-const {MockRegistrar} =
-  ChromeUtils.import("resource://testing-common/MockRegistrar.jsm", {});
-
-let MasterPassword;
-add_task(async function setup() {
-  ({MasterPassword} = ChromeUtils.import("resource://formautofill/MasterPassword.jsm", {}));
-});
-
-const TESTCASES = [{
-  description: "With master password set",
-  masterPassword: "fakemp",
-  mpEnabled: true,
-},
-{
-  description: "Without master password set",
-  masterPassword: "", // "" means no master password
-  mpEnabled: false,
-}];
-
-
-// Tests that PSM can successfully ask for a password from the user and relay it
-// back to NSS. Does so by mocking out the actual dialog and "filling in" the
-// password. Also tests that providing an incorrect password will fail (well,
-// technically the user will just get prompted again, but if they then cancel
-// the dialog the overall operation will fail).
-
-let gMockPrompter = {
-  passwordToTry: null,
-  numPrompts: 0,
-
-  // This intentionally does not use arrow function syntax to avoid an issue
-  // where in the context of the arrow function, |this != gMockPrompter| due to
-  // how objects get wrapped when going across xpcom boundaries.
-  promptPassword(dialogTitle, text, password, checkMsg, checkValue) {
-    this.numPrompts++;
-    if (this.numPrompts > 1) { // don't keep retrying a bad password
-      return false;
-    }
-    equal(text,
-          "Please enter your master password.",
-          "password prompt text should be as expected");
-    equal(checkMsg, null, "checkMsg should be null");
-    ok(this.passwordToTry, "passwordToTry should be non-null");
-    password.value = this.passwordToTry;
-    return true;
-  },
-
-  QueryInterface: ChromeUtils.generateQI([Ci.nsIPrompt]),
-};
-
-// Mock nsIWindowWatcher. PSM calls getNewPrompter on this to get an nsIPrompt
-// to call promptPassword. We return the mock one, above.
-let gWindowWatcher = {
-  getNewPrompter: () => gMockPrompter,
-  QueryInterface: ChromeUtils.generateQI([Ci.nsIWindowWatcher]),
-};
-
-// Ensure that the appropriate initialization has happened.
-do_get_profile();
-
-let windowWatcherCID =
-  MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1",
-                         gWindowWatcher);
-registerCleanupFunction(() => {
-  MockRegistrar.unregister(windowWatcherCID);
-});
-
-TESTCASES.forEach(testcase => {
-  add_task(async function test_encrypt_decrypt() {
-    info("Starting testcase: " + testcase.description);
-
-    let token = MasterPassword._token;
-    token.initPassword(testcase.masterPassword);
-
-    // Test only: Force the token login without asking for master password
-    token.login(/* force */ false);
-    Assert.equal(testcase.mpEnabled, token.isLoggedIn(), "Token should now be logged into");
-    Assert.equal(MasterPassword.isEnabled, testcase.mpEnabled);
-
-    let testText = "test string";
-    let cipherText = await MasterPassword.encrypt(testText);
-    Assert.notEqual(testText, cipherText);
-    let plainText = await MasterPassword.decrypt(cipherText);
-    Assert.equal(testText, plainText);
-    if (token.isLoggedIn()) {
-      // Reset state.
-      gMockPrompter.numPrompts = 0;
-      token.logoutSimple();
-
-      ok(!token.isLoggedIn(),
-         "Token should be logged out after calling logoutSimple()");
-
-      // Try with the correct password.
-      gMockPrompter.passwordToTry = testcase.masterPassword;
-      await MasterPassword.encrypt(testText);
-      Assert.equal(gMockPrompter.numPrompts, 1, "should have prompted for encryption");
-
-      // Reset state.
-      gMockPrompter.numPrompts = 0;
-      token.logoutSimple();
-
-      try {
-        // Try with the incorrect password.
-        gMockPrompter.passwordToTry = "XXX";
-        await MasterPassword.decrypt(cipherText);
-        throw new Error("Not receiving canceled master password error");
-      } catch (e) {
-        Assert.equal(e.message, "User canceled master password entry");
-      }
-    }
-
-    token.reset();
-  });
-});
--- a/browser/extensions/formautofill/test/unit/test_migrateRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_migrateRecords.js
@@ -1,24 +1,27 @@
 /**
  * Tests the migration algorithm in profileStorage.
  */
 
 "use strict";
 
+ChromeUtils.import("resource://testing-common/LoginTestUtils.jsm", this);
+
 let FormAutofillStorage;
-
+let OSKeyStore;
 add_task(async function setup() {
   ({FormAutofillStorage} = ChromeUtils.import("resource://formautofill/FormAutofillStorage.jsm", {}));
+  ({OSKeyStore} = ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", {}));
 });
 
 const TEST_STORE_FILE_NAME = "test-profile.json";
 
 const ADDRESS_SCHEMA_VERSION = 1;
-const CREDIT_CARD_SCHEMA_VERSION = 1;
+const CREDIT_CARD_SCHEMA_VERSION = 2;
 
 const ADDRESS_TESTCASES = [
   {
     description: "The record version is equal to the current version. The migration shouldn't be invoked.",
     record: {
       guid: "test-guid",
       version: ADDRESS_SCHEMA_VERSION,
       "given-name": "Timothy",
@@ -241,27 +244,98 @@ let do_check_record_matches = (expectedR
 };
 
 add_task(async function test_migrateAddressRecords() {
   let path = getTempFile(TEST_STORE_FILE_NAME).path;
 
   let profileStorage = new FormAutofillStorage(path);
   await profileStorage.initialize();
 
-  await Promise.all(ADDRESS_TESTCASES.map(async testcase => {
+  for (let testcase of ADDRESS_TESTCASES) {
     info(testcase.description);
-    await profileStorage.addresses._migrateRecord(testcase.record);
-    do_check_record_matches(testcase.expectedResult, testcase.record);
-  }));
+    profileStorage._store.data.addresses = [testcase.record];
+    await profileStorage.addresses._migrateRecord(testcase.record, 0);
+    do_check_record_matches(testcase.expectedResult, profileStorage.addresses._data[0]);
+  }
 });
 
 add_task(async function test_migrateCreditCardRecords() {
   let path = getTempFile(TEST_STORE_FILE_NAME).path;
 
   let profileStorage = new FormAutofillStorage(path);
   await profileStorage.initialize();
 
-  await Promise.all(CREDIT_CARD_TESTCASES.map(async testcase => {
+  for (let testcase of CREDIT_CARD_TESTCASES) {
     info(testcase.description);
-    await profileStorage.creditCards._migrateRecord(testcase.record);
-    do_check_record_matches(testcase.expectedResult, testcase.record);
-  }));
+    profileStorage._store.data.creditCards = [testcase.record];
+    await profileStorage.creditCards._migrateRecord(testcase.record, 0);
+    do_check_record_matches(testcase.expectedResult, profileStorage.creditCards._data[0]);
+  }
 });
+
+add_task(async function test_migrateEncryptedCreditCardNumber() {
+  let path = getTempFile(TEST_STORE_FILE_NAME).path;
+
+  let profileStorage = new FormAutofillStorage(path);
+  await profileStorage.initialize();
+
+  const ccNumber = "4111111111111111";
+  let cryptoSDR = Cc["@mozilla.org/login-manager/crypto/SDR;1"]
+    .createInstance(Ci.nsILoginManagerCrypto);
+
+  info("Encrypted credit card should be migrated from v1 to v2");
+
+  let record = {
+    guid: "test-guid",
+    version: 1,
+    "cc-name": "Timothy",
+    "cc-number-encrypted": cryptoSDR.encrypt(ccNumber),
+  };
+
+  let expectedRecord = {
+    guid: "test-guid",
+    version: CREDIT_CARD_SCHEMA_VERSION,
+    "cc-name": "Timothy",
+    "cc-given-name": "Timothy",
+  };
+  profileStorage._store.data.creditCards = [record];
+  await profileStorage.creditCards._migrateRecord(record, 0);
+  record = profileStorage.creditCards._data[0];
+
+  Assert.equal(expectedRecord.guid, record.guid);
+  Assert.equal(expectedRecord.version, record.version);
+  Assert.equal(expectedRecord["cc-name"], record["cc-name"]);
+  Assert.equal(expectedRecord["cc-given-name"], record["cc-given-name"]);
+
+  // Ciphertext of OS Key Store is not stable, must compare decrypted text here.
+  Assert.equal(ccNumber, await OSKeyStore.decrypt(record["cc-number-encrypted"]));
+});
+
+add_task(async function test_migrateEncryptedCreditCardNumberWithMP() {
+  LoginTestUtils.masterPassword.enable();
+
+  let path = getTempFile(TEST_STORE_FILE_NAME).path;
+
+  let profileStorage = new FormAutofillStorage(path);
+  await profileStorage.initialize();
+
+  info("Encrypted credit card should be migrated a tombstone if MP is enabled");
+
+  let record = {
+    guid: "test-guid",
+    version: 1,
+    "cc-name": "Timothy",
+    "cc-number-encrypted": "(encrypted to be discarded)",
+  };
+
+  profileStorage._store.data.creditCards = [record];
+  await profileStorage.creditCards._migrateRecord(record, 0);
+  record = profileStorage.creditCards._data[0];
+
+  Assert.equal(record.guid, "test-guid");
+  Assert.equal(record.deleted, true);
+  Assert.equal(typeof record.version, "undefined");
+  Assert.equal(typeof record["cc-name"], "undefined");
+  Assert.equal(typeof record["cc-number-encrypted"], "undefined");
+
+  LoginTestUtils.masterPassword.disable();
+});
+
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_osKeyStore.js
@@ -0,0 +1,144 @@
+/**
+ * Tests of OSKeyStore.jsm
+ */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+ChromeUtils.import("resource://testing-common/MockRegistrar.jsm");
+
+let OSKeyStore;
+add_task(async function setup() {
+  ({OSKeyStore} = ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", {}));
+});
+
+// Ensure that the appropriate initialization has happened.
+do_get_profile();
+
+// For NSS key store, mocking out the dialog and control it from here.
+let gMockPrompter = {
+  passwordToTry: "hunter2",
+  resolve: null,
+  login: undefined,
+
+  // This intentionally does not use arrow function syntax to avoid an issue
+  // where in the context of the arrow function, |this != gMockPrompter| due to
+  // how objects get wrapped when going across xpcom boundaries.
+  promptPassword(dialogTitle, text, password, checkMsg, checkValue) {
+    equal(text,
+          "Please enter your master password.",
+          "password prompt text should be as expected");
+    equal(checkMsg, null, "checkMsg should be null");
+    if (this.login) {
+      password.value = this.passwordToTry;
+    }
+    this.resolve();
+    this.resolve = null;
+
+    return this.login;
+  },
+
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIPrompt]),
+};
+
+// Mock nsIWindowWatcher. PSM calls getNewPrompter on this to get an nsIPrompt
+// to call promptPassword. We return the mock one, above.
+let gWindowWatcher = {
+  getNewPrompter: () => gMockPrompter,
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIWindowWatcher]),
+};
+
+let nssToken;
+
+const TEST_ONLY_REAUTH = "extensions.formautofill.osKeyStore.unofficialBuildOnlyLogin";
+
+async function waitForReauth(login = false) {
+  if (OSKeyStore.isNSSKeyStore) {
+    gMockPrompter.login = login;
+    await new Promise(resolve => { gMockPrompter.resolve = resolve; });
+
+    return;
+  }
+
+  let value = login ? "pass" : "cancel";
+  Services.prefs.setStringPref(TEST_ONLY_REAUTH, value);
+  await TestUtils.topicObserved("oskeystore-testonly-reauth",
+    (subject, data) => data == value);
+}
+
+const testText = "test string";
+let cipherText;
+
+add_task(async function test_encrypt_decrypt() {
+  Assert.equal(await OSKeyStore.ensureLoggedIn(), true, "Started logged in.");
+
+  cipherText = await OSKeyStore.encrypt(testText);
+  Assert.notEqual(testText, cipherText);
+
+  let plainText = await OSKeyStore.decrypt(cipherText);
+  Assert.equal(testText, plainText);
+});
+
+// TODO: skipped because re-auth is not implemented (bug 1429265).
+add_task(async function test_reauth() {
+  let canTest = OSKeyStore.isNSSKeyStore || !AppConstants.MOZILLA_OFFICIAL;
+  if (!canTest) {
+    todo_check_false(canTest,
+      "test_reauth: Cannot test OS key store login on official builds.");
+    return;
+  }
+
+  if (OSKeyStore.isNSSKeyStore) {
+    let windowWatcherCID;
+    windowWatcherCID =
+      MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1",
+                             gWindowWatcher);
+    registerCleanupFunction(() => {
+      MockRegistrar.unregister(windowWatcherCID);
+    });
+
+    // If we use the NSS key store implementation test that everything works
+    // when a master password is set.
+    // Set an initial password.
+    let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"]
+                    .getService(Ci.nsIPK11TokenDB);
+    nssToken = tokenDB.getInternalKeyToken();
+    nssToken.initPassword("hunter2");
+  }
+
+  let reauthObserved = waitForReauth(false);
+  await new Promise(resolve => TestUtils.executeSoon(resolve));
+  try {
+    await OSKeyStore.decrypt(cipherText, true);
+    throw new Error("Not receiving canceled OS unlock error");
+  } catch (ex) {
+    Assert.equal(ex.message, "User canceled OS unlock entry");
+    Assert.equal(ex.result, Cr.NS_ERROR_ABORT);
+  }
+  await reauthObserved;
+
+  reauthObserved = waitForReauth(false);
+  await new Promise(resolve => TestUtils.executeSoon(resolve));
+  Assert.equal(await OSKeyStore.ensureLoggedIn(true), false, "Reauth cancelled.");
+  await reauthObserved;
+
+  reauthObserved = waitForReauth(true);
+  await new Promise(resolve => TestUtils.executeSoon(resolve));
+  let plainText2 = await OSKeyStore.decrypt(cipherText, true);
+  await reauthObserved;
+  Assert.equal(testText, plainText2);
+
+  reauthObserved = waitForReauth(true);
+  await new Promise(resolve => TestUtils.executeSoon(resolve));
+  Assert.equal(await OSKeyStore.ensureLoggedIn(true), true, "Reauth logged in.");
+  await reauthObserved;
+}).skip();
+
+add_task(async function test_decryption_failure() {
+  try {
+    await OSKeyStore.decrypt("Malformed cipher text");
+    throw new Error("Not receiving decryption error");
+  } catch (ex) {
+    Assert.notEqual(ex.result, Cr.NS_ERROR_ABORT);
+  }
+});
--- a/browser/extensions/formautofill/test/unit/test_reconcile.js
+++ b/browser/extensions/formautofill/test/unit/test_reconcile.js
@@ -465,220 +465,220 @@ const ADDRESS_RECONCILE_TESTCASES = [
 ];
 
 const CREDIT_CARD_RECONCILE_TESTCASES = [
   {
     description: "Local change",
     parent: {
       // So when we last wrote the record to the server, it had these values.
       "guid": "2bbd2d8fbc6b",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       // The current local record - by comparing against parent we can see that
       // only the cc-number has changed locally.
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     }],
     remote: {
       // This is the incoming record. It has the same values as "parent", so
       // we can deduce the record hasn't actually been changed remotely so we
       // can safely ignore the incoming record and write our local changes.
       "guid": "2bbd2d8fbc6b",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     reconciled: {
       "guid": "2bbd2d8fbc6b",
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
   },
   {
     description: "Remote change",
     parent: {
       "guid": "e3680e9f890d",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     }],
     remote: {
       "guid": "e3680e9f890d",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
     reconciled: {
       "guid": "e3680e9f890d",
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
   },
 
   {
     description: "New local field",
     parent: {
       "guid": "0cba738b1be0",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     }],
     remote: {
       "guid": "0cba738b1be0",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     reconciled: {
       "guid": "0cba738b1be0",
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
   },
   {
     description: "New remote field",
     parent: {
       "guid": "be3ef97f8285",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     }],
     remote: {
       "guid": "be3ef97f8285",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     reconciled: {
       "guid": "be3ef97f8285",
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
   },
   {
     description: "Deleted field locally",
     parent: {
       "guid": "9627322248ec",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     }],
     remote: {
       "guid": "9627322248ec",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     reconciled: {
       "guid": "9627322248ec",
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
   },
   {
     description: "Deleted field remotely",
     parent: {
       "guid": "7d7509f3eeb2",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     }],
     remote: {
       "guid": "7d7509f3eeb2",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     reconciled: {
       "guid": "7d7509f3eeb2",
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
   },
   {
     description: "Local and remote changes to unrelated fields",
     parent: {
       // The last time we wrote this to the server, "cc-exp-month" was 12.
       "guid": "e087a06dfc57",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       // The current local record - so locally we've changed "cc-number".
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
       "cc-exp-month": 12,
     }],
     remote: {
       // Remotely, we've changed "cc-exp-month" to 1.
       "guid": "e087a06dfc57",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 1,
     },
     reconciled: {
       "guid": "e087a06dfc57",
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
       "cc-exp-month": 1,
     },
   },
   {
     description: "Multiple local changes",
     parent: {
       "guid": "340a078c596f",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "Skip",
       "cc-number": "4111111111111111",
     }, {
       "cc-name": "Skip",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     }],
     remote: {
       "guid": "340a078c596f",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-year": 2000,
     },
     reconciled: {
       "guid": "340a078c596f",
       "cc-name": "Skip",
       "cc-number": "4111111111111111",
@@ -687,54 +687,54 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
     },
   },
   {
     // Local and remote diverged from the shared parent, but the values are the
     // same, so we shouldn't fork.
     description: "Same change to local and remote",
     parent: {
       "guid": "0b3a72a1bea2",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     }],
     remote: {
       "guid": "0b3a72a1bea2",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
     reconciled: {
       "guid": "0b3a72a1bea2",
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
   },
   {
     description: "Conflicting changes to single field",
     parent: {
       // This is what we last wrote to the sync server.
       "guid": "62068784d089",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       // The current version of the local record - the cc-number has changed locally.
       "cc-name": "John Doe",
       "cc-number": "5103059495477870",
     }],
     remote: {
       // An incoming record has a different cc-number than any of the above!
       "guid": "62068784d089",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
     forked: {
       // So we've forked the local record to a new GUID (and the next sync is
       // going to write this as a new record)
       "cc-name": "John Doe",
       "cc-number": "5103059495477870",
@@ -745,29 +745,29 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
   },
   {
     description: "Conflicting changes to multiple fields",
     parent: {
       "guid": "244dbb692e94",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "5103059495477870",
       "cc-exp-month": 1,
     }],
     remote: {
       "guid": "244dbb692e94",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
       "cc-exp-month": 3,
     },
     forked: {
       "cc-name": "John Doe",
       "cc-number": "5103059495477870",
       "cc-exp-month": 1,
@@ -778,28 +778,28 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
       "cc-number": "4929001587121045",
       "cc-exp-month": 3,
     },
   },
   {
     description: "Field deleted locally, changed remotely",
     parent: {
       "guid": "6fc45e03d19a",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     }],
     remote: {
       "guid": "6fc45e03d19a",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 3,
     },
     forked: {
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
@@ -809,29 +809,29 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
       "cc-number": "4111111111111111",
       "cc-exp-month": 3,
     },
   },
   {
     description: "Field changed locally, deleted remotely",
     parent: {
       "guid": "fff9fa27fa18",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 3,
     }],
     remote: {
       "guid": "fff9fa27fa18",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     forked: {
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 3,
     },
@@ -843,28 +843,28 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
   },
   {
     // Created, last modified should be synced; last used and times used should
     // be local. Remote created time older than local, remote modified time
     // newer than local.
     description: "Created, last modified time reconciliation without local changes",
     parent: {
       "guid": "5113f329c42f",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "timeCreated": 1234,
       "timeLastModified": 5678,
       "timeLastUsed": 5678,
       "timesUsed": 6,
     },
     local: [],
     remote: {
       "guid": "5113f329c42f",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "timeCreated": 1200,
       "timeLastModified": 5700,
       "timeLastUsed": 5700,
       "timesUsed": 3,
     },
     reconciled: {
@@ -878,31 +878,31 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
     },
   },
   {
     // Local changes, remote created time newer than local, remote modified time
     // older than local.
     description: "Created, last modified time reconciliation with local changes",
     parent: {
       "guid": "791e5608b80a",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "timeCreated": 1234,
       "timeLastModified": 5678,
       "timeLastUsed": 5678,
       "timesUsed": 6,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     }],
     remote: {
       "guid": "791e5608b80a",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "timeCreated": 1300,
       "timeLastModified": 5000,
       "timeLastUsed": 5000,
       "timesUsed": 3,
     },
     reconciled: {
@@ -917,17 +917,17 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
 ];
 
 add_task(async function test_reconcile_unknown_version() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME);
 
   // Cross-version reconciliation isn't supported yet. See bug 1377204.
   await Assert.rejects(profileStorage.addresses.reconcile({
     "guid": "31d83d2725ec",
-    "version": 2,
+    "version": 3,
     "given-name": "Mark",
     "family-name": "Hammond",
   }), /Got unknown record version/);
 });
 
 add_task(async function test_reconcile_idempotent() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME);
 
--- a/browser/extensions/formautofill/test/unit/xpcshell.ini
+++ b/browser/extensions/formautofill/test/unit/xpcshell.ini
@@ -33,20 +33,20 @@ support-files =
 [test_getCategoriesFromFieldNames.js]
 [test_getFormInputDetails.js]
 [test_getInfo.js]
 [test_getRecords.js]
 [test_isAvailable.js]
 [test_isCJKName.js]
 [test_isFieldEligibleForAutofill.js]
 [test_markAsAutofillField.js]
-[test_masterPassword.js]
 [test_migrateRecords.js]
 [test_nameUtils.js]
 [test_onFormSubmitted.js]
+[test_osKeyStore.js]
 [test_parseAddressFormat.js]
 [test_profileAutocompleteResult.js]
 [test_phoneNumber.js]
 [test_reconcile.js]
 [test_savedFieldNames.js]
 [test_toOneLineAddress.js]
 [test_storage_tombstones.js]
 [test_storage_remove.js]
--- a/browser/tools/mozscreenshots/mozscreenshots/extension/moz.build
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/moz.build
@@ -2,17 +2,16 @@
 # vim: set filetype=python:
 # 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/.
 
 XPI_NAME = 'mozscreenshots'
 
 USE_EXTENSION_MANIFEST = True
-NO_JS_MANIFEST = True
 
 FINAL_TARGET_FILES += [
     'api.js',
     'manifest.json',
     'schema.json',
 ]
 
 FINAL_TARGET_FILES.resources += [
--- a/build/moz.configure/warnings.configure
+++ b/build/moz.configure/warnings.configure
@@ -82,18 +82,20 @@ check_and_add_gcc_warning('-Wimplicit-fa
 # --enable-warnings-as-errors is specified so that no unexpected fatal
 # warnings are produced.
 check_and_add_gcc_warning('-Werror=non-literal-null-conversion',
                           when='--enable-warnings-as-errors')
 
 # catches string literals used in boolean expressions
 check_and_add_gcc_warning('-Wstring-conversion')
 
-# catches overlapping range comparisons that are always true or false
+# catches comparisons that are always true or false
 check_and_add_gcc_warning('-Wtautological-overlap-compare')
+check_and_add_gcc_warning('-Wtautological-unsigned-enum-zero-compare')
+check_and_add_gcc_warning('-Wtautological-unsigned-zero-compare')
 
 # we inline 'new' and 'delete' in mozalloc
 check_and_add_gcc_warning('-Wno-inline-new-delete', cxx_compiler)
 
 # Prevent the following GCC warnings from being treated as errors:
 # too many false positives
 check_and_add_gcc_warning('-Wno-error=maybe-uninitialized')
 
--- a/client.mk
+++ b/client.mk
@@ -84,22 +84,16 @@ build::
 	# the command doesn't run in that case)
 	mkdir -p $(UPLOAD_PATH)
 	$(if $(findstring n,$(filter-out --%, $(MAKEFLAGS))),,+)env RUST_LOG=sccache=debug SCCACHE_ERROR_LOG=$(UPLOAD_PATH)/sccache.log $(MOZBUILD_MANAGE_SCCACHE_DAEMON) --start-server
 endif
 
 ####################################
 # Configure
 
-MAKEFILE      = $(wildcard $(OBJDIR)/Makefile)
-CONFIG_STATUS = $(wildcard $(OBJDIR)/config.status)
-
-# Include deps for configure written by configure itself.
-CONFIG_STATUS_DEPS := $(if $(wildcard $(OBJDIR)/config_status_deps.in),$(shell cat $(OBJDIR)/config_status_deps.in),)
-
 $(CONFIGURES): %: %.in
 	@echo Generating $@
 	cp -f $< $@
 	chmod +x $@
 
 CONFIGURE_ENV_ARGS += \
   MAKE='$(MAKE)' \
   $(NULL)
@@ -108,51 +102,31 @@ CONFIGURE_ENV_ARGS += \
 #   $(TOPSRCDIR) will set @srcdir@ to "."; otherwise, it is set to the full
 #   path of $(TOPSRCDIR).
 ifeq ($(TOPSRCDIR),$(OBJDIR))
   CONFIGURE = ./configure
 else
   CONFIGURE = $(TOPSRCDIR)/configure
 endif
 
-configure-files: $(CONFIGURES)
-
-configure-preqs = \
-  configure-files \
-  $(OBJDIR)/.mozconfig.json \
-  $(NULL)
-
-configure:: $(configure-preqs)
+configure:: $(CONFIGURES)
 	$(call BUILDSTATUS,TIERS configure)
 	$(call BUILDSTATUS,TIER_START configure)
 	@echo cd $(OBJDIR);
 	@echo $(CONFIGURE) $(CONFIGURE_ARGS)
 	@cd $(OBJDIR) && $(CONFIGURE_ENV_ARGS) $(CONFIGURE) $(CONFIGURE_ARGS) \
 	  || ( echo '*** Fix above errors and then restart with\
                "./mach build"' && exit 1 )
 	@touch $(OBJDIR)/Makefile
 	$(call BUILDSTATUS,TIER_FINISH configure)
 
-ifneq (,$(MAKEFILE))
-$(OBJDIR)/Makefile: $(OBJDIR)/config.status
-
-
-
-
-
-$(OBJDIR)/config.status: $(CONFIG_STATUS_DEPS)
-else
-$(OBJDIR)/Makefile: $(CONFIG_STATUS_DEPS)
-endif
-	@$(MAKE) -f $(TOPSRCDIR)/client.mk configure
-
 ####################################
 # Build it
 
-build::  $(OBJDIR)/Makefile $(OBJDIR)/config.status
+build::
 	+$(MOZ_MAKE)
 
 ifdef MOZ_AUTOMATION
 build::
 	+$(MOZ_MAKE) automation/build
 endif
 
 # This makefile doesn't support parallel execution. It does pass
--- a/devtools/client/debugger/debugger-controller.js
+++ b/devtools/client/debugger/debugger-controller.js
@@ -463,50 +463,50 @@ var DebuggerController = {
 
 function Workers() {
   this._workerForms = Object.create(null);
   this._onWorkerListChanged = this._onWorkerListChanged.bind(this);
   this._onWorkerSelect = this._onWorkerSelect.bind(this);
 }
 
 Workers.prototype = {
-  get _tabClient() {
+  get _targetFront() {
     return DebuggerController._target.activeTab;
   },
 
   connect: function () {
     if (!Prefs.workersEnabled) {
       return;
     }
 
     this._updateWorkerList();
 
-    // `_tabClient` can be BrowsingContextTargetFront (protocol.js front) or
+    // `_targetFront` can be BrowsingContextTargetFront (protocol.js front) or
     // WorkerClient/DebuggerClient (old fashion client)
-    if (typeof(this._tabClient.on) == "function") {
-      this._tabClient.on("workerListChanged", this._onWorkerListChanged);
+    if (typeof(this._targetFront.on) == "function") {
+      this._targetFront.on("workerListChanged", this._onWorkerListChanged);
     } else {
-      this._tabClient.addListener("workerListChanged", this._onWorkerListChanged);
+      this._targetFront.addListener("workerListChanged", this._onWorkerListChanged);
     }
   },
 
   disconnect: function () {
-    if (typeof(this._tabClient.on) == "function") {
-      this._tabClient.off("workerListChanged", this._onWorkerListChanged);
+    if (typeof(this._targetFront.on) == "function") {
+      this._targetFront.off("workerListChanged", this._onWorkerListChanged);
     } else {
-      this._tabClient.removeListener("workerListChanged", this._onWorkerListChanged);
+      this._targetFront.removeListener("workerListChanged", this._onWorkerListChanged);
     }
   },
 
   _updateWorkerList: function () {
-    if (!this._tabClient.listWorkers) {
+    if (!this._targetFront.listWorkers) {
       return;
     }
 
-    this._tabClient.listWorkers().then((response) => {
+    this._targetFront.listWorkers().then((response) => {
       let workerForms = Object.create(null);
       for (let worker of response.workers) {
         workerForms[worker.actor] = worker;
       }
 
       for (let workerTargetActor in this._workerForms) {
         if (!(workerTargetActor in workerForms)) {
           DebuggerView.Workers.removeWorker(this._workerForms[workerTargetActor]);
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-chrome-debugging.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-chrome-debugging.js
@@ -24,18 +24,18 @@ function initDebuggerClient() {
   DebuggerServer.registerAllActors();
   DebuggerServer.allowChromeProcess = true;
 
   let transport = DebuggerServer.connectPipe();
   return new DebuggerClient(transport);
 }
 
 async function attachThread(client, actor) {
-  let [response, tabClient] = await client.attachTarget(actor);
-  let [response2, threadClient] = await tabClient.attachThread(null);
+  let [response, targetFront] = await client.attachTarget(actor);
+  let [response2, threadClient] = await targetFront.attachThread(null);
   return threadClient;
 }
 
 function onNewSource(event, packet) {
   if (packet.source.url.startsWith("chrome:")) {
     ok(true, "Received a new chrome source: " + packet.source.url);
     gThreadClient.removeListener("newSource", onNewSource);
     gNewChromeSource.resolve();
--- a/devtools/client/debugger/test/mochitest/browser_dbg_listworkers.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_listworkers.js
@@ -7,53 +7,53 @@ function test() {
     DebuggerServer.init();
     DebuggerServer.registerAllActors();
 
     let client = new DebuggerClient(DebuggerServer.connectPipe());
     yield connect(client);
 
     let tab = yield addTab(TAB_URL);
     let { tabs } = yield listTabs(client);
-    let [, tabClient] = yield attachTarget(client, findTab(tabs, TAB_URL));
+    let [, targetFront] = yield attachTarget(client, findTab(tabs, TAB_URL));
 
-    let { workers } = yield listWorkers(tabClient);
+    let { workers } = yield listWorkers(targetFront);
     is(workers.length, 0);
 
     executeSoon(() => {
       evalInTab(tab, "var worker1 = new Worker('" + WORKER1_URL + "');");
     });
-    yield waitForWorkerListChanged(tabClient);
+    yield waitForWorkerListChanged(targetFront);
 
-    ({ workers } = yield listWorkers(tabClient));
+    ({ workers } = yield listWorkers(targetFront));
     is(workers.length, 1);
     is(workers[0].url, WORKER1_URL);
 
     executeSoon(() => {
       evalInTab(tab, "var worker2 = new Worker('" + WORKER2_URL + "');");
     });
-    yield waitForWorkerListChanged(tabClient);
+    yield waitForWorkerListChanged(targetFront);
 
-    ({ workers } = yield listWorkers(tabClient));
+    ({ workers } = yield listWorkers(targetFront));
     is(workers.length, 2);
     is(workers[0].url, WORKER1_URL);
     is(workers[1].url, WORKER2_URL);
 
     executeSoon(() => {
       evalInTab(tab, "worker1.terminate()");
     });
-    yield waitForWorkerListChanged(tabClient);
+    yield waitForWorkerListChanged(targetFront);
 
-    ({ workers } = yield listWorkers(tabClient));
+    ({ workers } = yield listWorkers(targetFront));
     is(workers.length, 1);
     is(workers[0].url, WORKER2_URL);
 
     executeSoon(() => {
       evalInTab(tab, "worker2.terminate()");
     });
-    yield waitForWorkerListChanged(tabClient);
+    yield waitForWorkerListChanged(targetFront);
 
-    ({ workers } = yield listWorkers(tabClient));
+    ({ workers } = yield listWorkers(targetFront));
     is(workers.length, 0);
 
     yield close(client);
     finish();
   });
 }
--- a/devtools/client/debugger/test/mochitest/browser_dbg_promises-chrome-allocation-stack.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_promises-chrome-allocation-stack.js
@@ -25,18 +25,18 @@ function test() {
 
     DebuggerServer.init();
     DebuggerServer.registerAllActors();
     DebuggerServer.allowChromeProcess = true;
 
     let client = new DebuggerClient(DebuggerServer.connectPipe());
     yield connect(client);
     let chrome = yield client.mainRoot.getProcess(0);
-    let [, tabClient] = yield attachTarget(client, chrome.form);
-    yield tabClient.attachThread();
+    let [, targetFront] = yield attachTarget(client, chrome.form);
+    yield targetFront.attachThread();
 
     yield testGetAllocationStack(client, chrome.form, () => {
       let p = new Promise(() => {});
       p.name = "p";
       let q = p.then();
       q.name = "q";
       let r = p.catch(() => {});
       r.name = "r";
--- a/devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js
@@ -17,23 +17,23 @@ add_task(async function() {
   DebuggerServer.init();
   DebuggerServer.registerAllActors();
 
   let client = new DebuggerClient(DebuggerServer.connectPipe());
   await connect(client);
 
   let tab = await addTab(TAB_URL);
   let { tabs } = await listTabs(client);
-  let [, tabClient] = await attachTarget(client, findTab(tabs, TAB_URL));
+  let [, targetFront] = await attachTarget(client, findTab(tabs, TAB_URL));
 
-  await listWorkers(tabClient);
+  await listWorkers(targetFront);
   await createWorkerInTab(tab, WORKER_URL);
 
-  let { workers } = await listWorkers(tabClient);
-  let [, workerClient] = await attachWorker(tabClient,
+  let { workers } = await listWorkers(targetFront);
+  let [, workerClient] = await attachWorker(targetFront,
                                              findWorker(workers, WORKER_URL));
 
   let toolbox = await gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
                                             "jsdebugger",
                                             Toolbox.HostType.WINDOW);
 
   is(toolbox.hostType, "window", "correct host");
 
--- a/devtools/client/debugger/test/mochitest/head.js
+++ b/devtools/client/debugger/test/mochitest/head.js
@@ -1084,34 +1084,34 @@ function findTab(tabs, url) {
   return null;
 }
 
 function attachTarget(client, tab) {
   info("Attaching to tab with url '" + tab.url + "'.");
   return client.attachTarget(tab.actor);
 }
 
-function listWorkers(tabClient) {
+function listWorkers(targetFront) {
   info("Listing workers.");
-  return tabClient.listWorkers();
+  return targetFront.listWorkers();
 }
 
 function findWorker(workers, url) {
   info("Finding worker with url '" + url + "'.");
   for (let worker of workers) {
     if (worker.url === url) {
       return worker;
     }
   }
   return null;
 }
 
-function attachWorker(tabClient, worker) {
+function attachWorker(targetFront, worker) {
   info("Attaching to worker with url '" + worker.url + "'.");
-  return tabClient.attachWorker(worker.actor);
+  return targetFront.attachWorker(worker.actor);
 }
 
 function waitForWorkerListChanged(targetFront) {
   info("Waiting for worker list to change.");
   return targetFront.once("workerListChanged");
 }
 
 function attachThread(workerClient, options) {
@@ -1278,25 +1278,25 @@ async function initWorkerDebugger(TAB_UR
   DebuggerServer.init();
   DebuggerServer.registerAllActors();
 
   let client = new DebuggerClient(DebuggerServer.connectPipe());
   await connect(client);
 
   let tab = await addTab(TAB_URL);
   let { tabs } = await listTabs(client);
-  let [, tabClient] = await attachTarget(client, findTab(tabs, TAB_URL));
+  let [, targetFront] = await attachTarget(client, findTab(tabs, TAB_URL));
 
   await createWorkerInTab(tab, WORKER_URL);
 
-  let { workers } = await listWorkers(tabClient);
-  let [, workerClient] = await attachWorker(tabClient,
+  let { workers } = await listWorkers(targetFront);
+  let [, workerClient] = await attachWorker(targetFront,
                                              findWorker(workers, WORKER_URL));
 
   let toolbox = await gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
                                             "jsdebugger",
                                             Toolbox.HostType.WINDOW);
 
   let debuggerPanel = toolbox.getCurrentPanel();
   let gDebugger = debuggerPanel.panelWin;
 
-  return {client, tab, tabClient, workerClient, toolbox, gDebugger};
+  return {client, tab, targetFront, workerClient, toolbox, gDebugger};
 }
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -511,18 +511,18 @@ TabTarget.prototype = {
    */
   attach() {
     if (this._attach) {
       return this._attach;
     }
 
     // Attach the target actor
     const attachTarget = async () => {
-      const [response, tabClient] = await this._client.attachTarget(this._form.actor);
-      this.activeTab = tabClient;
+      const [response, targetFront] = await this._client.attachTarget(this._form.actor);
+      this.activeTab = targetFront;
       this.threadActor = response.threadActor;
 
       this.activeTab.on("tabNavigated", this._onTabNavigated);
       this._onFrameUpdate = packet => {
         this.emit("frame-update", packet);
       };
       this.activeTab.on("frameUpdate", this._onFrameUpdate);
     };
@@ -852,20 +852,20 @@ function WorkerTarget(workerClient) {
   EventEmitter.decorate(this);
   this._workerClient = workerClient;
 }
 
 /**
  * A WorkerTarget represents a worker. Unlike TabTarget, which can represent
  * either a local or remote tab, WorkerTarget always represents a remote worker.
  * Moreover, unlike TabTarget, which is constructed with a placeholder object
- * for remote tabs (from which a TabClient can then be lazily obtained),
+ * for remote tabs (from which a TargetFront can then be lazily obtained),
  * WorkerTarget is constructed with a WorkerClient directly.
  *
- * WorkerClient is designed to mimic the interface of TabClient as closely as
+ * WorkerClient is designed to mimic the interface of TargetFront as closely as
  * possible. This allows us to debug workers as if they were ordinary tabs,
  * requiring only minimal changes to the rest of the frontend.
  */
 WorkerTarget.prototype = {
   get isRemote() {
     return true;
   },
 
--- a/devtools/client/netmonitor/src/har/har-automation.js
+++ b/devtools/client/netmonitor/src/har/har-automation.js
@@ -65,17 +65,17 @@ HarAutomation.prototype = {
       return;
     }
 
     if (!tabGrip) {
       return;
     }
 
     this.debuggerClient = client;
-    this.tabClient = this.toolbox.target.activeTab;
+    this.targetFront = this.toolbox.target.activeTab;
     this.webConsoleClient = this.toolbox.target.activeConsole;
 
     this.tabWatcher = new TabWatcher(this.toolbox, this);
     this.tabWatcher.connect();
   },
 
   pageLoadBegin: function(response) {
     this.resetCollector();
--- a/devtools/client/shared/test/browser_dbg_WorkerTargetActor.attach.js
+++ b/devtools/client/shared/test/browser_dbg_WorkerTargetActor.attach.js
@@ -29,50 +29,50 @@ function test() {
     DebuggerServer.init();
     DebuggerServer.registerAllActors();
 
     const client = new DebuggerClient(DebuggerServer.connectPipe());
     yield connect(client);
 
     const tab = yield addTab(TAB1_URL);
     const { tabs } = yield listTabs(client);
-    const [, tabClient] = yield attachTarget(client, findTab(tabs, TAB1_URL));
-    yield listWorkers(tabClient);
+    const [, targetFront] = yield attachTarget(client, findTab(tabs, TAB1_URL));
+    yield listWorkers(targetFront);
 
     // If a page still has pending network requests, it will not be moved into
     // the bfcache. Consequently, we cannot use waitForWorkerListChanged here,
     // because the worker is not guaranteed to have finished loading when it is
     // registered. Instead, we have to wait for the promise returned by
     // createWorker in the tab to be resolved.
     yield createWorkerInTab(tab, WORKER1_URL);
-    let { workers } = yield listWorkers(tabClient);
-    let [, workerClient1] = yield attachWorker(tabClient,
+    let { workers } = yield listWorkers(targetFront);
+    let [, workerClient1] = yield attachWorker(targetFront,
                                                findWorker(workers, WORKER1_URL));
     is(workerClient1.isClosed, false, "worker in tab 1 should not be closed");
 
     executeSoon(() => {
       BrowserTestUtils.loadURI(tab.linkedBrowser, TAB2_URL);
     });
     yield waitForWorkerClose(workerClient1);
     is(workerClient1.isClosed, true, "worker in tab 1 should be closed");
 
     yield createWorkerInTab(tab, WORKER2_URL);
-    ({ workers } = yield listWorkers(tabClient));
-    const [, workerClient2] = yield attachWorker(tabClient,
+    ({ workers } = yield listWorkers(targetFront));
+    const [, workerClient2] = yield attachWorker(targetFront,
                                                findWorker(workers, WORKER2_URL));
     is(workerClient2.isClosed, false, "worker in tab 2 should not be closed");
 
     executeSoon(() => {
       tab.linkedBrowser.goBack();
     });
     yield waitForWorkerClose(workerClient2);
     is(workerClient2.isClosed, true, "worker in tab 2 should be closed");
 
-    ({ workers } = yield listWorkers(tabClient));
-    [, workerClient1] = yield attachWorker(tabClient,
+    ({ workers } = yield listWorkers(targetFront));
+    [, workerClient1] = yield attachWorker(targetFront,
                                            findWorker(workers, WORKER1_URL));
     is(workerClient1.isClosed, false, "worker in tab 1 should not be closed");
 
     yield close(client);
     SpecialPowers.setIntPref(MAX_TOTAL_VIEWERS, oldMaxTotalViewers);
     finish();
   });
 }
--- a/devtools/client/shared/test/helper_workers.js
+++ b/devtools/client/shared/test/helper_workers.js
@@ -93,34 +93,34 @@ function findTab(tabs, url) {
   return null;
 }
 
 function attachTarget(client, tab) {
   info("Attaching to tab with url '" + tab.url + "'.");
   return client.attachTarget(tab.actor);
 }
 
-function listWorkers(tabClient) {
+function listWorkers(targetFront) {
   info("Listing workers.");
-  return tabClient.listWorkers();
+  return targetFront.listWorkers();
 }
 
 function findWorker(workers, url) {
   info("Finding worker with url '" + url + "'.");
   for (const worker of workers) {
     if (worker.url === url) {
       return worker;
     }
   }
   return null;
 }
 
-function attachWorker(tabClient, worker) {
+function attachWorker(targetFront, worker) {
   info("Attaching to worker with url '" + worker.url + "'.");
-  return tabClient.attachWorker(worker.actor);
+  return targetFront.attachWorker(worker.actor);
 }
 
 function attachThread(workerClient, options) {
   info("Attaching to thread.");
   return workerClient.attachThread(options);
 }
 
 async function waitForWorkerClose(workerClient) {
@@ -154,35 +154,35 @@ async function initWorkerDebugger(TAB_UR
   DebuggerServer.init();
   DebuggerServer.registerAllActors();
 
   const client = new DebuggerClient(DebuggerServer.connectPipe());
   await connect(client);
 
   const tab = await addTab(TAB_URL);
   const { tabs } = await listTabs(client);
-  const [, tabClient] = await attachTarget(client, findTab(tabs, TAB_URL));
+  const [, targetFront] = await attachTarget(client, findTab(tabs, TAB_URL));
 
   await createWorkerInTab(tab, WORKER_URL);
 
-  const { workers } = await listWorkers(tabClient);
-  const [, workerClient] = await attachWorker(tabClient,
+  const { workers } = await listWorkers(targetFront);
+  const [, workerClient] = await attachWorker(targetFront,
                                              findWorker(workers, WORKER_URL));
 
   const toolbox = await gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
                                             "jsdebugger",
                                             Toolbox.HostType.WINDOW);
 
   const debuggerPanel = toolbox.getCurrentPanel();
 
   const gDebugger = debuggerPanel.panelWin;
 
   const context = createDebuggerContext(toolbox);
 
-  return { ...context, client, tab, tabClient, workerClient, toolbox, gDebugger};
+  return { ...context, client, tab, targetFront, workerClient, toolbox, gDebugger};
 }
 
 // Override addTab/removeTab as defined by shared-head, since these have
 // an extra window parameter and add a frame script
 this.addTab = function addTab(url, win) {
   info("Adding tab: " + url);
 
   const deferred = getDeferredPromise().defer();
--- a/devtools/docs/backend/client-api.md
+++ b/devtools/docs/backend/client-api.md
@@ -73,42 +73,42 @@ Attaching to a browser tab requires enum
 ```javascript
 function attachToTab() {
   // Get the list of tabs to find the one to attach to.
   client.listTabs().then((response) => {
     // Find the active tab.
     let tab = response.tabs[response.selected];
 
     // Attach to the tab.
-    client.attachTarget(tab.actor).then(([response, tabClient]) => {
-      if (!tabClient) {
+    client.attachTarget(tab.actor).then(([response, targetFront]) => {
+      if (!targetFront) {
         return;
       }
 
-      // Now the tabClient is ready and can be used.
+      // Now the targetFront is ready and can be used.
 
       // Attach listeners for client events.
-      tabClient.addListener("tabNavigated", onTab);
+      targetFront.addListener("tabNavigated", onTab);
     });
   });
 }
 ```
 
 The debugger client will send event notifications for a number of events the application may be interested in. These events include state changes in the debugger, like pausing and resuming, stack frames or source scripts being ready for retrieval, etc.
 
 ## Handling location changes
 
 When the user navigates away from a page, a `tabNavigated` event will be fired. The proper way to handle this event is to detach from the previous thread and tab and attach to the new ones:
 
 ```javascript
 async function onTab() {
   // Detach from the previous thread.
   await client.activeThread.detach();
   // Detach from the previous tab.
-  await tabClient.activeTab.detach();
+  await targetFront.activeTab.detach();
   // Start debugging the new tab.
   start();
 }
 ```
 
 ## Debugging JavaScript running in a browser tab
 
 Once the application is attached to a tab, it can attach to its thread in order to interact with the JavaScript debugger:
@@ -181,18 +181,18 @@ function shutdownDebugger() {
  * Start debugging the current tab.
  */
 function debugTab() {
   // Get the list of tabs to find the one to attach to.
   client.listTabs().then(response => {
     // Find the active tab.
     let tab = response.tabs[response.selected];
     // Attach to the tab.
-    client.attachTarget(tab.actor).then(([response, tabClient]) => {
-      if (!tabClient) {
+    client.attachTarget(tab.actor).then(([response, targetFront]) => {
+      if (!targetFront) {
         return;
       }
 
       // Attach to the thread (context).
       client.attachThread(response.threadActor, (response, thread) => {
         if (!thread) {
           return;
         }
--- a/devtools/server/tests/mochitest/webconsole-helpers.js
+++ b/devtools/server/tests/mochitest/webconsole-helpers.js
@@ -24,17 +24,17 @@ if (!DebuggerServer.initialized) {
 /**
  * Open a tab, load the url, find the tab with the debugger server,
  * and attach the console to it.
  *
  * @param {string} url : url to navigate to
  * @return {Promise} Promise resolving when the console is attached.
  *         The Promise resolves with an object containing :
  *           - tab: the attached tab
- *           - tabClient: the tab client
+ *           - targetFront: the target front
  *           - consoleClient: the console client
  *           - cleanup: a generator function which can be called to close
  *             the opened tab and disconnect its debugger client.
  */
 async function attachURL(url) {
   let win = window.open(url, "_blank");
   let client = null;
 
@@ -54,18 +54,18 @@ async function attachURL(url) {
   await client.connect();
   const {tabs} = await client.listTabs();
   const attachedTab = tabs.find(tab => tab.url === url);
 
   if (!attachedTab) {
     throw new Error(`Could not find a tab matching URL ${url}`);
   }
 
-  const [, tabClient] = await client.attachTarget(attachedTab.actor);
+  const [, targetFront] = await client.attachTarget(attachedTab.actor);
   const [, consoleClient] = await client.attachConsole(attachedTab.consoleActor, []);
 
   return {
     tab: attachedTab,
-    tabClient,
+    targetFront,
     consoleClient,
     cleanup,
   };
 }
--- a/devtools/server/tests/unit/head_dbg.js
+++ b/devtools/server/tests/unit/head_dbg.js
@@ -185,19 +185,19 @@ function attachTarget(client, tab) {
 
 function waitForNewSource(threadClient, url) {
   dump("Waiting for new source with url '" + url + "'.\n");
   return waitForEvent(threadClient, "newSource", function(packet) {
     return packet.source.url === url;
   });
 }
 
-function attachThread(tabClient, options = {}) {
+function attachThread(targetFront, options = {}) {
   dump("Attaching to thread.\n");
-  return tabClient.attachThread(options);
+  return targetFront.attachThread(options);
 }
 
 function resume(threadClient) {
   dump("Resuming thread.\n");
   return threadClient.resume();
 }
 
 function getSources(threadClient) {
@@ -350,51 +350,51 @@ function getTestTab(client, title, callb
         return;
       }
     }
     callback(null);
   });
 }
 
 // Attach to |client|'s tab whose title is |title|; pass |callback| the
-// response packet and a TabClient instance referring to that tab.
+// response packet and a TargetFront instance referring to that tab.
 function attachTestTab(client, title, callback) {
   getTestTab(client, title, function(tab) {
-    client.attachTarget(tab.actor).then(([response, tabClient]) => {
-      callback(response, tabClient);
+    client.attachTarget(tab.actor).then(([response, targetFront]) => {
+      callback(response, targetFront);
     });
   });
 }
 
 // Attach to |client|'s tab whose title is |title|, and then attach to
 // that tab's thread. Pass |callback| the thread attach response packet, a
-// TabClient referring to the tab, and a ThreadClient referring to the
+// TargetFront referring to the tab, and a ThreadClient referring to the
 // thread.
 function attachTestThread(client, title, callback) {
-  attachTestTab(client, title, function(tabResponse, tabClient) {
+  attachTestTab(client, title, function(tabResponse, targetFront) {
     function onAttach([response, threadClient]) {
-      callback(response, tabClient, threadClient, tabResponse);
+      callback(response, targetFront, threadClient, tabResponse);
     }
-    tabClient.attachThread({
+    targetFront.attachThread({
       useSourceMaps: true,
       autoBlackBox: true
     }).then(onAttach);
   });
 }
 
 // Attach to |client|'s tab whose title is |title|, attach to the tab's
 // thread, and then resume it. Pass |callback| the thread's response to
-// the 'resume' packet, a TabClient for the tab, and a ThreadClient for the
+// the 'resume' packet, a TargetFront for the tab, and a ThreadClient for the
 // thread.
 function attachTestTabAndResume(client, title, callback = () => {}) {
   return new Promise((resolve) => {
-    attachTestThread(client, title, function(response, tabClient, threadClient) {
+    attachTestThread(client, title, function(response, targetFront, threadClient) {
       threadClient.resume(function(response) {
-        callback(response, tabClient, threadClient);
-        resolve([response, tabClient, threadClient]);
+        callback(response, targetFront, threadClient);
+        resolve([response, targetFront, threadClient]);
       });
     });
   });
 }
 
 /**
  * Initialize the testing debugger server.
  */
@@ -857,19 +857,19 @@ async function setupTestFromUrl(url) {
   const global = createTestGlobal("test");
   DebuggerServer.addTestGlobal(global);
 
   const debuggerClient = new DebuggerClient(DebuggerServer.connectPipe());
   await connect(debuggerClient);
 
   const { tabs } = await listTabs(debuggerClient);
   const tab = findTab(tabs, "test");
-  const [, tabClient] = await attachTarget(debuggerClient, tab);
+  const [, targetFront] = await attachTarget(debuggerClient, tab);
 
-  const [, threadClient] = await attachThread(tabClient);
+  const [, threadClient] = await attachThread(targetFront);
   await resume(threadClient);
 
   const sourceUrl = getFileUrl(url);
   const promise = waitForNewSource(threadClient, sourceUrl);
   loadSubScript(sourceUrl, global);
   const { source } = await promise;
 
   const sourceClient = threadClient.source(source);
--- a/devtools/server/tests/unit/test_attach.js
+++ b/devtools/server/tests/unit/test_attach.js
@@ -9,25 +9,25 @@ var gDebuggee;
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = testGlobal("test-1");
   DebuggerServer.addTestGlobal(gDebuggee);
 
   const transport = DebuggerServer.connectPipe();
   gClient = new DebuggerClient(transport);
   gClient.connect().then(function([type, traits]) {
-    attachTestTab(gClient, "test-1", function(reply, tabClient) {
-      test_attach(tabClient);
+    attachTestTab(gClient, "test-1", function(reply, targetFront) {
+      test_attach(targetFront);
     });
   });
   do_test_pending();
 }
 
-function test_attach(tabClient) {
-  tabClient.attachThread({}).then(function([response, threadClient]) {
+function test_attach(targetFront) {
+  targetFront.attachThread({}).then(function([response, threadClient]) {
     Assert.equal(threadClient.state, "paused");
     threadClient.resume(cleanup);
   });
 }
 
 function cleanup() {
   gClient.addListener("closed", function(event) {
     do_test_finished();
--- a/devtools/server/tests/unit/test_blackboxing-01.js
+++ b/devtools/server/tests/unit/test_blackboxing-01.js
@@ -12,17 +12,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-black-box");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-black-box",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              testBlackBox();
                            });
   });
   do_test_pending();
 }
 
 const BLACK_BOXED_URL = "http://example.com/blackboxme.js";
--- a/devtools/server/tests/unit/test_blackboxing-02.js
+++ b/devtools/server/tests/unit/test_blackboxing-02.js
@@ -16,17 +16,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-black-box");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-black-box",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_black_box();
                            });
   });
   do_test_pending();
 }
 
 const BLACK_BOXED_URL = "http://example.com/blackboxme.js";
--- a/devtools/server/tests/unit/test_blackboxing-03.js
+++ b/devtools/server/tests/unit/test_blackboxing-03.js
@@ -14,17 +14,17 @@ var gThreadClient;
 var gBpClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-black-box");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-black-box",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_black_box();
                            });
   });
   do_test_pending();
 }
 
 const BLACK_BOXED_URL = "http://example.com/blackboxme.js";
--- a/devtools/server/tests/unit/test_blackboxing-04.js
+++ b/devtools/server/tests/unit/test_blackboxing-04.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-black-box");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-black-box",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_black_box();
                            });
   });
   do_test_pending();
 }
 
 const BLACK_BOXED_URL = "http://example.com/blackboxme.js";
--- a/devtools/server/tests/unit/test_blackboxing-05.js
+++ b/devtools/server/tests/unit/test_blackboxing-05.js
@@ -14,17 +14,17 @@ var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-black-box");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(
       gClient, "test-black-box",
-      function(response, tabClient, threadClient) {
+      function(response, targetFront, threadClient) {
         gThreadClient = threadClient;
         // XXX: We have to do an executeSoon so that the error isn't caught and
         // reported by DebuggerClient.requester (because we are using the local
         // transport and share a stack) which causes the test to fail.
         Services.tm.dispatchToMainThread({
           run: test_black_box
         });
       });
--- a/devtools/server/tests/unit/test_blackboxing-06.js
+++ b/devtools/server/tests/unit/test_blackboxing-06.js
@@ -16,17 +16,17 @@ const {SourceNode} = require("source-map
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-black-box");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(
       gClient, "test-black-box",
-      function(response, tabClient, threadClient) {
+      function(response, targetFront, threadClient) {
         gThreadClient = threadClient;
 
         Promise.resolve(setup_code())
           .then(black_box_code)
           .then(run_code)
           .then(test_correct_location)
           .catch(function(error) {
             Assert.ok(false, "Should not get an error, got " + error);
--- a/devtools/server/tests/unit/test_blackboxing-07.js
+++ b/devtools/server/tests/unit/test_blackboxing-07.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-black-box");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-black-box",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              testBlackBox();
                            });
   });
   do_test_pending();
 }
 
 const BLACK_BOXED_URL = "http://example.com/black-boxed.min.js";
--- a/devtools/server/tests/unit/test_breakpoint-01.js
+++ b/devtools/server/tests/unit/test_breakpoint-01.js
@@ -21,17 +21,17 @@ function run_test() {
 
 function run_test_with_server(server, callback) {
   gCallback = callback;
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-stack", server);
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_simple_breakpoint();
                            });
   });
 }
 
 function test_simple_breakpoint() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_breakpoint-02.js
+++ b/devtools/server/tests/unit/test_breakpoint-02.js
@@ -22,17 +22,17 @@ function run_test() {
 
 function run_test_with_server(server, callback) {
   gCallback = callback;
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-stack", server);
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_breakpoint_running();
                            });
   });
 }
 
 function test_breakpoint_running() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_breakpoint-03.js
+++ b/devtools/server/tests/unit/test_breakpoint-03.js
@@ -25,17 +25,17 @@ function run_test() {
 function run_test_with_server(server, callback) {
   gCallback = callback;
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-stack", server);
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient,
                            "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_skip_breakpoint();
                            });
   });
 }
 
 var test_no_skip_breakpoint = async function(source, location) {
   const [response, bpClient] = await source.setBreakpoint(
--- a/devtools/server/tests/unit/test_breakpoint-04.js
+++ b/devtools/server/tests/unit/test_breakpoint-04.js
@@ -22,17 +22,17 @@ function run_test() {
 
 function run_test_with_server(server, callback) {
   gCallback = callback;
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-stack", server);
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_child_breakpoint();
                            });
   });
 }
 
 function test_child_breakpoint() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_breakpoint-05.js
+++ b/devtools/server/tests/unit/test_breakpoint-05.js
@@ -23,17 +23,17 @@ function run_test() {
 
 function run_test_with_server(server, callback) {
   gCallback = callback;
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-stack", server);
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_child_skip_breakpoint();
                            });
   });
 }
 
 function test_child_skip_breakpoint() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_breakpoint-06.js
+++ b/devtools/server/tests/unit/test_breakpoint-06.js
@@ -23,17 +23,17 @@ function run_test() {
 
 function run_test_with_server(server, callback) {
   gCallback = callback;
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-stack", server);
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_nested_breakpoint();
                            });
   });
 }
 
 function test_nested_breakpoint() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_breakpoint-07.js
+++ b/devtools/server/tests/unit/test_breakpoint-07.js
@@ -23,17 +23,17 @@ function run_test() {
 
 function run_test_with_server(server, callback) {
   gCallback = callback;
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-stack", server);
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_second_child_skip_breakpoint();
                            });
   });
 }
 
 function test_second_child_skip_breakpoint() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_breakpoint-08.js
+++ b/devtools/server/tests/unit/test_breakpoint-08.js
@@ -23,17 +23,17 @@ function run_test() {
 
 function run_test_with_server(server, callback) {
   gCallback = callback;
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-stack", server);
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_child_skip_breakpoint();
                            });
   });
 }
 
 function test_child_skip_breakpoint() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_breakpoint-09.js
+++ b/devtools/server/tests/unit/test_breakpoint-09.js
@@ -22,17 +22,17 @@ function run_test() {
 
 function run_test_with_server(server, callback) {
   gCallback = callback;
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-stack", server);
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_remove_breakpoint();
                            });
   });
 }
 
 function test_remove_breakpoint() {
   let done = false;
--- a/devtools/server/tests/unit/test_breakpoint-10.js
+++ b/devtools/server/tests/unit/test_breakpoint-10.js
@@ -23,17 +23,17 @@ function run_test() {
 
 function run_test_with_server(server, callback) {
   gCallback = callback;
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-stack", server);
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_child_breakpoint();
                            });
   });
 }
 
 function test_child_breakpoint() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_breakpoint-11.js
+++ b/devtools/server/tests/unit/test_breakpoint-11.js
@@ -23,17 +23,17 @@ function run_test() {
 
 function run_test_with_server(server, callback) {
   gCallback = callback;
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-stack", server);
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_child_breakpoint();
                            });
   });
 }
 
 function test_child_breakpoint() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_breakpoint-12.js
+++ b/devtools/server/tests/unit/test_breakpoint-12.js
@@ -27,17 +27,17 @@ function run_test() {
 function run_test_with_server(server, callback) {
   gCallback = callback;
   gCount = 1;
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-stack", server);
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_child_skip_breakpoint();
                            });
   });
 }
 
 function test_child_skip_breakpoint() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_breakpoint-13.js
+++ b/devtools/server/tests/unit/test_breakpoint-13.js
@@ -23,17 +23,17 @@ function run_test() {
 
 function run_test_with_server(server, callback) {
   gCallback = callback;
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-stack", server);
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_simple_breakpoint();
                            });
   });
 }
 
 function test_simple_breakpoint() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_breakpoint-14.js
+++ b/devtools/server/tests/unit/test_breakpoint-14.js
@@ -23,17 +23,17 @@ function run_test() {
 
 function run_test_with_server(server, callback) {
   gCallback = callback;
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-stack", server);
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_simple_breakpoint();
                            });
   });
 }
 
 function test_simple_breakpoint() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_breakpoint-15.js
+++ b/devtools/server/tests/unit/test_breakpoint-15.js
@@ -12,17 +12,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              testSameBreakpoint();
                            });
   });
   do_test_pending();
 }
 
 const SOURCE_URL = "http://example.com/source.js";
--- a/devtools/server/tests/unit/test_breakpoint-16.js
+++ b/devtools/server/tests/unit/test_breakpoint-16.js
@@ -23,17 +23,17 @@ function run_test() {
 function run_test_with_server(server, callback) {
   gCallback = callback;
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-breakpoints", server);
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient,
                            "test-breakpoints",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_column_breakpoint();
                            });
   });
 }
 
 function test_column_breakpoint() {
   // Debugger statement
--- a/devtools/server/tests/unit/test_breakpoint-17.js
+++ b/devtools/server/tests/unit/test_breakpoint-17.js
@@ -21,17 +21,17 @@ function run_test() {
 
 function run_test_with_server(server, callback) {
   gCallback = callback;
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-breakpoints", server);
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-breakpoints",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_breakpoints_columns();
                            });
   });
 }
 
 const code =
 "(" + function(global) {
--- a/devtools/server/tests/unit/test_breakpoint-18.js
+++ b/devtools/server/tests/unit/test_breakpoint-18.js
@@ -24,17 +24,17 @@ function run_test_with_server(server, ca
   gCallback = callback;
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-breakpoints", server);
   gDebuggee.console = { log: x => void x };
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient,
                            "test-breakpoints",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              setUpCode();
                            });
   });
 }
 
 function setUpCode() {
   gClient.addOneTimeListener("paused", setBreakpoint);
--- a/devtools/server/tests/unit/test_breakpoint-19.js
+++ b/devtools/server/tests/unit/test_breakpoint-19.js
@@ -22,17 +22,17 @@ function run_test() {
 function run_test_with_server(server, callback) {
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-breakpoints", server);
   gDebuggee.console = { log: x => void x };
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient,
                            "test-breakpoints",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              testBreakpoint();
                            });
   });
 }
 
 const URL = "test.js";
 
--- a/devtools/server/tests/unit/test_breakpoint-20.js
+++ b/devtools/server/tests/unit/test_breakpoint-20.js
@@ -16,17 +16,17 @@ function run_test() {
   gDebuggee = addTestGlobal("test-breakpoints");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestThread(gClient, "test-breakpoints", testBreakpoint);
   });
   do_test_pending();
 }
 
-const testBreakpoint = async function(threadResponse, tabClient,
+const testBreakpoint = async function(threadResponse, targetFront,
                                              threadClient, tabResponse) {
   evalSetupCode();
 
   // Load the test source once.
 
   evalTestCode();
   equal(gDebuggee.functions.length, 1,
         "The test code should have added a function.");
--- a/devtools/server/tests/unit/test_breakpoint-21.js
+++ b/devtools/server/tests/unit/test_breakpoint-21.js
@@ -22,17 +22,17 @@ function run_test() {
 
 function run_test_with_server(server, callback) {
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-breakpoints", server);
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient,
                            "test-breakpoints",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test();
                            });
   });
 }
 
 const test = async function() {
   // Populate the `ScriptStore` so that we only test that the script
--- a/devtools/server/tests/unit/test_breakpoint-22.js
+++ b/devtools/server/tests/unit/test_breakpoint-22.js
@@ -21,17 +21,17 @@ function run_test() {
 
 function run_test_with_server(server, callback) {
   initTestDebuggerServer(server);
   gDebuggee = addTestGlobal("test-breakpoints", server);
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient,
                            "test-breakpoints",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test();
                            });
   });
 }
 
 const test = async function() {
   // Populate the `ScriptStore` so that we only test that the script
--- a/devtools/server/tests/unit/test_client_close.js
+++ b/devtools/server/tests/unit/test_client_close.js
@@ -9,17 +9,17 @@ var gDebuggee;
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = testGlobal("test-1");
   DebuggerServer.addTestGlobal(gDebuggee);
 
   const transport = DebuggerServer.connectPipe();
   gClient = new DebuggerClient(transport);
   gClient.connect().then(function(type, traits) {
-    attachTestTab(gClient, "test-1", function(reply, tabClient) {
+    attachTestTab(gClient, "test-1", function(reply, targetFront) {
       test_close(transport);
     });
   });
   do_test_pending();
 }
 
 function test_close(transport) {
   // Check that, if we fake a transport shutdown
--- a/devtools/server/tests/unit/test_conditional_breakpoint-01.js
+++ b/devtools/server/tests/unit/test_conditional_breakpoint-01.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-conditional-breakpoint");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-conditional-breakpoint",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_simple_breakpoint();
                            });
   });
   do_test_pending();
 }
 
 function test_simple_breakpoint() {
--- a/devtools/server/tests/unit/test_conditional_breakpoint-02.js
+++ b/devtools/server/tests/unit/test_conditional_breakpoint-02.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-conditional-breakpoint");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-conditional-breakpoint",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_simple_breakpoint();
                            });
   });
   do_test_pending();
 }
 
 function test_simple_breakpoint() {
--- a/devtools/server/tests/unit/test_conditional_breakpoint-03.js
+++ b/devtools/server/tests/unit/test_conditional_breakpoint-03.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-conditional-breakpoint");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-conditional-breakpoint",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_simple_breakpoint();
                            });
   });
   do_test_pending();
 }
 
 function test_simple_breakpoint() {
--- a/devtools/server/tests/unit/test_dbgclient_debuggerstatement.js
+++ b/devtools/server/tests/unit/test_dbgclient_debuggerstatement.js
@@ -1,36 +1,36 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 var gClient;
-var gTabClient;
+var gTargetFront;
 var gDebuggee;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = testGlobal("test-1");
   DebuggerServer.addTestGlobal(gDebuggee);
 
   const transport = DebuggerServer.connectPipe();
   gClient = new DebuggerClient(transport);
   gClient.connect().then(function([type, traits]) {
-    attachTestTab(gClient, "test-1", function(reply, tabClient) {
-      gTabClient = tabClient;
+    attachTestTab(gClient, "test-1", function(reply, targetFront) {
+      gTargetFront = targetFront;
       test_threadAttach(reply.threadActor);
     });
   });
   do_test_pending();
 }
 
 function test_threadAttach(threadActorID) {
   info("Trying to attach to thread " + threadActorID);
-  gTabClient.attachThread({}).then(function([response, threadClient]) {
+  gTargetFront.attachThread({}).then(function([response, threadClient]) {
     Assert.equal(threadClient.state, "paused");
     Assert.equal(threadClient.actor, threadActorID);
     threadClient.resume(function() {
       Assert.equal(threadClient.state, "attached");
       test_debugger_statement(threadClient);
     });
   });
 }
--- a/devtools/server/tests/unit/test_eval-01.js
+++ b/devtools/server/tests/unit/test_eval-01.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_simple_eval();
                            });
   });
   do_test_pending();
 }
 
 function test_simple_eval() {
--- a/devtools/server/tests/unit/test_eval-02.js
+++ b/devtools/server/tests/unit/test_eval-02.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_throw_eval();
                            });
   });
   do_test_pending();
 }
 
 function test_throw_eval() {
--- a/devtools/server/tests/unit/test_eval-03.js
+++ b/devtools/server/tests/unit/test_eval-03.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_syntax_error_eval();
                            });
   });
   do_test_pending();
 }
 
 function test_syntax_error_eval() {
--- a/devtools/server/tests/unit/test_eval-04.js
+++ b/devtools/server/tests/unit/test_eval-04.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_different_frames_eval();
                            });
   });
   do_test_pending();
 }
 
 function test_different_frames_eval() {
--- a/devtools/server/tests/unit/test_eval-05.js
+++ b/devtools/server/tests/unit/test_eval-05.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pauses_eval();
                            });
   });
   do_test_pending();
 }
 
 function test_pauses_eval() {
--- a/devtools/server/tests/unit/test_frameactor-01.js
+++ b/devtools/server/tests/unit/test_frameactor-01.js
@@ -12,17 +12,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
--- a/devtools/server/tests/unit/test_frameactor-02.js
+++ b/devtools/server/tests/unit/test_frameactor-02.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
--- a/devtools/server/tests/unit/test_frameactor-03.js
+++ b/devtools/server/tests/unit/test_frameactor-03.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
--- a/devtools/server/tests/unit/test_frameactor-04.js
+++ b/devtools/server/tests/unit/test_frameactor-04.js
@@ -12,17 +12,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 var gFrames = [
--- a/devtools/server/tests/unit/test_frameactor-05.js
+++ b/devtools/server/tests/unit/test_frameactor-05.js
@@ -14,17 +14,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
--- a/devtools/server/tests/unit/test_frameactor_wasm-01.js
+++ b/devtools/server/tests/unit/test_frameactor_wasm-01.js
@@ -19,17 +19,17 @@ function run_test() {
   }
 
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(
       gClient, "test-stack",
-      function(response, tabClient, threadClient) {
+      function(response, targetFront, threadClient) {
         gThreadClient = threadClient;
         gThreadClient.reconfigure({
           observeAsmJS: true,
           wasmBinarySource: true
         }, function(response) {
           Assert.equal(!!response.error, false);
           test_pause_frame();
         });
--- a/devtools/server/tests/unit/test_framearguments-01.js
+++ b/devtools/server/tests/unit/test_framearguments-01.js
@@ -12,17 +12,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
--- a/devtools/server/tests/unit/test_framebindings-01.js
+++ b/devtools/server/tests/unit/test_framebindings-01.js
@@ -12,17 +12,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
--- a/devtools/server/tests/unit/test_framebindings-02.js
+++ b/devtools/server/tests/unit/test_framebindings-02.js
@@ -12,17 +12,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
--- a/devtools/server/tests/unit/test_framebindings-03.js
+++ b/devtools/server/tests/unit/test_framebindings-03.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
--- a/devtools/server/tests/unit/test_framebindings-04.js
+++ b/devtools/server/tests/unit/test_framebindings-04.js
@@ -14,17 +14,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
--- a/devtools/server/tests/unit/test_framebindings-05.js
+++ b/devtools/server/tests/unit/test_framebindings-05.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
--- a/devtools/server/tests/unit/test_framebindings-06.js
+++ b/devtools/server/tests/unit/test_framebindings-06.js
@@ -9,17 +9,17 @@ var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-grips");
 
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_banana_environment();
                            });
   });
   do_test_pending();
 }
 
 function test_banana_environment() {
--- a/devtools/server/tests/unit/test_framebindings-07.js
+++ b/devtools/server/tests/unit/test_framebindings-07.js
@@ -11,17 +11,17 @@ var gThreadClient;
 // Test that the EnvironmentClient's getBindings() method works as expected.
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-bindings");
 
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-bindings",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_banana_environment();
                            });
   });
   do_test_pending();
 }
 
 function test_banana_environment() {
--- a/devtools/server/tests/unit/test_frameclient-01.js
+++ b/devtools/server/tests/unit/test_frameclient-01.js
@@ -9,17 +9,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
--- a/devtools/server/tests/unit/test_frameclient-02.js
+++ b/devtools/server/tests/unit/test_frameclient-02.js
@@ -8,17 +8,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
--- a/devtools/server/tests/unit/test_functiongrips-01.js
+++ b/devtools/server/tests/unit/test_functiongrips-01.js
@@ -12,17 +12,17 @@ function run_test() {
   gDebuggee = addTestGlobal("test-grips");
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_named_function();
                            });
   });
   do_test_pending();
 }
 
 function test_named_function() {
--- a/devtools/server/tests/unit/test_get-executable-lines-source-map.js
+++ b/devtools/server/tests/unit/test_get-executable-lines-source-map.js
@@ -17,17 +17,17 @@ const SOURCE_MAPPED_FILE = getFileUrl("s
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-get-executable-lines");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function _onConnect() {
     attachTestTabAndResume(
       gClient,
       "test-get-executable-lines",
-      function(response, tabClient, threadClient) {
+      function(response, targetFront, threadClient) {
         gThreadClient = threadClient;
         test_executable_lines();
       }
     );
   });
 
   do_test_pending();
 }
--- a/devtools/server/tests/unit/test_get-executable-lines.js
+++ b/devtools/server/tests/unit/test_get-executable-lines.js
@@ -17,17 +17,17 @@ const SOURCE_MAPPED_FILE = getFileUrl("s
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-get-executable-lines");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function _onConnect() {
     attachTestTabAndResume(
       gClient,
       "test-get-executable-lines",
-      function(response, tabClient, threadClient) {
+      function(response, targetFront, threadClient) {
         gThreadClient = threadClient;
         test_executable_lines();
       }
     );
   });
 
   do_test_pending();
 }
--- a/devtools/server/tests/unit/test_interrupt.js
+++ b/devtools/server/tests/unit/test_interrupt.js
@@ -15,18 +15,18 @@ function run_test() {
   const transport = DebuggerServer.connectPipe();
   gClient = new DebuggerClient(transport);
   gClient.connect().then(function(type, traits) {
     attachTestTab(gClient, "test-1", test_attach);
   });
   do_test_pending();
 }
 
-function test_attach(response, tabClient) {
-  tabClient.attachThread({}).then(function([response, threadClient]) {
+function test_attach(response, targetFront) {
+  targetFront.attachThread({}).then(function([response, threadClient]) {
     Assert.equal(threadClient.paused, true);
     threadClient.resume(function() {
       test_interrupt(threadClient);
     });
   });
 }
 
 function test_interrupt(threadClient) {
--- a/devtools/server/tests/unit/test_listsources-01.js
+++ b/devtools/server/tests/unit/test_listsources-01.js
@@ -22,17 +22,17 @@ function run_test() {
       if (request.type === "sources") {
         ++gNumTimesSourcesSent;
       }
       return origRequest.call(this, request, onResponse);
     };
   }(gClient.request));
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_simple_listsources();
                            });
   });
   do_test_pending();
 }
 
 function test_simple_listsources() {
--- a/devtools/server/tests/unit/test_listsources-02.js
+++ b/devtools/server/tests/unit/test_listsources-02.js
@@ -21,17 +21,17 @@ function run_test() {
       if (request.type === "sources") {
         ++gNumTimesSourcesSent;
       }
       return origRequest.call(this, request, onResponse);
     };
   }(gClient.request));
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_listing_zero_sources();
                            });
   });
   do_test_pending();
 }
 
 function test_listing_zero_sources() {
--- a/devtools/server/tests/unit/test_listsources-03.js
+++ b/devtools/server/tests/unit/test_listsources-03.js
@@ -12,17 +12,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-sources");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-sources",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_simple_listsources();
                            });
   });
   do_test_pending();
 }
 
 function test_simple_listsources() {
--- a/devtools/server/tests/unit/test_longstringgrips-01.js
+++ b/devtools/server/tests/unit/test_longstringgrips-01.js
@@ -12,17 +12,17 @@ function run_test() {
   gDebuggee = addTestGlobal("test-grips");
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_longstring_grip();
                            });
   });
   do_test_pending();
 }
 
 function test_longstring_grip() {
--- a/devtools/server/tests/unit/test_longstringgrips-02.js
+++ b/devtools/server/tests/unit/test_longstringgrips-02.js
@@ -12,17 +12,17 @@ function run_test() {
   gDebuggee = addTestGlobal("test-grips");
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(
-      gClient, "test-grips", function(response, tabClient, threadClient) {
+      gClient, "test-grips", function(response, targetFront, threadClient) {
         gThreadClient = threadClient;
         test_longstring_grip();
       });
   });
   do_test_pending();
 }
 
 function test_longstring_grip() {
--- a/devtools/server/tests/unit/test_nesting-01.js
+++ b/devtools/server/tests/unit/test_nesting-01.js
@@ -12,17 +12,17 @@ var gThreadActor;
 
 function run_test() {
   initTestDebuggerServer();
   addTestGlobal("test-nesting");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(
       gClient, "test-nesting",
-      function(response, tabClient, threadClient) {
+      function(response, targetFront, threadClient) {
         // Reach over the protocol connection and get a reference to the thread actor.
         gThreadActor =
           threadClient._transport._serverConnection.getActor(threadClient._actor);
 
         test_nesting();
       });
   });
   do_test_pending();
--- a/devtools/server/tests/unit/test_nesting-02.js
+++ b/devtools/server/tests/unit/test_nesting-02.js
@@ -12,17 +12,17 @@ var gThreadActor;
 
 function run_test() {
   initTestDebuggerServer();
   addTestGlobal("test-nesting");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(
       gClient, "test-nesting",
-      function(response, tabClient, threadClient) {
+      function(response, targetFront, threadClient) {
         // Reach over the protocol connection and get a reference to the thread
         // actor.
         gThreadActor =
           threadClient._transport._serverConnection.getActor(threadClient._actor);
 
         test_nesting();
       });
   });
--- a/devtools/server/tests/unit/test_nesting-03.js
+++ b/devtools/server/tests/unit/test_nesting-03.js
@@ -12,29 +12,29 @@ var gClient1, gClient2, gThreadClient1, 
 function run_test() {
   initTestDebuggerServer();
   addTestGlobal("test-nesting1");
   addTestGlobal("test-nesting1");
   // Conect the first client to the first debuggee.
   gClient1 = new DebuggerClient(DebuggerServer.connectPipe());
   gClient1.connect(function() {
     attachTestThread(gClient1, "test-nesting1",
-                     function(response, tabClient, threadClient) {
+                     function(response, targetFront, threadClient) {
                        gThreadClient1 = threadClient;
                        start_second_connection();
                      });
   });
   do_test_pending();
 }
 
 function start_second_connection() {
   gClient2 = new DebuggerClient(DebuggerServer.connectPipe());
   gClient2.connect(function() {
     attachTestThread(gClient2, "test-nesting1",
-                     function(response, tabClient, threadClient) {
+                     function(response, targetFront, threadClient) {
                        gThreadClient2 = threadClient;
                        test_nesting();
                      });
   });
 }
 
 function test_nesting() {
   gThreadClient1.resume(response => {
--- a/devtools/server/tests/unit/test_new_source-01.js
+++ b/devtools/server/tests/unit/test_new_source-01.js
@@ -12,17 +12,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_simple_new_source();
                            });
   });
   do_test_pending();
 }
 
 function test_simple_new_source() {
--- a/devtools/server/tests/unit/test_new_source-02.js
+++ b/devtools/server/tests/unit/test_new_source-02.js
@@ -12,17 +12,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_simple_new_source();
                            });
   });
   do_test_pending();
 }
 
 function test_simple_new_source() {
--- a/devtools/server/tests/unit/test_objectgrips-01.js
+++ b/devtools/server/tests/unit/test_objectgrips-01.js
@@ -21,17 +21,17 @@ function run_test_with_server(server, ca
   gDebuggee = addTestGlobal("test-grips", server);
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_object_grip();
                            });
   });
 }
 
 function test_object_grip() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_objectgrips-02.js
+++ b/devtools/server/tests/unit/test_objectgrips-02.js
@@ -22,17 +22,17 @@ function run_test_with_server(server, ca
   gDebuggee = addTestGlobal("test-grips", server);
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_object_grip();
                            });
   });
 }
 
 function test_object_grip() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_objectgrips-03.js
+++ b/devtools/server/tests/unit/test_objectgrips-03.js
@@ -22,17 +22,17 @@ function run_test_with_server(server, ca
   gDebuggee = addTestGlobal("test-grips", server);
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_object_grip();
                            });
   });
 }
 
 function test_object_grip() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_objectgrips-04.js
+++ b/devtools/server/tests/unit/test_objectgrips-04.js
@@ -22,17 +22,17 @@ function run_test_with_server(server, ca
   gDebuggee = addTestGlobal("test-grips", server);
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_object_grip();
                            });
   });
 }
 
 function test_object_grip() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_objectgrips-05.js
+++ b/devtools/server/tests/unit/test_objectgrips-05.js
@@ -26,17 +26,17 @@ function run_test_with_server(server, ca
   gDebuggee = addTestGlobal("test-grips", server);
   gDebuggee.eval(function stopMe(arg1, arg2) {
     debugger;
   }.toString());
 
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_object_grip();
                            });
   });
 }
 
 function test_object_grip() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_objectgrips-06.js
+++ b/devtools/server/tests/unit/test_objectgrips-06.js
@@ -26,17 +26,17 @@ function run_test_with_server(server, ca
   gDebuggee = addTestGlobal("test-grips", server);
   gDebuggee.eval(function stopMe(arg1, arg2) {
     debugger;
   }.toString());
 
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_object_grip();
                            });
   });
 }
 
 function test_object_grip() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_objectgrips-07.js
+++ b/devtools/server/tests/unit/test_objectgrips-07.js
@@ -26,17 +26,17 @@ function run_test_with_server(server, ca
   gDebuggee = addTestGlobal("test-grips", server);
   gDebuggee.eval(function stopMe(arg1, arg2, arg3, arg4) {
     debugger;
   }.toString());
 
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_object_grip();
                            });
   });
 }
 
 function test_object_grip() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_objectgrips-08.js
+++ b/devtools/server/tests/unit/test_objectgrips-08.js
@@ -21,17 +21,17 @@ function run_test_with_server(server, ca
   gDebuggee = addTestGlobal("test-grips", server);
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_object_grip();
                            });
   });
 }
 
 function test_object_grip() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_objectgrips-09.js
+++ b/devtools/server/tests/unit/test_objectgrips-09.js
@@ -26,17 +26,17 @@ function run_test_with_server(server, ca
   gDebuggee = addTestGlobal("test-grips", server);
   gDebuggee.eval(function stopMe(arg1, arg2) {
     debugger;
   }.toString());
 
   gClient = new DebuggerClient(server.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_object_grip();
                            });
   });
 }
 
 function test_object_grip() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
--- a/devtools/server/tests/unit/test_objectgrips-10.js
+++ b/devtools/server/tests/unit/test_objectgrips-10.js
@@ -12,17 +12,17 @@ var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-closures");
 
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-closures",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_object_grip();
                            });
   });
   do_test_pending();
 }
 
 function test_object_grip() {
--- a/devtools/server/tests/unit/test_objectgrips-11.js
+++ b/devtools/server/tests/unit/test_objectgrips-11.js
@@ -14,17 +14,17 @@ function run_test() {
   gDebuggee = addTestGlobal("test-grips");
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_object_grip();
                            });
   });
   do_test_pending();
 }
 
 function test_object_grip() {
--- a/devtools/server/tests/unit/test_objectgrips-12.js
+++ b/devtools/server/tests/unit/test_objectgrips-12.js
@@ -17,17 +17,17 @@ function run_test() {
   gDebuggee = addTestGlobal("test-grips");
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_display_string();
                            });
   });
   do_test_pending();
 }
 
 function test_display_string() {
--- a/devtools/server/tests/unit/test_objectgrips-13.js
+++ b/devtools/server/tests/unit/test_objectgrips-13.js
@@ -15,17 +15,17 @@ function run_test() {
   gDebuggee = addTestGlobal("test-grips");
   Cu.evalInSandbox(function stopMe() {
     debugger;
   }.toString(), gDebuggee);
 
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              add_pause_listener();
                            });
   });
   do_test_pending();
 }
 
 function add_pause_listener() {
--- a/devtools/server/tests/unit/test_objectgrips-14.js
+++ b/devtools/server/tests/unit/test_objectgrips-14.js
@@ -12,17 +12,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-object-grip");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-object-grip",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              testObjectGroup();
                            });
   });
   do_test_pending();
 }
 
 function evalCode() {
--- a/devtools/server/tests/unit/test_objectgrips-15.js
+++ b/devtools/server/tests/unit/test_objectgrips-15.js
@@ -12,17 +12,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-object-grip");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-object-grip",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              testObjectGroup();
                            });
   });
   do_test_pending();
 }
 
 function evalCode() {
--- a/devtools/server/tests/unit/test_pause_exceptions-01.js
+++ b/devtools/server/tests/unit/test_pause_exceptions-01.js
@@ -14,17 +14,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
--- a/devtools/server/tests/unit/test_pause_exceptions-02.js
+++ b/devtools/server/tests/unit/test_pause_exceptions-02.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
--- a/devtools/server/tests/unit/test_pauselifetime-01.js
+++ b/devtools/server/tests/unit/test_pauselifetime-01.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
--- a/devtools/server/tests/unit/test_pauselifetime-02.js
+++ b/devtools/server/tests/unit/test_pauselifetime-02.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
--- a/devtools/server/tests/unit/test_pauselifetime-03.js
+++ b/devtools/server/tests/unit/test_pauselifetime-03.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
--- a/devtools/server/tests/unit/test_pauselifetime-04.js
+++ b/devtools/server/tests/unit/test_pauselifetime-04.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-stack",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_pause_frame();
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
--- a/devtools/server/tests/unit/test_promise_state-01.js
+++ b/devtools/server/tests/unit/test_promise_state-01.js
@@ -11,17 +11,17 @@
 
 function run_test() {
   initTestDebuggerServer();
   const debuggee = addTestGlobal("test-promise-state");
   const client = new DebuggerClient(DebuggerServer.connectPipe());
   client.connect().then(function() {
     attachTestTabAndResume(
       client, "test-promise-state",
-      function(response, tabClient, threadClient) {
+      function(response, targetFront, threadClient) {
         (async function() {
           const packet = await executeOnNextTickAndWaitForPause(
             () => evalCode(debuggee), client);
 
           const grip = packet.frame.environment.bindings.variables.p;
           ok(grip.value.preview);
           equal(grip.value.class, "Promise");
           equal(grip.value.promiseState.state, "pending");
--- a/devtools/server/tests/unit/test_promise_state-02.js
+++ b/devtools/server/tests/unit/test_promise_state-02.js
@@ -11,17 +11,17 @@
 
 function run_test() {
   initTestDebuggerServer();
   const debuggee = addTestGlobal("test-promise-state");
   const client = new DebuggerClient(DebuggerServer.connectPipe());
   client.connect().then(function() {
     attachTestTabAndResume(
       client, "test-promise-state",
-      function(response, tabClient, threadClient) {
+      function(response, targetFront, threadClient) {
         (async function() {
           const packet = await executeOnNextTickAndWaitForPause(
             () => evalCode(debuggee), client);
 
           const grip = packet.frame.environment.bindings.variables.p;
           ok(grip.value.preview);
           equal(grip.value.class, "Promise");
           equal(grip.value.promiseState.state, "fulfilled");
--- a/devtools/server/tests/unit/test_promise_state-03.js
+++ b/devtools/server/tests/unit/test_promise_state-03.js
@@ -11,17 +11,17 @@
 
 function run_test() {
   initTestDebuggerServer();
   const debuggee = addTestGlobal("test-promise-state");
   const client = new DebuggerClient(DebuggerServer.connectPipe());
   client.connect().then(function() {
     attachTestTabAndResume(
       client, "test-promise-state",
-      function(response, tabClient, threadClient) {
+      function(response, targetFront, threadClient) {
         (async function() {
           const packet = await executeOnNextTickAndWaitForPause(
             () => evalCode(debuggee), client);
 
           const grip = packet.frame.environment.bindings.variables.p;
           ok(grip.value.preview);
           equal(grip.value.class, "Promise");
           equal(grip.value.promiseState.state, "rejected");
--- a/devtools/server/tests/unit/test_reattach-thread.js
+++ b/devtools/server/tests/unit/test_reattach-thread.js
@@ -2,54 +2,54 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 /**
  * Test that reattaching to a previously detached thread works.
  */
 
-var gClient, gDebuggee, gThreadClient, gTabClient;
+var gClient, gDebuggee, gThreadClient, gTargetFront;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = testGlobal("test-reattach");
   DebuggerServer.addTestGlobal(gDebuggee);
 
   const transport = DebuggerServer.connectPipe();
   gClient = new DebuggerClient(transport);
   gClient.connect().then(() => {
-    attachTestTab(gClient, "test-reattach", (reply, tabClient) => {
-      gTabClient = tabClient;
+    attachTestTab(gClient, "test-reattach", (reply, targetFront) => {
+      gTargetFront = targetFront;
       test_attach();
     });
   });
   do_test_pending();
 }
 
 function test_attach() {
-  gTabClient.attachThread({}).then(([response, threadClient]) => {
+  gTargetFront.attachThread({}).then(([response, threadClient]) => {
     Assert.equal(threadClient.state, "paused");
     gThreadClient = threadClient;
     threadClient.resume(test_detach);
   });
 }
 
 function test_detach() {
   gThreadClient.detach(() => {
     Assert.equal(gThreadClient.state, "detached");
-    Assert.equal(gTabClient.thread, null);
+    Assert.equal(gTargetFront.thread, null);
     test_reattach();
   });
 }
 
 function test_reattach() {
-  gTabClient.attachThread({}).then(([response, threadClient]) => {
+  gTargetFront.attachThread({}).then(([response, threadClient]) => {
     Assert.notEqual(gThreadClient, threadClient);
     Assert.equal(threadClient.state, "paused");
-    Assert.equal(gTabClient.thread, threadClient);
+    Assert.equal(gTargetFront.thread, threadClient);
     threadClient.resume(cleanup);
   });
 }
 
 function cleanup() {
   gClient.close().then(do_test_finished);
 }
--- a/devtools/server/tests/unit/test_setBreakpoint-at-the-beginning-of-a-line.js
+++ b/devtools/server/tests/unit/test_setBreakpoint-at-the-beginning-of-a-line.js
@@ -10,18 +10,18 @@ async function run_test() {
   const global = createTestGlobal("test");
   DebuggerServer.addTestGlobal(global);
 
   const client = new DebuggerClient(DebuggerServer.connectPipe());
   await connect(client);
 
   const { tabs } = await listTabs(client);
   const tab = findTab(tabs, "test");
-  const [, tabClient] = await attachTarget(client, tab);
-  const [, threadClient] = await attachThread(tabClient);
+  const [, targetFront] = await attachTarget(client, tab);
+  const [, threadClient] = await attachThread(targetFront);
   await resume(threadClient);
 
   const promise = waitForNewSource(threadClient, SOURCE_URL);
   loadSubScript(SOURCE_URL, global);
   const { source } = await promise;
   const sourceClient = threadClient.source(source);
 
   const location = { line: 4, column: 2 };
--- a/devtools/server/tests/unit/test_setBreakpoint-at-the-end-of-a-line.js
+++ b/devtools/server/tests/unit/test_setBreakpoint-at-the-end-of-a-line.js
@@ -12,18 +12,18 @@ async function run_test() {
   const global = createTestGlobal("test");
   DebuggerServer.addTestGlobal(global);
 
   const client = new DebuggerClient(DebuggerServer.connectPipe());
   await connect(client);
 
   const { tabs } = await listTabs(client);
   const tab = findTab(tabs, "test");
-  const [, tabClient] = await attachTarget(client, tab);
-  const [, threadClient] = await attachThread(tabClient);
+  const [, targetFront] = await attachTarget(client, tab);
+  const [, threadClient] = await attachThread(targetFront);
   await resume(threadClient);
 
   const promise = waitForNewSource(threadClient, SOURCE_URL);
   loadSubScript(SOURCE_URL, global);
   const { source } = await promise;
   const sourceClient = threadClient.source(source);
 
   const location = { line: 4, column: 42 };
--- a/devtools/server/tests/unit/test_setBreakpoint-on-column-in-gcd-script.js
+++ b/devtools/server/tests/unit/test_setBreakpoint-on-column-in-gcd-script.js
@@ -14,31 +14,31 @@ function run_test() {
     DebuggerServer.setRootActor(createRootActor);
     DebuggerServer.init(() => true);
     DebuggerServer.addTestGlobal(global);
     const client = new DebuggerClient(DebuggerServer.connectPipe());
     await connect(client);
 
     const { tabs } = await listTabs(client);
     const tab = findTab(tabs, "test");
-    const [, tabClient] = await attachTarget(client, tab);
-    const [, threadClient] = await attachThread(tabClient);
+    const [, targetFront] = await attachTarget(client, tab);
+    const [, threadClient] = await attachThread(targetFront);
     await resume(threadClient);
 
     const { sources } = await getSources(threadClient);
     const source = findSource(sources, SOURCE_URL);
     const sourceClient = threadClient.source(source);
 
     const location = { line: 6, column: 17 };
     let [packet, breakpointClient] = await setBreakpoint(sourceClient, location);
     Assert.ok(packet.isPending);
     Assert.equal(false, "actualLocation" in packet);
 
     packet = await executeOnNextTickAndWaitForPause(function() {
-      reload(tabClient).then(function() {
+      reload(targetFront).then(function() {
         loadSubScriptWithOptions(SOURCE_URL, {target: global, ignoreCache: true});
       });
     }, client);
     Assert.equal(packet.type, "paused");
     const why = packet.why;
     Assert.equal(why.type, "breakpoint");
     Assert.equal(why.actors.length, 1);
     Assert.equal(why.actors[0], breakpointClient.actor);
--- a/devtools/server/tests/unit/test_setBreakpoint-on-column.js
+++ b/devtools/server/tests/unit/test_setBreakpoint-on-column.js
@@ -13,18 +13,18 @@ function run_test() {
     const global = createTestGlobal("test");
     DebuggerServer.addTestGlobal(global);
 
     const client = new DebuggerClient(DebuggerServer.connectPipe());
     await connect(client);
 
     const { tabs } = await listTabs(client);
     const tab = findTab(tabs, "test");
-    const [, tabClient] = await attachTarget(client, tab);
-    const [, threadClient] = await attachThread(tabClient);
+    const [, targetFront] = await attachTarget(client, tab);
+    const [, threadClient] = await attachThread(targetFront);
     await resume(threadClient);
 
     const promise = waitForNewSource(threadClient, SOURCE_URL);
     loadSubScript(SOURCE_URL, global);
     const { source } = await promise;
     const sourceClient = threadClient.source(source);
 
     const location = { line: 4, column: 17 };
--- a/devtools/server/tests/unit/test_setBreakpoint-on-line-in-gcd-script.js
+++ b/devtools/server/tests/unit/test_setBreakpoint-on-line-in-gcd-script.js
@@ -14,31 +14,31 @@ function run_test() {
     DebuggerServer.setRootActor(createRootActor);
     DebuggerServer.init(() => true);
     DebuggerServer.addTestGlobal(global);
     const client = new DebuggerClient(DebuggerServer.connectPipe());
     await connect(client);
 
     const { tabs } = await listTabs(client);
     const tab = findTab(tabs, "test");
-    const [, tabClient] = await attachTarget(client, tab);
-    const [, threadClient] = await attachThread(tabClient);
+    const [, targetFront] = await attachTarget(client, tab);
+    const [, threadClient] = await attachThread(targetFront);
     await resume(threadClient);
 
     const { sources } = await getSources(threadClient);
     const source = findSource(sources, SOURCE_URL);
     const sourceClient = threadClient.source(source);
 
     const location = { line: 7 };
     let [packet, breakpointClient] = await setBreakpoint(sourceClient, location);
     Assert.ok(packet.isPending);
     Assert.equal(false, "actualLocation" in packet);
 
     packet = await executeOnNextTickAndWaitForPause(function() {
-      reload(tabClient).then(function() {
+      reload(targetFront).then(function() {
         loadSubScriptWithOptions(SOURCE_URL, {target: global, ignoreCache: true});
       });
     }, client);
     Assert.equal(packet.type, "paused");
     const why = packet.why;
     Assert.equal(why.type, "breakpoint");
     Assert.equal(why.actors.length, 1);
     Assert.equal(why.actors[0], breakpointClient.actor);
--- a/devtools/server/tests/unit/test_setBreakpoint-on-line-with-multiple-offsets.js
+++ b/devtools/server/tests/unit/test_setBreakpoint-on-line-with-multiple-offsets.js
@@ -13,19 +13,19 @@ function run_test() {
     const global = createTestGlobal("test");
     DebuggerServer.addTestGlobal(global);
 
     const client = new DebuggerClient(DebuggerServer.connectPipe());
     await connect(client);
 
     const { tabs } = await listTabs(client);
     const tab = findTab(tabs, "test");
-    const [, tabClient] = await attachTarget(client, tab);
+    const [, targetFront] = await attachTarget(client, tab);
 
-    const [, threadClient] = await attachThread(tabClient);
+    const [, threadClient] = await attachThread(targetFront);
     await resume(threadClient);
 
     const promise = waitForNewSource(threadClient, SOURCE_URL);
     loadSubScript(SOURCE_URL, global);
     const { source } = await promise;
     const sourceClient = threadClient.source(source);
 
     const location = { line: 4 };
--- a/devtools/server/tests/unit/test_setBreakpoint-on-line-with-multiple-statements.js
+++ b/devtools/server/tests/unit/test_setBreakpoint-on-line-with-multiple-statements.js
@@ -13,19 +13,19 @@ function run_test() {
     const global = createTestGlobal("test");
     DebuggerServer.addTestGlobal(global);
 
     const client = new DebuggerClient(DebuggerServer.connectPipe());
     await connect(client);
 
     const { tabs } = await listTabs(client);
     const tab = findTab(tabs, "test");
-    const [, tabClient] = await attachTarget(client, tab);
+    const [, targetFront] = await attachTarget(client, tab);
 
-    const [, threadClient] = await attachThread(tabClient);
+    const [, threadClient] = await attachThread(targetFront);
     await resume(threadClient);
 
     const promise = waitForNewSource(threadClient, SOURCE_URL);
     loadSubScript(SOURCE_URL, global);
     const { source } = await promise;
     const sourceClient = threadClient.source(source);
 
     const location = { line: 4 };
--- a/devtools/server/tests/unit/test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js
+++ b/devtools/server/tests/unit/test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js
@@ -15,31 +15,31 @@ function run_test() {
     DebuggerServer.init(() => true);
     DebuggerServer.addTestGlobal(global);
 
     const client = new DebuggerClient(DebuggerServer.connectPipe());
     await connect(client);
 
     const { tabs } = await listTabs(client);
     const tab = findTab(tabs, "test");
-    const [, tabClient] = await attachTarget(client, tab);
-    const [, threadClient] = await attachThread(tabClient);
+    const [, targetFront] = await attachTarget(client, tab);
+    const [, threadClient] = await attachThread(targetFront);
     await resume(threadClient);
 
     const { sources } = await getSources(threadClient);
     const source = findSource(sources, SOURCE_URL);
     const sourceClient = threadClient.source(source);
 
     const location = { line: 7 };
     let [packet, breakpointClient] = await setBreakpoint(sourceClient, location);
     Assert.ok(packet.isPending);
     Assert.equal(false, "actualLocation" in packet);
 
     packet = await executeOnNextTickAndWaitForPause(function() {
-      reload(tabClient).then(function() {
+      reload(targetFront).then(function() {
         loadSubScriptWithOptions(SOURCE_URL, {target: global, ignoreCache: true});
       });
     }, client);
     Assert.equal(packet.type, "paused");
     const why = packet.why;
     Assert.equal(why.type, "breakpoint");
     Assert.equal(why.actors.length, 1);
     Assert.equal(why.actors[0], breakpointClient.actor);
--- a/devtools/server/tests/unit/test_setBreakpoint-on-line-with-no-offsets.js
+++ b/devtools/server/tests/unit/test_setBreakpoint-on-line-with-no-offsets.js
@@ -13,18 +13,18 @@ function run_test() {
     const global = createTestGlobal("test");
     DebuggerServer.addTestGlobal(global);
 
     const client = new DebuggerClient(DebuggerServer.connectPipe());
     await connect(client);
 
     const { tabs } = await listTabs(client);
     const tab = findTab(tabs, "test");
-    const [, tabClient] = await attachTarget(client, tab);
-    const [, threadClient] = await attachThread(tabClient);
+    const [, targetFront] = await attachTarget(client, tab);
+    const [, threadClient] = await attachThread(targetFront);
     await resume(threadClient);
 
     const promise = waitForNewSource(threadClient, SOURCE_URL);
     loadSubScript(SOURCE_URL, global);
     const { source } = await promise;
     const sourceClient = threadClient.source(source);
 
     const location = { line: 5 };
--- a/devtools/server/tests/unit/test_setBreakpoint-on-line.js
+++ b/devtools/server/tests/unit/test_setBreakpoint-on-line.js
@@ -13,19 +13,19 @@ function run_test() {
     const global = createTestGlobal("test");
     DebuggerServer.addTestGlobal(global);
 
     const client = new DebuggerClient(DebuggerServer.connectPipe());
     await connect(client);
 
     const { tabs } = await listTabs(client);
     const tab = findTab(tabs, "test");
-    const [, tabClient] = await attachTarget(client, tab);
+    const [, targetFront] = await attachTarget(client, tab);
 
-    const [, threadClient] = await attachThread(tabClient);
+    const [, threadClient] = await attachThread(targetFront);
     await resume(threadClient);
 
     const promise = waitForNewSource(threadClient, SOURCE_URL);
     loadSubScript(SOURCE_URL, global);
     const { source } = await promise;
     const sourceClient = threadClient.source(source);
 
     const location = { line: 5 };
--- a/devtools/server/tests/unit/test_source-01.js
+++ b/devtools/server/tests/unit/test_source-01.js
@@ -23,17 +23,17 @@ function run_test() {
     gDebuggee,
     "1.8",
     getFileUrl("test_source-01.js")
   );
 
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_source();
                            });
   });
   do_test_pending();
 }
 
 const SOURCE_URL = "http://example.com/foobar.js";
--- a/devtools/server/tests/unit/test_sourcemaps-01.js
+++ b/devtools/server/tests/unit/test_sourcemaps-01.js
@@ -14,17 +14,17 @@ var gThreadClient;
 const {SourceNode} = require("source-map");
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-source-map");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-source-map",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_simple_source_map();
                            });
   });
   do_test_pending();
 }
 
 function test_simple_source_map() {
--- a/devtools/server/tests/unit/test_sourcemaps-02.js
+++ b/devtools/server/tests/unit/test_sourcemaps-02.js
@@ -14,17 +14,17 @@ var gThreadClient;
 const {SourceNode} = require("source-map");
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-source-map");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-source-map",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_simple_source_map();
                            });
   });
   do_test_pending();
 }
 
 function test_simple_source_map() {
--- a/devtools/server/tests/unit/test_sourcemaps-03.js
+++ b/devtools/server/tests/unit/test_sourcemaps-03.js
@@ -14,17 +14,17 @@ var gThreadClient;
 const {SourceNode} = require("source-map");
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-source-map");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-source-map",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_simple_source_map();
                            });
   });
   do_test_pending();
 }
 
 function testBreakpointMapping(name, callback) {
--- a/devtools/server/tests/unit/test_sourcemaps-04.js
+++ b/devtools/server/tests/unit/test_sourcemaps-04.js
@@ -12,17 +12,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-source-map");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-source-map",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_absolute_source_map();
                            });
   });
   do_test_pending();
 }
 
 function test_absolute_source_map() {
--- a/devtools/server/tests/unit/test_sourcemaps-05.js
+++ b/devtools/server/tests/unit/test_sourcemaps-05.js
@@ -12,17 +12,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-source-map");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-source-map",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_relative_source_map();
                            });
   });
   do_test_pending();
 }
 
 function test_relative_source_map() {
--- a/devtools/server/tests/unit/test_sourcemaps-06.js
+++ b/devtools/server/tests/unit/test_sourcemaps-06.js
@@ -15,17 +15,17 @@ var gThreadClient;
 const {SourceNode} = require("source-map");
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-source-map");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-source-map",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_source_content();
                            });
   });
   do_test_pending();
 }
 
 function test_source_content() {
--- a/devtools/server/tests/unit/test_sourcemaps-07.js
+++ b/devtools/server/tests/unit/test_sourcemaps-07.js
@@ -15,17 +15,17 @@ var gThreadClient;
 const {SourceNode} = require("source-map");
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-source-map");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-source-map",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_cached_original_sources();
                            });
   });
   do_test_pending();
 }
 
 function test_cached_original_sources() {
--- a/devtools/server/tests/unit/test_sourcemaps-08.js
+++ b/devtools/server/tests/unit/test_sourcemaps-08.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-source-map");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-source-map",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_source_maps();
                            });
   });
   do_test_pending();
 }
 
 function test_source_maps() {
--- a/devtools/server/tests/unit/test_sourcemaps-09.js
+++ b/devtools/server/tests/unit/test_sourcemaps-09.js
@@ -12,17 +12,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-source-map");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-source-map",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_minified();
                            });
   });
   do_test_pending();
 }
 
 function test_minified() {
--- a/devtools/server/tests/unit/test_sourcemaps-10.js
+++ b/devtools/server/tests/unit/test_sourcemaps-10.js
@@ -15,17 +15,17 @@ const {SourceNode} = require("source-map
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-source-map");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(
       gClient, "test-source-map",
-      function(response, tabClient, threadClient) {
+      function(response, targetFront, threadClient) {
         gThreadClient = threadClient;
         Promise.resolve(define_code())
           .then(run_code)
           .then(test_frame_location)
           .catch(error => {
             dump(error + "\n");
             dump(error.stack);
             Assert.ok(false);
--- a/devtools/server/tests/unit/test_sourcemaps-11.js
+++ b/devtools/server/tests/unit/test_sourcemaps-11.js
@@ -15,17 +15,17 @@ const {SourceNode} = require("source-map
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-source-map");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(
       gClient, "test-source-map",
-      function(response, tabClient, threadClient) {
+      function(response, targetFront, threadClient) {
         gThreadClient = threadClient;
         Promise.resolve(define_code())
           .then(run_code)
           .then(test_frames)
           .catch(error => {
             dump(error + "\n");
             dump(error.stack);
             Assert.ok(false);
--- a/devtools/server/tests/unit/test_sourcemaps-12.js
+++ b/devtools/server/tests/unit/test_sourcemaps-12.js
@@ -15,17 +15,17 @@ var gThreadClient;
 const {SourceNode} = require("source-map");
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-source-map");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-source-map",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              define_code();
                            });
   });
   do_test_pending();
 }
 
 function define_code() {
--- a/devtools/server/tests/unit/test_sourcemaps-13.js
+++ b/devtools/server/tests/unit/test_sourcemaps-13.js
@@ -5,29 +5,29 @@
 
 /**
  * Test that we don't permanently cache source maps across reloads.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
-var gTabClient;
+var gTargetFront;
 
 const {SourceNode} = require("source-map");
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-source-map");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-source-map",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
-                             gTabClient = tabClient;
+                             gTargetFront = targetFront;
                              setup_code();
                            });
   });
   do_test_pending();
 }
 
 // The MAP_FILE_NAME is .txt so that the OS will definitely have an extension ->
 // content type mapping for the extension. If it doesn't (like .map or .json),
@@ -59,17 +59,17 @@ function setup_code() {
 }
 
 function test_initial_sources() {
   gThreadClient.getSources(function({ error, sources }) {
     Assert.ok(!error);
     sources = sources.filter(source => source.url);
     Assert.equal(sources.length, 1);
     Assert.equal(sources[0].url, getFileUrl(TEMP_FILE_1, true));
-    reload(gTabClient).then(setup_new_code);
+    reload(gTargetFront).then(setup_new_code);
   });
 }
 
 function setup_new_code() {
   const node = new SourceNode(1, 0,
                             getFileUrl(TEMP_FILE_2, true),
                             "function temporary2() {}\n");
   let { code, map } = node.toStringWithSourceMap({
--- a/devtools/server/tests/unit/test_sourcemaps-16.js
+++ b/devtools/server/tests/unit/test_sourcemaps-16.js
@@ -16,17 +16,17 @@ function run_test() {
   gDebuggee = addTestGlobal("test-sourcemaps");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestThread(gClient, "test-sourcemaps", testSourcemap);
   });
   do_test_pending();
 }
 
-const testSourcemap = async function(threadResponse, tabClient, threadClient,
+const testSourcemap = async function(threadResponse, targetFront, threadClient,
   tabResponse) {
   evalTestCode();
 
   const { sources } = await getSources(threadClient);
 
   for (const form of sources) {
     const sourceResponse = await getSourceContent(threadClient.source(form));
     ok(sourceResponse, "Should be able to get the source response");
--- a/devtools/server/tests/unit/test_sourcemaps-17.js
+++ b/devtools/server/tests/unit/test_sourcemaps-17.js
@@ -15,17 +15,17 @@ var gThreadClient;
 const {SourceNode} = require("source-map");
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-source-map");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-source-map",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_source_map();
                            });
   });
   do_test_pending();
 }
 
 function test_source_map() {
--- a/devtools/server/tests/unit/test_symbols-01.js
+++ b/devtools/server/tests/unit/test_symbols-01.js
@@ -11,17 +11,17 @@ const URL = "foo.js";
 
 function run_test() {
   initTestDebuggerServer();
   const debuggee = addTestGlobal("test-symbols");
   const client = new DebuggerClient(DebuggerServer.connectPipe());
 
   client.connect().then(function() {
     attachTestTabAndResume(client, "test-symbols",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              add_task(testSymbols.bind(null, client, debuggee));
                              run_next_test();
                            });
   });
 
   do_test_pending();
 }
 
--- a/devtools/server/tests/unit/test_symbols-02.js
+++ b/devtools/server/tests/unit/test_symbols-02.js
@@ -11,17 +11,17 @@ const URL = "foo.js";
 
 function run_test() {
   initTestDebuggerServer();
   const debuggee = addTestGlobal("test-symbols");
   const client = new DebuggerClient(DebuggerServer.connectPipe());
 
   client.connect().then(function() {
     attachTestTabAndResume(client, "test-symbols",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              add_task(testSymbols.bind(null, client, debuggee));
                              run_next_test();
                            });
   });
 
   do_test_pending();
 }
 
--- a/devtools/server/tests/unit/test_threadlifetime-01.js
+++ b/devtools/server/tests/unit/test_threadlifetime-01.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-grips");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_thread_lifetime();
                            });
   });
   do_test_pending();
 }
 
 function test_thread_lifetime() {
--- a/devtools/server/tests/unit/test_threadlifetime-02.js
+++ b/devtools/server/tests/unit/test_threadlifetime-02.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-grips");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_thread_lifetime();
                            });
   });
   do_test_pending();
 }
 
 function test_thread_lifetime() {
--- a/devtools/server/tests/unit/test_threadlifetime-03.js
+++ b/devtools/server/tests/unit/test_threadlifetime-03.js
@@ -13,17 +13,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-grips");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_thread_lifetime();
                            });
   });
   do_test_pending();
 }
 
 function test_thread_lifetime() {
--- a/devtools/server/tests/unit/test_threadlifetime-04.js
+++ b/devtools/server/tests/unit/test_threadlifetime-04.js
@@ -14,17 +14,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-grips");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_thread_lifetime();
                            });
   });
   do_test_pending();
 }
 
 function test_thread_lifetime() {
--- a/devtools/server/tests/unit/test_threadlifetime-05.js
+++ b/devtools/server/tests/unit/test_threadlifetime-05.js
@@ -14,17 +14,17 @@ var gThreadClient;
 var gPauseGrip;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-grips");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_thread_lifetime();
                            });
   });
   do_test_pending();
 }
 
 function arg_grips(frameArgs, onResponse) {
--- a/devtools/server/tests/unit/test_threadlifetime-06.js
+++ b/devtools/server/tests/unit/test_threadlifetime-06.js
@@ -14,17 +14,17 @@ var gClient;
 var gThreadClient;
 
 function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-grips");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     attachTestTabAndResume(gClient, "test-grips",
-                           function(response, tabClient, threadClient) {
+                           function(response, targetFront, threadClient) {
                              gThreadClient = threadClient;
                              test_thread_lifetime();
                            });
   });
   do_test_pending();
 }
 
 function test_thread_lifetime() {
--- a/devtools/server/tests/unit/test_wasm_source-01.js
+++ b/devtools/server/tests/unit/test_wasm_source-01.js
@@ -22,17 +22,17 @@ function run_test() {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-wasm-source");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect().then(function() {
     Assert.ok(gClient.mainRoot.traits.wasmBinarySource);
 
     attachTestTabAndResume(
       gClient, "test-wasm-source",
-      function(response, tabClient, threadClient) {
+      function(response, targetFront, threadClient) {
         gThreadClient = threadClient;
         gThreadClient.reconfigure({
           observeAsmJS: true,
           wasmBinarySource: true
         }, function(response) {
           Assert.equal(!!response.error, false);
           test_source();
         });
--- a/devtools/shared/webconsole/test/common.js
+++ b/devtools/shared/webconsole/test/common.js
@@ -89,35 +89,35 @@ var _attachConsole = async function(
   response = await state.dbgClient.listTabs();
   if (response.error) {
     console.error("listTabs failed: " + response.error + " " +
                   response.message);
     callback(state, response);
     return;
   }
   const tab = response.tabs[response.selected];
-  const [, tabClient] = await state.dbgClient.attachTarget(tab.actor);
+  const [, targetFront] = await state.dbgClient.attachTarget(tab.actor);
   if (attachToWorker) {
     const workerName = "console-test-worker.js#" + new Date().getTime();
     const worker = new Worker(workerName);
     // Keep a strong reference to the Worker to avoid it being
     // GCd during the test (bug 1237492).
     // eslint-disable-next-line camelcase
     state._worker_ref = worker;
     await waitForMessage(worker);
 
-    const { workers } = await tabClient.listWorkers();
+    const { workers } = await targetFront.listWorkers();
     const workerTargetActor = workers.filter(w => w.url == workerName)[0].actor;
     if (!workerTargetActor) {
       console.error("listWorkers failed. Unable to find the " +
                     "worker actor\n");
       return;
     }
     const [workerResponse, workerClient] =
-      await tabClient.attachWorker(workerTargetActor);
+      await targetFront.attachWorker(workerTargetActor);
     if (!workerClient || workerResponse.error) {
       console.error("attachWorker failed. No worker client or " +
                     " error: " + workerResponse.error);
       return;
     }
     await workerClient.attachThread({});
     state.actor = workerClient.consoleActor;
     state.dbgClient.attachConsole(workerClient.consoleActor, listeners)
--- a/dom/base/nsGlobalWindowInner.h
+++ b/dom/base/nsGlobalWindowInner.h
@@ -811,16 +811,29 @@ public:
   {
     return GetScrollX(aError);
   }
   double GetScrollY(mozilla::ErrorResult& aError);
   double GetPageYOffset(mozilla::ErrorResult& aError)
   {
     return GetScrollY(aError);
   }
+
+  int32_t GetScreenLeft(mozilla::dom::CallerType aCallerType,
+                        mozilla::ErrorResult& aError)
+  {
+    return GetScreenX(aCallerType, aError);
+  }
+
+  int32_t GetScreenTop(mozilla::dom::CallerType aCallerType,
+                       mozilla::ErrorResult& aError)
+  {
+    return GetScreenY(aCallerType, aError);
+  }
+
   void GetScreenX(JSContext* aCx, JS::MutableHandle<JS::Value> aValue,
                   mozilla::dom::CallerType aCallerType,
                   mozilla::ErrorResult& aError);
   void SetScreenX(JSContext* aCx, JS::Handle<JS::Value> aValue,
                   mozilla::dom::CallerType aCallerType,
                   mozilla::ErrorResult& aError);
   void GetScreenY(JSContext* aCx, JS::MutableHandle<JS::Value> aValue,
                   mozilla::dom::CallerType aCallerType,
new file mode 100644
--- /dev/null
+++ b/dom/canvas/CacheInvalidator.cpp
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 13; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=13 sts=4 et sw=4 tw=90: */
+/* 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/. */
+
+#include "CacheInvalidator.h"
+
+namespace mozilla {
+
+void
+CacheInvalidator::InvalidateCaches() const
+{
+    // The only sane approach is to require caches to remove invalidators.
+    while (mCaches.size()) {
+        const auto& itr = mCaches.begin();
+        const auto pEntry = *itr;
+        pEntry->OnInvalidate();
+        MOZ_ASSERT(mCaches.find(pEntry) == mCaches.end());
+    }
+}
+
+// -
+
+AbstractCache::InvalidatorListT
+AbstractCache::ResetInvalidators(InvalidatorListT&& newList)
+{
+    for (const auto& cur : mInvalidators) {
+        if (cur) {
+            (void)cur->mCaches.erase(this);
+        }
+    }
+
+    auto ret = std::move(mInvalidators);
+    mInvalidators = std::move(newList);
+
+    for (const auto& cur : mInvalidators) {
+        // Don't assert that we insert, since there may be dupes in `invalidators`.
+        // (and it's not worth removing the dupes)
+        if (cur) {
+            (void)cur->mCaches.insert(this);
+        }
+    }
+
+    return ret;
+}
+
+void
+AbstractCache::AddInvalidator(const CacheInvalidator& x)
+{
+    mInvalidators.push_back(&x);
+    x.mCaches.insert(this);
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/canvas/CacheInvalidator.h
@@ -0,0 +1,206 @@
+/* -*- Mode: C++; tab-width: 13; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=13 sts=4 et sw=4 tw=90: */
+/* 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/. */
+
+#ifndef MOZILLA_CACHE_INVALIDATOR_H_
+#define MOZILLA_CACHE_INVALIDATOR_H_
+
+#include "mozilla/Maybe.h"
+#include "mozilla/UniquePtr.h"
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+namespace std { // You know it's going to be good with this at the top of the file.
+
+// The STL is lazy and doesn't provide these:
+template<typename T>
+struct hash<const T*>
+{
+    auto operator()(const T* const x) const {
+        return hash<T*>()(const_cast<T*>(x));
+    }
+};
+
+template<typename T>
+struct hash<const T>
+{
+    auto operator()(const T x) const {
+        return hash<T>()(const_cast<T>(x));
+    }
+};
+
+} // namespace std
+
+// -
+
+namespace mozilla {
+
+class AbstractCache;
+
+// -
+
+class CacheInvalidator
+{
+    friend class AbstractCache;
+private:
+    mutable std::unordered_set<AbstractCache*> mCaches;
+
+public:
+    virtual ~CacheInvalidator() {
+        // It's actually generally unsafe to wait until now to invalidate caches, because
+        // when used as a mixin, this dtor is called after the dtor for the derived class.
+        // This means that if the derived class holds a cache (or is a cache!),
+        // OnInvalidate() will be called on a destroyed object.
+        //MOZ_ASSERT(!mCaches);
+        InvalidateCaches();
+    }
+
+    void InvalidateCaches() const;
+};
+
+// -
+
+class AbstractCache
+{
+    typedef std::vector<const CacheInvalidator*> InvalidatorListT;
+private:
+    InvalidatorListT mInvalidators;
+
+public:
+    AbstractCache() = default;
+
+    explicit AbstractCache(InvalidatorListT&& invalidators) {
+        ResetInvalidators(std::move(invalidators));
+    }
+
+    virtual ~AbstractCache() {
+        ResetInvalidators({});
+    }
+
+public:
+    virtual void OnInvalidate() = 0;
+
+    InvalidatorListT ResetInvalidators(InvalidatorListT&&); // Returns the old list.
+    void AddInvalidator(const CacheInvalidator&);
+};
+
+// -
+
+template<typename T>
+class CacheMaybe : public AbstractCache
+{
+    Maybe<T> mVal;
+
+public:
+    template<typename U>
+    CacheMaybe& operator=(Maybe<U>&& rhs) {
+        mVal.reset();
+        if (rhs) {
+            mVal.emplace(std::move(rhs.ref()));
+        }
+        return *this;
+    }
+
+    CacheMaybe& operator=(Nothing) {
+        return *this = Maybe<T>();
+    }
+
+    void OnInvalidate() override {
+        *this = Nothing();
+        ResetInvalidators({});
+    }
+
+    explicit operator bool() const { return bool(mVal); }
+    T* get() const { return mVal.ptrOr(nullptr); }
+    T* operator->() const { return get(); }
+};
+
+// -
+
+template<typename KeyT, typename ValueT>
+class CacheWeakMap final
+{
+    class Entry final : public AbstractCache {
+    public:
+        CacheWeakMap& mParent;
+        const KeyT mKey;
+        const ValueT mValue;
+
+        Entry(CacheWeakMap& parent, const KeyT& key, ValueT&& value)
+            : mParent(parent)
+            , mKey(key)
+            , mValue(value)
+        { }
+
+        void OnInvalidate() override {
+            const auto erased = mParent.mMap.erase(&mKey);
+            MOZ_ALWAYS_TRUE( erased == 1 );
+        }
+    };
+
+    struct DerefHash final {
+        size_t operator ()(const KeyT* const a) const {
+            return std::hash<const KeyT>()(*a);
+        }
+    };
+    struct DerefEqual final {
+        bool operator ()(const KeyT* const a, const KeyT* const b) const {
+            return *a == *b;
+        }
+    };
+
+    typedef std::unordered_map<const KeyT*, UniquePtr<Entry>, DerefHash,
+                               DerefEqual> MapT;
+    MapT mMap;
+
+public:
+    UniquePtr<Entry> MakeEntry(const KeyT& key, ValueT&& value) {
+        return UniquePtr<Entry>( new Entry(*this, key, std::move(value)) );
+    }
+    UniquePtr<Entry> MakeEntry(const KeyT& key, const ValueT& value) {
+        return MakeEntry(key, ValueT(value));
+    }
+
+    const ValueT* Insert(UniquePtr<Entry>&& entry)
+    {
+        auto insertable = typename MapT::value_type{
+            &entry->mKey,
+            std::move(entry)
+        };
+
+        const auto res = mMap.insert(std::move(insertable));
+        const auto& didInsert = res.second;
+        MOZ_ALWAYS_TRUE( didInsert );
+
+        const auto& itr = res.first;
+        return &itr->second->mValue;
+    }
+
+    const ValueT* Find(const KeyT& key) const {
+        const auto itr = mMap.find(&key);
+        if (itr == mMap.end())
+            return nullptr;
+
+        return &itr->second->mValue;
+    }
+
+    void Clear() const {
+        while (true) {
+            const auto itr = mMap.begin();
+            if (itr == mMap.end())
+                return;
+            itr->second->OnInvalidate();
+        }
+    }
+
+    ~CacheWeakMap() {
+        Clear();
+    }
+};
+
+} // namespace mozilla
+
+#endif // MOZILLA_CACHE_INVALIDATOR_H_
deleted file mode 100644
--- a/dom/canvas/CacheMap.cpp
+++ /dev/null
@@ -1,43 +0,0 @@
-/* -*- Mode: C++; tab-width: 13; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
-/* vim: set ts=13 sts=4 et sw=4 tw=90: */
-/* 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/. */
-
-#include "CacheMap.h"
-
-namespace mozilla {
-
-void
-CacheMapInvalidator::InvalidateCaches() const
-{
-    while (mCacheEntries.size()) {
-        const auto pEntry = *(mCacheEntries.begin());
-        pEntry->Invalidate();
-        MOZ_ASSERT(mCacheEntries.find(pEntry) == mCacheEntries.end());
-    }
-}
-
-namespace detail {
-
-CacheMapUntypedEntry::CacheMapUntypedEntry(std::vector<const CacheMapInvalidator*>&& invalidators)
-    : mInvalidators(std::move(invalidators))
-{
-    for (const auto& cur : mInvalidators) {
-        // Don't assert that we insert, since there may be dupes in `invalidators`.
-        // (and it's not worth removing the dupes)
-        (void)cur->mCacheEntries.insert(this);
-    }
-}
-
-CacheMapUntypedEntry::~CacheMapUntypedEntry()
-{
-    for (const auto& cur : mInvalidators) {
-        // There might be dupes, so erase might return >1.
-        (void)cur->mCacheEntries.erase(this);
-    }
-}
-
-} // namespace detail
-
-} // namespace mozilla
deleted file mode 100644
--- a/dom/canvas/CacheMap.h
+++ /dev/null
@@ -1,131 +0,0 @@
-/* -*- Mode: C++; tab-width: 13; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
-/* vim: set ts=13 sts=4 et sw=4 tw=90: */
-/* 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/. */
-
-#ifndef MOZILLA_CACHE_MAP_H_
-#define MOZILLA_CACHE_MAP_H_
-
-#include "mozilla/UniquePtr.h"
-#include <map>
-#include <unordered_set>
-#include <vector>
-
-namespace mozilla {
-
-namespace detail {
-class CacheMapUntypedEntry;
-}
-
-class CacheMapInvalidator
-{
-    friend class detail::CacheMapUntypedEntry;
-
-    mutable std::unordered_set<const detail::CacheMapUntypedEntry*> mCacheEntries;
-
-public:
-    ~CacheMapInvalidator() {
-        InvalidateCaches();
-    }
-
-    void InvalidateCaches() const;
-};
-
-namespace detail {
-
-class CacheMapUntypedEntry
-{
-    template<typename, typename> friend class CacheMap;
-
-private:
-    const std::vector<const CacheMapInvalidator*> mInvalidators;
-
-protected:
-    CacheMapUntypedEntry(std::vector<const CacheMapInvalidator*>&& invalidators);
-    ~CacheMapUntypedEntry();
-
-public:
-    virtual void Invalidate() const = 0;
-};
-
-struct DerefLess final {
-    template<typename T>
-    bool operator ()(const T* const a, const T* const b) const {
-        return *a < *b;
-    }
-};
-
-} // namespace detail
-
-
-template<typename KeyT, typename ValueT>
-class CacheMap final
-{
-    class Entry final : public detail::CacheMapUntypedEntry {
-    public:
-        CacheMap& mParent;
-        const KeyT mKey;
-        const ValueT mValue;
-
-        Entry(std::vector<const CacheMapInvalidator*>&& invalidators, CacheMap& parent,
-              KeyT&& key, ValueT&& value)
-            : detail::CacheMapUntypedEntry(std::move(invalidators))
-            , mParent(parent)
-            , mKey(std::move(key))
-            , mValue(std::move(value))
-        { }
-
-        void Invalidate() const override {
-            const auto erased = mParent.mMap.erase(&mKey);
-            MOZ_ALWAYS_TRUE( erased == 1 );
-        }
-
-        bool operator <(const Entry& x) const {
-            return mKey < x.mKey;
-        }
-    };
-
-    typedef std::map<const KeyT*, UniquePtr<const Entry>, detail::DerefLess> MapT;
-    MapT mMap;
-
-public:
-    const ValueT* Insert(KeyT&& key, ValueT&& value,
-                         std::vector<const CacheMapInvalidator*>&& invalidators)
-    {
-        UniquePtr<const Entry> entry( new Entry(std::move(invalidators), *this, std::move(key),
-                                                std::move(value)) );
-
-        typename MapT::value_type insertable{
-            &entry->mKey,
-            nullptr
-        };
-        insertable.second = std::move(entry);
-
-        const auto res = mMap.insert(std::move(insertable));
-        const auto& didInsert = res.second;
-        MOZ_ALWAYS_TRUE( didInsert );
-
-        const auto& itr = res.first;
-        return &itr->second->mValue;
-    }
-
-    const ValueT* Find(const KeyT& key) const {
-        const auto itr = mMap.find(&key);
-        if (itr == mMap.end())
-            return nullptr;
-
-        return &itr->second->mValue;
-    }
-
-    void Invalidate() {
-        while (mMap.size()) {
-            const auto& itr = mMap.begin();
-            itr->second->Invalidate();
-        }
-    }
-};
-
-} // namespace mozilla
-
-#endif // MOZILLA_CACHE_MAP_H_
--- a/dom/canvas/WebGL2ContextSamplers.cpp
+++ b/dom/canvas/WebGL2ContextSamplers.cpp
@@ -25,18 +25,16 @@ WebGL2Context::DeleteSampler(WebGLSample
 {
     const FuncScope funcScope(*this, "deleteSampler");
     if (!ValidateDeleteObject(sampler))
         return;
 
     for (uint32_t n = 0; n < mGLMaxTextureUnits; n++) {
         if (mBoundSamplers[n] == sampler) {
             mBoundSamplers[n] = nullptr;
-
-            InvalidateResolveCacheForTextureWithTexUnit(n);
         }
     }
 
     sampler->RequestDelete();
 }
 
 bool
 WebGL2Context::IsSampler(const WebGLSampler* const obj)
@@ -60,17 +58,16 @@ WebGL2Context::BindSampler(GLuint unit, 
 
     if (unit >= mGLMaxTextureUnits)
         return ErrorInvalidValue("unit must be < %u", mGLMaxTextureUnits);
 
     ////
 
     gl->fBindSampler(unit, sampler ? sampler->mGLName : 0);
 
-    InvalidateResolveCacheForTextureWithTexUnit(unit);
     mBoundSamplers[unit] = sampler;
 }
 
 void
 WebGL2Context::SamplerParameteri(WebGLSampler& sampler, GLenum pname, GLint param)
 {
     const FuncScope funcScope(*this, "samplerParameteri");
     if (IsContextLost())
--- a/dom/canvas/WebGL2ContextUniforms.cpp
+++ b/dom/canvas/WebGL2ContextUniforms.cpp
@@ -19,48 +19,48 @@ namespace mozilla {
 
 // -------------------------------------------------------------------------
 // Uniforms
 
 void
 WebGLContext::Uniform1ui(WebGLUniformLocation* loc, GLuint v0)
 {
     const FuncScope funcScope(*this, "uniform1ui");
-    if (!ValidateUniformSetter(loc, 1, LOCAL_GL_UNSIGNED_INT))
+    if (!ValidateUniformSetter(loc, 1, webgl::AttribBaseType::UInt))
         return;
 
     gl->fUniform1ui(loc->mLoc, v0);
 }
 
 void
 WebGLContext::Uniform2ui(WebGLUniformLocation* loc, GLuint v0, GLuint v1)
 {
     const FuncScope funcScope(*this, "uniform2ui");
-    if (!ValidateUniformSetter(loc, 2, LOCAL_GL_UNSIGNED_INT))
+    if (!ValidateUniformSetter(loc, 2, webgl::AttribBaseType::UInt))
         return;
 
     gl->fUniform2ui(loc->mLoc, v0, v1);
 }
 
 void
 WebGLContext::Uniform3ui(WebGLUniformLocation* loc, GLuint v0, GLuint v1, GLuint v2)
 {
     const FuncScope funcScope(*this, "uniform3ui");
-    if (!ValidateUniformSetter(loc, 3, LOCAL_GL_UNSIGNED_INT))
+    if (!ValidateUniformSetter(loc, 3, webgl::AttribBaseType::UInt))
         return;
 
     gl->fUniform3ui(loc->mLoc, v0, v1, v2);
 }
 
 void
 WebGLContext::Uniform4ui(WebGLUniformLocation* loc, GLuint v0, GLuint v1, GLuint v2,
                          GLuint v3)
 {
     const FuncScope funcScope(*this, "uniform4ui");
-    if (!ValidateUniformSetter(loc, 4, LOCAL_GL_UNSIGNED_INT))
+    if (!ValidateUniformSetter(loc, 4, webgl::AttribBaseType::UInt))
         return;
 
     gl->fUniform4ui(loc->mLoc, v0, v1, v2, v3);
 }
 
 // -------------------------------------------------------------------------
 // Uniform Buffer Objects and Transform Feedback Buffers
 
--- a/dom/canvas/WebGLActiveInfo.cpp
+++ b/dom/canvas/WebGLActiveInfo.cpp
@@ -73,26 +73,99 @@ ElemSizeFromType(GLenum elemType)
 
     default:
         MOZ_CRASH("GFX: Bad `elemType`.");
     }
 }
 
 ////////////////////
 
+static webgl::AttribBaseType
+ElemBaseType(const GLenum elemType)
+{
+    switch (elemType) {
+    case LOCAL_GL_FLOAT:
+    case LOCAL_GL_FLOAT_VEC2:
+    case LOCAL_GL_FLOAT_VEC3:
+    case LOCAL_GL_FLOAT_VEC4:
+
+    case LOCAL_GL_FLOAT_MAT2:
+    case LOCAL_GL_FLOAT_MAT2x3:
+    case LOCAL_GL_FLOAT_MAT2x4:
+
+    case LOCAL_GL_FLOAT_MAT3x2:
+    case LOCAL_GL_FLOAT_MAT3:
+    case LOCAL_GL_FLOAT_MAT3x4:
+
+    case LOCAL_GL_FLOAT_MAT4x2:
+    case LOCAL_GL_FLOAT_MAT4x3:
+    case LOCAL_GL_FLOAT_MAT4:
+        return webgl::AttribBaseType::Float;
+
+    // -
+
+    case LOCAL_GL_INT:
+    case LOCAL_GL_INT_VEC2:
+    case LOCAL_GL_INT_VEC3:
+    case LOCAL_GL_INT_VEC4:
+
+    case LOCAL_GL_SAMPLER_2D:
+    case LOCAL_GL_SAMPLER_3D:
+    case LOCAL_GL_SAMPLER_CUBE:
+    case LOCAL_GL_SAMPLER_2D_ARRAY:
+    case LOCAL_GL_SAMPLER_2D_SHADOW:
+    case LOCAL_GL_SAMPLER_CUBE_SHADOW:
+    case LOCAL_GL_SAMPLER_2D_ARRAY_SHADOW:
+
+    case LOCAL_GL_INT_SAMPLER_2D:
+    case LOCAL_GL_INT_SAMPLER_3D:
+    case LOCAL_GL_INT_SAMPLER_CUBE:
+    case LOCAL_GL_INT_SAMPLER_2D_ARRAY:
+
+    case LOCAL_GL_UNSIGNED_INT_SAMPLER_2D:
+    case LOCAL_GL_UNSIGNED_INT_SAMPLER_3D:
+    case LOCAL_GL_UNSIGNED_INT_SAMPLER_CUBE:
+    case LOCAL_GL_UNSIGNED_INT_SAMPLER_2D_ARRAY:
+        return webgl::AttribBaseType::Int;
+
+    // -
+
+    case LOCAL_GL_UNSIGNED_INT:
+    case LOCAL_GL_UNSIGNED_INT_VEC2:
+    case LOCAL_GL_UNSIGNED_INT_VEC3:
+    case LOCAL_GL_UNSIGNED_INT_VEC4:
+        return webgl::AttribBaseType::UInt;
+
+    // -
+
+    case LOCAL_GL_BOOL:
+    case LOCAL_GL_BOOL_VEC2:
+    case LOCAL_GL_BOOL_VEC3:
+    case LOCAL_GL_BOOL_VEC4:
+        return webgl::AttribBaseType::Bool;
+
+    // -
+
+    default:
+        MOZ_ASSERT(false, "unexpected attrib elemType");
+        return webgl::AttribBaseType::Float; // Just pick something.
+    }
+}
+
 WebGLActiveInfo::WebGLActiveInfo(WebGLContext* webgl, GLint elemCount, GLenum elemType,
                                  bool isArray, const nsACString& baseUserName,
                                  const nsACString& baseMappedName)
     : mWebGL(webgl)
     , mElemCount(elemCount)
     , mElemType(elemType)
     , mBaseUserName(baseUserName)
     , mIsArray(isArray)
     , mElemSize(ElemSizeFromType(elemType))
     , mBaseMappedName(baseMappedName)
+    , mBaseType(ElemBaseType(mElemType))
 { }
 
 bool
 WebGLActiveInfo::IsSampler() const
 {
     switch (mElemType) {
     case LOCAL_GL_SAMPLER_2D:
     case LOCAL_GL_SAMPLER_3D:
--- a/dom/canvas/WebGLActiveInfo.h
+++ b/dom/canvas/WebGLActiveInfo.h
@@ -36,16 +36,17 @@ public:
     const uint32_t mElemCount; // `size`
     const GLenum mElemType; // `type`
     const nsCString mBaseUserName; // `name`, but ASCII, and without any final "[0]".
 
     // Not actually part of ActiveInfo:
     const bool mIsArray;
     const uint8_t mElemSize;
     const nsCString mBaseMappedName; // Without any final "[0]".
+    const webgl::AttribBaseType mBaseType = webgl::AttribBaseType::Float;
 
     bool IsSampler() const;
 
     WebGLActiveInfo(WebGLContext* webgl, GLint elemCount, GLenum elemType, bool isArray,
                     const nsACString& baseUserName, const nsACString& baseMappedName);
 
     /* GLES 2.0.25, p33:
      *   This command will return as much information about active
--- a/dom/canvas/WebGLBuffer.h
+++ b/dom/canvas/WebGLBuffer.h
@@ -3,17 +3,17 @@
  * 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/. */
 
 #ifndef WEBGL_BUFFER_H_
 #define WEBGL_BUFFER_H_
 
 #include <map>
 
-#include "CacheMap.h"
+#include "CacheInvalidator.h"
 #include "GLDefs.h"
 #include "mozilla/LinkedList.h"
 #include "nsWrapperCache.h"
 #include "WebGLObjectModel.h"
 #include "WebGLTypes.h"
 
 namespace mozilla {
 
@@ -122,16 +122,16 @@ protected:
             return indexCount < x.indexCount;
         }
     };
 
     UniqueBuffer mIndexCache;
     mutable std::map<IndexRange, Maybe<uint32_t>> mIndexRanges;
 
 public:
-    CacheMapInvalidator mFetchInvalidator;
+    CacheInvalidator mFetchInvalidator;
 
     void ResetLastUpdateFenceId() const;
 };
 
 } // namespace mozilla
 
 #endif // WEBGL_BUFFER_H_
--- a/dom/canvas/WebGLContext.cpp
+++ b/dom/canvas/WebGLContext.cpp
@@ -267,25 +267,16 @@ WebGLContext::DestroyResourcesAndContext
 
     if (mEmptyTFO) {
         gl->fDeleteTransformFeedbacks(1, &mEmptyTFO);
         mEmptyTFO = 0;
     }
 
     //////
 
-    mFakeBlack_2D_0000       = nullptr;
-    mFakeBlack_2D_0001       = nullptr;
-    mFakeBlack_CubeMap_0000  = nullptr;
-    mFakeBlack_CubeMap_0001  = nullptr;
-    mFakeBlack_3D_0000       = nullptr;
-    mFakeBlack_3D_0001       = nullptr;
-    mFakeBlack_2D_Array_0000 = nullptr;
-    mFakeBlack_2D_Array_0001 = nullptr;
-
     if (mFakeVertexAttrib0BufferObject) {
         gl->fDeleteBuffers(1, &mFakeVertexAttrib0BufferObject);
         mFakeVertexAttrib0BufferObject = 0;
     }
 
     // disable all extensions except "WEBGL_lose_context". see bug #927969
     // spec: http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
     for (size_t i = 0; i < size_t(WebGLExtensionID::Max); ++i) {
@@ -1335,92 +1326,70 @@ WebGLContext::GetContextAttributes(dom::
     result.mStencil = mOptions.stencil;
     result.mAntialias = mOptions.antialias;
     result.mPremultipliedAlpha = mOptions.premultipliedAlpha;
     result.mPreserveDrawingBuffer = mOptions.preserveDrawingBuffer;
     result.mFailIfMajorPerformanceCaveat = mOptions.failIfMajorPerformanceCaveat;
     result.mPowerPreference = mOptions.powerPreference;
 }
 
-void
-WebGLContext::ForceClearFramebufferWithDefaultValues(const GLbitfield clearBits,
-                                                     const bool fakeNoAlpha) const
+// -
+
+namespace webgl {
+
+ScopedPrepForResourceClear::ScopedPrepForResourceClear(const WebGLContext& webgl_)
+    : webgl(webgl_)
 {
-    const bool initializeColorBuffer = bool(clearBits & LOCAL_GL_COLOR_BUFFER_BIT);
-    const bool initializeDepthBuffer = bool(clearBits & LOCAL_GL_DEPTH_BUFFER_BIT);
-    const bool initializeStencilBuffer = bool(clearBits & LOCAL_GL_STENCIL_BUFFER_BIT);
-
-    // Fun GL fact: No need to worry about the viewport here, glViewport is just
-    // setting up a coordinates transformation, it doesn't affect glClear at all.
-    AssertCachedGlobalState();
-
-    // Prepare GL state for clearing.
-    if (mScissorTestEnabled) {
+    const auto& gl = webgl.gl;
+
+    if (webgl.mScissorTestEnabled) {
         gl->fDisable(LOCAL_GL_SCISSOR_TEST);
     }
-
-    if (initializeColorBuffer) {
-        DoColorMask(0x0f);
-
-        if (fakeNoAlpha) {
-            gl->fClearColor(0.0f, 0.0f, 0.0f, 1.0f);
-        } else {
-            gl->fClearColor(0.0f, 0.0f, 0.0f, 0.0f);
-        }
-    }
-
-    if (initializeDepthBuffer) {
-        gl->fDepthMask(1);
-        gl->fClearDepth(1.0f);
-    }
-
-    if (initializeStencilBuffer) {
-        // "The clear operation always uses the front stencil write mask
-        //  when clearing the stencil buffer."
-        gl->fStencilMaskSeparate(LOCAL_GL_FRONT, 0xffffffff);
-        gl->fStencilMaskSeparate(LOCAL_GL_BACK,  0xffffffff);
-        gl->fClearStencil(0);
-    }
-
-    if (mRasterizerDiscardEnabled) {
+    if (webgl.mRasterizerDiscardEnabled) {
         gl->fDisable(LOCAL_GL_RASTERIZER_DISCARD);
     }
 
-    // Do the clear!
-    gl->fClear(clearBits);
-
-    // And reset!
-    if (mScissorTestEnabled) {
+    // "The clear operation always uses the front stencil write mask
+    //  when clearing the stencil buffer."
+    webgl.DoColorMask(0x0f);
+    gl->fDepthMask(true);
+    gl->fStencilMaskSeparate(LOCAL_GL_FRONT, 0xffffffff);
+
+    gl->fClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+    gl->fClearDepth(1.0f); // Depth formats are always cleared to 1.0f, not 0.0f.
+    gl->fClearStencil(0);
+}
+
+ScopedPrepForResourceClear::~ScopedPrepForResourceClear()
+{
+    const auto& gl = webgl.gl;
+
+    if (webgl.mScissorTestEnabled) {
         gl->fEnable(LOCAL_GL_SCISSOR_TEST);
     }
-
-    if (mRasterizerDiscardEnabled) {
+    if (webgl.mRasterizerDiscardEnabled) {
         gl->fEnable(LOCAL_GL_RASTERIZER_DISCARD);
     }
 
-    // Restore GL state after clearing.
-    if (initializeColorBuffer) {
-        gl->fClearColor(mColorClearValue[0],
-                        mColorClearValue[1],
-                        mColorClearValue[2],
-                        mColorClearValue[3]);
-    }
-
-    if (initializeDepthBuffer) {
-        gl->fDepthMask(mDepthWriteMask);
-        gl->fClearDepth(mDepthClearValue);
-    }
-
-    if (initializeStencilBuffer) {
-        gl->fStencilMaskSeparate(LOCAL_GL_FRONT, mStencilWriteMaskFront);
-        gl->fStencilMaskSeparate(LOCAL_GL_BACK,  mStencilWriteMaskBack);
-        gl->fClearStencil(mStencilClearValue);
-    }
+    // DoColorMask() is lazy.
+    gl->fDepthMask(webgl.mDepthWriteMask);
+    gl->fStencilMaskSeparate(LOCAL_GL_FRONT, webgl.mStencilWriteMaskFront);
+
+    gl->fClearColor(webgl.mColorClearValue[0],
+                    webgl.mColorClearValue[1],
+                    webgl.mColorClearValue[2],
+                    webgl.mColorClearValue[3]);
+    gl->fClearDepth(webgl.mDepthClearValue);
+    gl->fClearStencil(webgl.mStencilClearValue);
 }
 
+} // namespace webgl
+
+// -
+
 void
 WebGLContext::OnEndOfFrame() const
 {
    if (gfxPrefs::WebGLSpewFrameAllocs()) {
       GeneratePerfWarning("[webgl.perf.spew-frame-allocs] %" PRIu64 " data allocations this frame.",
                            mDataAllocGLCallCount);
    }
    mDataAllocGLCallCount = 0;
@@ -1908,22 +1877,27 @@ WebGLContext::ValidateAndInitFB(const We
 {
     if (fb)
         return fb->ValidateAndInitAttachments();
 
     if (!EnsureDefaultFB())
         return false;
 
     if (mDefaultFB_IsInvalid) {
+        // Clear it!
         gl->fBindFramebuffer(LOCAL_GL_FRAMEBUFFER, mDefaultFB->mFB);
+        const webgl::ScopedPrepForResourceClear scopedPrep(*this);
+        if (!mOptions.alpha) {
+            gl->fClearColor(0, 0, 0, 1);
+        }
         const GLbitfield bits = LOCAL_GL_COLOR_BUFFER_BIT |
                                 LOCAL_GL_DEPTH_BUFFER_BIT |
                                 LOCAL_GL_STENCIL_BUFFER_BIT;
-        const bool fakeNoAlpha = !mOptions.alpha;
-        ForceClearFramebufferWithDefaultValues(bits, fakeNoAlpha);
+        gl->fClear(bits);
+
         mDefaultFB_IsInvalid = false;
     }
     return true;
 }
 
 void
 WebGLContext::DoBindFB(const WebGLFramebuffer* const fb, const GLenum target) const
 {
@@ -2031,18 +2005,18 @@ ScopedDrawCallWrapper::ScopedDrawCallWra
                                  // rendering?
         } else {
             driverColorMask &= ~(uint8_t(mWebGL.mNeedsFakeNoAlpha) << 3);
         }
         driverDepthTest   &= !mWebGL.mNeedsFakeNoDepth;
         driverStencilTest &= !mWebGL.mNeedsFakeNoStencil;
     } else {
         if (mWebGL.mNeedsFakeNoStencil_UserFBs &&
-            fb->DepthAttachment().IsDefined() &&
-            !fb->StencilAttachment().IsDefined())
+            fb->DepthAttachment().HasAttachment() &&
+            !fb->StencilAttachment().HasAttachment())
         {
             driverStencilTest = false;
         }
     }
 
     const auto& gl = mWebGL.gl;
     mWebGL.DoColorMask(driverColorMask);
     if (mWebGL.mDriverDepthTest != driverDepthTest) {
@@ -2092,17 +2066,17 @@ IndexedBufferBinding::ByteCount() const
         return 0;
     bufferSize -= mRangeStart;
 
     return std::min(bufferSize, mRangeSize);
 }
 
 ////////////////////////////////////////
 
-ScopedUnpackReset::ScopedUnpackReset(WebGLContext* webgl)
+ScopedUnpackReset::ScopedUnpackReset(const WebGLContext* const webgl)
     : ScopedGLWrapper<ScopedUnpackReset>(webgl->gl)
     , mWebGL(webgl)
 {
     if (mWebGL->mPixelStore_UnpackAlignment != 4) mGL->fPixelStorei(LOCAL_GL_UNPACK_ALIGNMENT, 4);
 
     if (mWebGL->IsWebGL2()) {
         if (mWebGL->mPixelStore_UnpackRowLength   != 0) mGL->fPixelStorei(LOCAL_GL_UNPACK_ROW_LENGTH  , 0);
         if (mWebGL->mPixelStore_UnpackImageHeight != 0) mGL->fPixelStorei(LOCAL_GL_UNPACK_IMAGE_HEIGHT, 0);
@@ -2386,17 +2360,17 @@ WebGLContext::EnsureVRReady()
         const auto caps = gl->Screen()->mCaps;
         auto flags = TextureFlags::ORIGIN_BOTTOM_LEFT;
         if (!IsPremultAlpha() && mOptions.alpha) {
             flags |= TextureFlags::NON_PREMULTIPLIED;
         }
         auto factory = gl::GLScreenBuffer::CreateFactory(gl, caps, imageBridge.get(), flags);
         gl->Screen()->Morph(std::move(factory));
 #if defined(MOZ_WIDGET_ANDROID)
-        // On Android we are using a different GLScreenBuffer for WebVR, so we need a resize here because 
+        // On Android we are using a different GLScreenBuffer for WebVR, so we need a resize here because
         // PresentScreenBuffer() may not be called for the gl->Screen() after we set the new factory.
         gl->Screen()->Resize(DrawingBufferSize());
 #endif
         mVRReady = true;
     }
 }
 
 ////////////////////////////////////////////////////////////////////////////////
--- a/dom/canvas/WebGLContext.h
+++ b/dom/canvas/WebGLContext.h
@@ -26,22 +26,21 @@
 #include "nsLayoutUtils.h"
 #include "nsTArray.h"
 #include "nsWrapperCache.h"
 #include "SurfaceTypes.h"
 #include "ScopedGLHelpers.h"
 #include "TexUnpackBlob.h"
 
 // Local
-#include "CacheMap.h"
+#include "CacheInvalidator.h"
 #include "WebGLContextLossHandler.h"
 #include "WebGLContextUnchecked.h"
 #include "WebGLObjectModel.h"
 #include "WebGLStrongTypes.h"
-#include "WebGLTexture.h"
 
 // Generated
 #include "nsIDOMEventListener.h"
 #include "nsICanvasRenderingContextInternal.h"
 #include "nsIObserver.h"
 #include "mozilla/dom/HTMLCanvasElement.h"
 #include "nsWrapperCache.h"
 #include "nsLayoutUtils.h"
@@ -99,17 +98,20 @@ class MozFramebuffer;
 } // namespace gl
 
 namespace webgl {
 class AvailabilityRunnable;
 struct CachedDrawFetchLimits;
 struct FormatInfo;
 class FormatUsageAuthority;
 struct FormatUsageInfo;
+struct ImageInfo;
 struct LinkedProgramInfo;
+struct SamplingState;
+class ScopedPrepForResourceClear;
 class ShaderValidator;
 class TexUnpackBlob;
 struct UniformInfo;
 struct UniformBlockInfo;
 } // namespace webgl
 
 WebGLTexelFormat GetWebGLTexelFormat(TexInternalFormat format);
 
@@ -301,16 +303,17 @@ class WebGLContext
     friend class WebGLExtensionDisjointTimerQuery;
     friend class WebGLExtensionDrawBuffers;
     friend class WebGLExtensionLoseContext;
     friend class WebGLExtensionMOZDebug;
     friend class WebGLExtensionVertexArray;
     friend class WebGLMemoryTracker;
     friend class webgl::AvailabilityRunnable;
     friend struct webgl::LinkedProgramInfo;
+    friend class webgl::ScopedPrepForResourceClear;
     friend struct webgl::UniformBlockInfo;
 
     friend const webgl::CachedDrawFetchLimits*
         ValidateDraw(WebGLContext*, GLenum, uint32_t);
 
     enum {
         UNPACK_FLIP_Y_WEBGL = 0x9240,
         UNPACK_PREMULTIPLY_ALPHA_WEBGL = 0x9241,
@@ -469,18 +472,16 @@ public:
      */
     WebGLTexture*
     ActiveBoundTextureForTexImageTarget(const TexImageTarget texImgTarget) const
     {
         const TexTarget texTarget = TexImageTargetToTexTarget(texImgTarget);
         return ActiveBoundTextureForTarget(texTarget);
     }
 
-    void InvalidateResolveCacheForTextureWithTexUnit(const GLuint);
-
     already_AddRefed<Layer>
     GetCanvasLayer(nsDisplayListBuilder* builder, Layer* oldLayer,
                    LayerManager* manager) override;
 
     bool
     UpdateWebRenderCanvasData(nsDisplayListBuilder* aBuilder,
                               WebRenderCanvasData* aCanvasData) override;
 
@@ -508,23 +509,16 @@ public:
     void BeginComposition(gl::GLScreenBuffer* const screen = nullptr);
     // Clean up the context after captured for compositing
     void EndComposition();
 
     // a number that increments every time we have an event that causes
     // all context resources to be lost.
     uint32_t Generation() const { return mGeneration.value(); }
 
-    // This is similar to GLContext::ClearSafely, but tries to minimize the
-    // amount of work it does.
-    // It only clears the buffers we specify, and can reset its state without
-    // first having to query anything, as WebGL knows its state at all times.
-    void ForceClearFramebufferWithDefaultValues(GLbitfield bufferBits,
-                                                bool fakeNoAlpha) const;
-
     void RunContextLossTimer();
     void UpdateContextLossStatus();
     void EnqueueUpdateContextLossStatus();
 
     bool TryToRestoreContext();
 
     void AssertCachedBindings() const;
     void AssertCachedGlobalState() const;
@@ -930,27 +924,28 @@ public:
 
     #undef FOO
 
     ////////////////////////////////////
 
     void UseProgram(WebGLProgram* prog);
 
     bool ValidateAttribArraySetter(uint32_t count, uint32_t arrayLength);
-    bool ValidateUniformLocation(WebGLUniformLocation* loc);
-    bool ValidateUniformSetter(WebGLUniformLocation* loc, uint8_t setterSize,
-                               GLenum setterType);
-    bool ValidateUniformArraySetter(WebGLUniformLocation* loc,
-                                    uint8_t setterElemSize, GLenum setterType,
+    bool ValidateUniformLocation(const WebGLUniformLocation* loc);
+    bool ValidateUniformSetter(const WebGLUniformLocation* loc, uint8_t setterElemSize,
+                               webgl::AttribBaseType setterType);
+    bool ValidateUniformArraySetter(const WebGLUniformLocation* loc,
+                                    uint8_t setterElemSize,
+                                    webgl::AttribBaseType setterType,
                                     uint32_t setterArraySize,
                                     uint32_t* out_numElementsToUpload);
-    bool ValidateUniformMatrixArraySetter(WebGLUniformLocation* loc,
+    bool ValidateUniformMatrixArraySetter(const WebGLUniformLocation* loc,
                                           uint8_t setterCols,
                                           uint8_t setterRows,
-                                          GLenum setterType,
+                                          webgl::AttribBaseType setterType,
                                           uint32_t setterArraySize,
                                           bool setterTranspose,
                                           uint32_t* out_numElementsToUpload);
     void ValidateProgram(const WebGLProgram& prog);
     bool ValidateUniformLocation(const char* info, WebGLUniformLocation* loc);
     bool ValidateSamplerUniformSetter(const char* info,
                                       WebGLUniformLocation* loc, GLint value);
     void Viewport(GLint x, GLint y, GLsizei width, GLsizei height);
@@ -1314,24 +1309,24 @@ public:
 
 protected:
     bool ValidateTexImageSpecification(uint8_t funcDims,
                                        GLenum texImageTarget, GLint level,
                                        GLsizei width, GLsizei height, GLsizei depth,
                                        GLint border,
                                        TexImageTarget* const out_target,
                                        WebGLTexture** const out_texture,
-                                       WebGLTexture::ImageInfo** const out_imageInfo);
+                                       webgl::ImageInfo** const out_imageInfo);
     bool ValidateTexImageSelection(uint8_t funcDims,
                                    GLenum texImageTarget, GLint level, GLint xOffset,
                                    GLint yOffset, GLint zOffset, GLsizei width,
                                    GLsizei height, GLsizei depth,
                                    TexImageTarget* const out_target,
                                    WebGLTexture** const out_texture,
-                                   WebGLTexture::ImageInfo** const out_imageInfo);
+                                   webgl::ImageInfo** const out_imageInfo);
     bool ValidateUnpackInfo(bool usePBOs, GLenum format,
                             GLenum type, webgl::PackingInfo* const out);
 
     UniquePtr<webgl::TexUnpackBlob>
     FromDomElem(TexImageTarget target, uint32_t width,
                 uint32_t height, uint32_t depth, const dom::Element& elem,
                 ErrorResult* const out_error);
 
@@ -1881,52 +1876,27 @@ protected:
                           uint32_t* const out_endOffset);
 
     GLenum mPixelStore_ColorspaceConversion = 0;
     bool mPixelStore_FlipY = false;
     bool mPixelStore_PremultiplyAlpha = false;
     bool mPixelStore_RequireFastPath = false;
 
     ////////////////////////////////////
-    class FakeBlackTexture {
-    public:
-        static UniquePtr<FakeBlackTexture> Create(gl::GLContext* gl,
-                                                  TexTarget target,
-                                                  FakeBlackType type);
-        gl::GLContext* const mGL;
-        const GLuint mGLName;
-
-        ~FakeBlackTexture();
-    protected:
-        explicit FakeBlackTexture(gl::GLContext* gl);
-    };
-
-    UniquePtr<FakeBlackTexture> mFakeBlack_2D_0000;
-    UniquePtr<FakeBlackTexture> mFakeBlack_2D_0001;
-    UniquePtr<FakeBlackTexture> mFakeBlack_CubeMap_0000;
-    UniquePtr<FakeBlackTexture> mFakeBlack_CubeMap_0001;
-    UniquePtr<FakeBlackTexture> mFakeBlack_3D_0000;
-    UniquePtr<FakeBlackTexture> mFakeBlack_3D_0001;
-    UniquePtr<FakeBlackTexture> mFakeBlack_2D_Array_0000;
-    UniquePtr<FakeBlackTexture> mFakeBlack_2D_Array_0001;
-
-    bool BindFakeBlack(uint32_t texUnit, TexTarget target, FakeBlackType fakeBlack);
-
-    ////////////////////////////////////
 
 protected:
     GLuint mEmptyTFO;
 
     // Generic Vertex Attributes
     // Though CURRENT_VERTEX_ATTRIB is listed under "Vertex Shader State" in the spec
     // state tables, this isn't vertex shader /object/ state. This array is merely state
     // useful to vertex shaders, but is global state.
-    UniquePtr<GLenum[]> mGenericVertexAttribTypes;
+    std::vector<webgl::AttribBaseType> mGenericVertexAttribTypes;
     uint8_t mGenericVertexAttrib0Data[sizeof(float) * 4];
-    CacheMapInvalidator mGenericVertexAttribTypeInvalidator;
+    CacheInvalidator mGenericVertexAttribTypeInvalidator;
 
     GLuint mFakeVertexAttrib0BufferObject = 0;
     size_t mFakeVertexAttrib0BufferObjectSize = 0;
     bool mFakeVertexAttrib0DataDefined = false;
     uint8_t mFakeVertexAttrib0Data[sizeof(float) * 4];
 
     JSObject* GetVertexAttribFloat32Array(JSContext* cx, GLuint index);
     JSObject* GetVertexAttribInt32Array(JSContext* cx, GLuint index);
@@ -2044,16 +2014,18 @@ public:
     void UpdateMaxDrawBuffers();
 
     // --
 private:
     webgl::AvailabilityRunnable* mAvailabilityRunnable = nullptr;
 public:
     webgl::AvailabilityRunnable* EnsureAvailabilityRunnable();
 
+    // -
+
     // Friend list
     friend class ScopedCopyTexImageSource;
     friend class ScopedResolveTexturesForDraw;
     friend class ScopedUnpackReset;
     friend class webgl::TexUnpackBlob;
     friend class webgl::TexUnpackBytes;
     friend class webgl::TexUnpackImage;
     friend class webgl::TexUnpackSurface;
@@ -2104,35 +2076,35 @@ ValidateTexImageTarget(WebGLContext* web
                        WebGLTexture** const out_tex);
 
 class ScopedUnpackReset final
     : public gl::ScopedGLWrapper<ScopedUnpackReset>
 {
     friend struct gl::ScopedGLWrapper<ScopedUnpackReset>;
 
 private:
-    WebGLContext* const mWebGL;
+    const WebGLContext* const mWebGL;
 
 public:
-    explicit ScopedUnpackReset(WebGLContext* webgl);
+    explicit ScopedUnpackReset(const WebGLContext* webgl);
 
 private:
     void UnwrapImpl();
 };
 
 class ScopedFBRebinder final
     : public gl::ScopedGLWrapper<ScopedFBRebinder>
 {
     friend struct gl::ScopedGLWrapper<ScopedFBRebinder>;
 
 private:
-    WebGLContext* const mWebGL;
+    const WebGLContext* const mWebGL;
 
 public:
-    explicit ScopedFBRebinder(WebGLContext* webgl)
+    explicit ScopedFBRebinder(const WebGLContext* const webgl)
         : ScopedGLWrapper<ScopedFBRebinder>(webgl->gl)
         , mWebGL(webgl)
     { }
 
 private:
     void UnwrapImpl();
 };
 
@@ -2167,16 +2139,27 @@ class ScopedDrawCallWrapper final
 {
 public:
     WebGLContext& mWebGL;
 
     explicit ScopedDrawCallWrapper(WebGLContext& webgl);
     ~ScopedDrawCallWrapper();
 };
 
+namespace webgl {
+class ScopedPrepForResourceClear final
+{
+    const WebGLContext& webgl;
+
+public:
+    explicit ScopedPrepForResourceClear(const WebGLContext&);
+    ~ScopedPrepForResourceClear();
+};
+} // namespace webgl
+
 ////
 
 void
 ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& callback,
                             const std::vector<IndexedBufferBinding>& field,
                             const char* name, uint32_t flags = 0);
 
 void
--- a/dom/canvas/WebGLContextDraw.cpp
+++ b/dom/canvas/WebGLContextDraw.cpp
@@ -42,114 +42,124 @@ class ScopedResolveTexturesForDraw
     WebGLContext* const mWebGL;
     std::vector<TexRebindRequest> mRebindRequests;
 
 public:
     ScopedResolveTexturesForDraw(WebGLContext* webgl, bool* const out_error);
     ~ScopedResolveTexturesForDraw();
 };
 
-bool
-WebGLTexture::IsFeedback(WebGLContext* webgl, uint32_t texUnit,
-                         const std::vector<const WebGLFBAttachPoint*>& fbAttachments) const
+static bool
+ValidateNoSamplingFeedback(const WebGLTexture& tex, const uint32_t sampledLevels,
+                           const WebGLFramebuffer* const fb, const uint32_t texUnit)
 {
-    auto itr = fbAttachments.cbegin();
-    for (; itr != fbAttachments.cend(); ++itr) {
-        const auto& attach = *itr;
-        if (attach->Texture() == this)
-            break;
-    }
-
-    if (itr == fbAttachments.cend())
-        return false;
+    if (!fb)
+        return true;
 
-    ////
-
-    const auto minLevel = mBaseMipmapLevel;
-    uint32_t maxLevel;
-    if (!MaxEffectiveMipmapLevel(texUnit, &maxLevel)) {
-        // No valid mips. Will need fake-black.
-        return false;
-    }
-
-    ////
-
-    for (; itr != fbAttachments.cend(); ++itr) {
-        const auto& attach = *itr;
-        if (attach->Texture() != this)
+    const auto& texAttachments = fb->GetCompletenessInfo()->texAttachments;
+    for (const auto& attach : texAttachments) {
+        if (attach->Texture() != &tex)
             continue;
 
-        const auto dstLevel = attach->MipLevel();
-
-        if (minLevel <= dstLevel && dstLevel <= maxLevel) {
-            webgl->ErrorInvalidOperation("Feedback loop detected between tex target"
-                                         " 0x%04x, tex unit %u, levels %u-%u; and"
-                                         " framebuffer attachment 0x%04x, level %u.",
-                                         mTarget.get(), texUnit, minLevel,
-                                         maxLevel, attach->mAttachmentPoint, dstLevel);
-            return true;
+        const auto& srcBase = tex.BaseMipmapLevel();
+        const auto srcLast = srcBase + sampledLevels - 1;
+        const auto& dstLevel = attach->MipLevel();
+        if (MOZ_UNLIKELY( srcBase <= dstLevel && dstLevel <= srcLast )) {
+            const auto& webgl = tex.mContext;
+            const auto& texTargetStr = EnumString(tex.Target().get());
+            const auto& attachStr = EnumString(attach->mAttachmentPoint);
+            webgl->ErrorInvalidOperation("Texture level %u would be read by %s unit %u,"
+                                         " but written by framebuffer attachment %s,"
+                                         " which would be illegal feedback.",
+                                         dstLevel, texTargetStr.c_str(), texUnit,
+                                         attachStr.c_str());
+            return false;
         }
     }
-
-    return false;
+    return true;
 }
 
 ScopedResolveTexturesForDraw::ScopedResolveTexturesForDraw(WebGLContext* webgl,
                                                            bool* const out_error)
     : mWebGL(webgl)
 {
-    MOZ_ASSERT(mWebGL->gl->IsCurrent());
-
-    const std::vector<const WebGLFBAttachPoint*>* attachList = nullptr;
     const auto& fb = mWebGL->mBoundDrawFramebuffer;
-    if (fb) {
-        attachList = &(fb->ResolvedCompleteData()->texDrawBuffers);
-    }
 
     MOZ_ASSERT(mWebGL->mActiveProgramLinkInfo);
     const auto& uniformSamplers = mWebGL->mActiveProgramLinkInfo->uniformSamplers;
     for (const auto& uniform : uniformSamplers) {
         const auto& texList = *(uniform->mSamplerTexList);
 
+        const auto& uniformBaseType = uniform->mTexBaseType;
         for (const auto& texUnit : uniform->mSamplerValues) {
             if (texUnit >= texList.Length())
                 continue;
 
             const auto& tex = texList[texUnit];
             if (!tex)
                 continue;
 
-            if (attachList &&
-                tex->IsFeedback(mWebGL, texUnit, *attachList))
-            {
+            const auto& sampler = mWebGL->mBoundSamplers[texUnit];
+            const auto& samplingInfo = tex->GetSampleableInfo(sampler.get());
+            if (!samplingInfo) { // There was an error.
                 *out_error = true;
                 return;
             }
+            if (!samplingInfo->IsComplete()) {
+                if (samplingInfo->incompleteReason) {
+                    const auto& targetName = GetEnumName(tex->Target().get());
+                    mWebGL->GenerateWarning("%s at unit %u is incomplete: %s",
+                                            targetName, texUnit,
+                                            samplingInfo->incompleteReason);
+                }
+                mRebindRequests.push_back({texUnit, tex});
+                continue;
+            }
 
-            FakeBlackType fakeBlack;
-            if (!tex->ResolveForDraw(texUnit, &fakeBlack)) {
-                mWebGL->ErrorOutOfMemory("Failed to resolve textures for draw.");
+            // We have more validation to do if we're otherwise complete:
+            const auto& texBaseType = samplingInfo->usage->format->baseType;
+            if (texBaseType != uniformBaseType) {
+                const auto& targetName = GetEnumName(tex->Target().get());
+                const auto& srcType = ToString(texBaseType);
+                const auto& dstType = ToString(uniformBaseType);
+                mWebGL->ErrorInvalidOperation("%s at unit %u is of type %s, but"
+                                              " the shader samples as %s.",
+                                              targetName, texUnit, srcType,
+                                              dstType);
                 *out_error = true;
                 return;
             }
 
-            if (fakeBlack == FakeBlackType::None)
-                continue;
-
-            if (!mWebGL->BindFakeBlack(texUnit, tex->Target(), fakeBlack)) {
-                mWebGL->ErrorOutOfMemory("Failed to create fake black texture.");
+            if (uniform->mIsShadowSampler != samplingInfo->isDepthTexCompare) {
+                const auto& targetName = GetEnumName(tex->Target().get());
+                mWebGL->ErrorInvalidOperation("%s at unit %u is%s a depth texture"
+                                              " with TEXTURE_COMPARE_MODE, but"
+                                              " the shader sampler is%s a shadow"
+                                              " sampler.",
+                                              targetName, texUnit,
+                                              samplingInfo->isDepthTexCompare ? "" : " not",
+                                              uniform->mIsShadowSampler ? "" : " not");
                 *out_error = true;
                 return;
             }
 
-            mRebindRequests.push_back({texUnit, tex});
+            if (!ValidateNoSamplingFeedback(*tex, samplingInfo->levels, fb.get(),
+                                            texUnit))
+            {
+                *out_error = true;
+                return;
+            }
         }
     }
 
-    *out_error = false;
+    const auto& gl = mWebGL->gl;
+    for (const auto& itr : mRebindRequests) {
+        gl->fActiveTexture(LOCAL_GL_TEXTURE0 + itr.texUnit);
+        gl->fBindTexture(itr.tex->Target().get(), 0); // Tex 0 is always incomplete.
+    }
 }
 
 ScopedResolveTexturesForDraw::~ScopedResolveTexturesForDraw()
 {
     if (mRebindRequests.empty())
         return;
 
     gl::GLContext* gl = mWebGL->gl;
@@ -157,97 +167,32 @@ ScopedResolveTexturesForDraw::~ScopedRes
     for (const auto& itr : mRebindRequests) {
         gl->fActiveTexture(LOCAL_GL_TEXTURE0 + itr.texUnit);
         gl->fBindTexture(itr.tex->Target().get(), itr.tex->mGLName);
     }
 
     gl->fActiveTexture(LOCAL_GL_TEXTURE0 + mWebGL->mActiveTexture);
 }
 
-bool
-WebGLContext::BindFakeBlack(uint32_t texUnit, TexTarget target, FakeBlackType fakeBlack)
-{
-    MOZ_ASSERT(fakeBlack == FakeBlackType::RGBA0000 ||
-               fakeBlack == FakeBlackType::RGBA0001);
-
-    const auto fnGetSlot = [this, target, fakeBlack]() -> UniquePtr<FakeBlackTexture>*
-    {
-        switch (fakeBlack) {
-        case FakeBlackType::RGBA0000:
-            switch (target.get()) {
-            case LOCAL_GL_TEXTURE_2D      : return &mFakeBlack_2D_0000;
-            case LOCAL_GL_TEXTURE_CUBE_MAP: return &mFakeBlack_CubeMap_0000;
-            case LOCAL_GL_TEXTURE_3D      : return &mFakeBlack_3D_0000;
-            case LOCAL_GL_TEXTURE_2D_ARRAY: return &mFakeBlack_2D_Array_0000;
-            default: return nullptr;
-            }
-
-        case FakeBlackType::RGBA0001:
-            switch (target.get()) {
-            case LOCAL_GL_TEXTURE_2D      : return &mFakeBlack_2D_0001;
-            case LOCAL_GL_TEXTURE_CUBE_MAP: return &mFakeBlack_CubeMap_0001;
-            case LOCAL_GL_TEXTURE_3D      : return &mFakeBlack_3D_0001;
-            case LOCAL_GL_TEXTURE_2D_ARRAY: return &mFakeBlack_2D_Array_0001;
-            default: return nullptr;
-            }
-
-        default:
-            return nullptr;
-        }
-    };
-
-    UniquePtr<FakeBlackTexture>* slot = fnGetSlot();
-    if (!slot) {
-        MOZ_CRASH("GFX: fnGetSlot failed.");
-    }
-    UniquePtr<FakeBlackTexture>& fakeBlackTex = *slot;
-
-    if (!fakeBlackTex) {
-        gl->fPixelStorei(LOCAL_GL_UNPACK_ALIGNMENT, 1);
-        if (IsWebGL2()) {
-            gl->fPixelStorei(LOCAL_GL_UNPACK_SKIP_PIXELS, 0);
-            gl->fPixelStorei(LOCAL_GL_UNPACK_SKIP_ROWS, 0);
-            gl->fPixelStorei(LOCAL_GL_UNPACK_SKIP_IMAGES, 0);
-        }
-
-        fakeBlackTex = FakeBlackTexture::Create(gl, target, fakeBlack);
-
-        gl->fPixelStorei(LOCAL_GL_UNPACK_ALIGNMENT, mPixelStore_UnpackAlignment);
-        if (IsWebGL2()) {
-            gl->fPixelStorei(LOCAL_GL_UNPACK_SKIP_PIXELS, mPixelStore_UnpackSkipPixels);
-            gl->fPixelStorei(LOCAL_GL_UNPACK_SKIP_ROWS, mPixelStore_UnpackSkipRows);
-            gl->fPixelStorei(LOCAL_GL_UNPACK_SKIP_IMAGES, mPixelStore_UnpackSkipImages);
-        }
-        if (!fakeBlackTex) {
-            return false;
-        }
-    }
-
-    gl->fActiveTexture(LOCAL_GL_TEXTURE0 + texUnit);
-    gl->fBindTexture(target.get(), fakeBlackTex->mGLName);
-    gl->fActiveTexture(LOCAL_GL_TEXTURE0 + mActiveTexture);
-    return true;
-}
-
 ////////////////////////////////////////
 
 bool
 WebGLContext::ValidateStencilParamsForDrawCall() const
 {
     const auto stencilBits = [&]() -> uint8_t {
         if (!mStencilTestEnabled)
             return 0;
 
         if (!mBoundDrawFramebuffer)
             return mOptions.stencil ? 8 : 0;
 
-        if (mBoundDrawFramebuffer->StencilAttachment().IsDefined())
+        if (mBoundDrawFramebuffer->StencilAttachment().HasAttachment())
             return 8;
 
-        if (mBoundDrawFramebuffer->DepthStencilAttachment().IsDefined())
+        if (mBoundDrawFramebuffer->DepthStencilAttachment().HasAttachment())
             return 8;
 
         return 0;
     }();
     const uint32_t stencilMax = (1 << stencilBits) - 1;
 
     const auto fnMask = [&](const uint32_t x) { return x & stencilMax; };
     const auto fnClamp = [&](const int32_t x) {
@@ -832,27 +777,22 @@ WebGLContext::Draw_cleanup()
                 gl->fFlush();
                 mDrawCallsSinceLastFlush = 0;
             }
         }
     }
 
     // Let's check for a really common error: Viewport is larger than the actual
     // destination framebuffer.
-    uint32_t destWidth = mViewportWidth;
-    uint32_t destHeight = mViewportHeight;
-
+    uint32_t destWidth;
+    uint32_t destHeight;
     if (mBoundDrawFramebuffer) {
-        const auto& drawBuffers = mBoundDrawFramebuffer->ColorDrawBuffers();
-        for (const auto& cur : drawBuffers) {
-            if (!cur->IsDefined())
-                continue;
-            cur->Size(&destWidth, &destHeight);
-            break;
-        }
+        const auto& info = mBoundDrawFramebuffer->GetCompletenessInfo();
+        destWidth = info->width;
+        destHeight = info->height;
     } else {
         destWidth = mDefaultFB->mSize.width;
         destHeight = mDefaultFB->mSize.height;
     }
 
     if (mViewportWidth > int32_t(destWidth) ||
         mViewportHeight > int32_t(destHeight))
     {
@@ -914,30 +854,28 @@ WebGLContext::DoFakeVertexAttrib0(const 
         gl->fGenBuffers(1, &mFakeVertexAttrib0BufferObject);
         mFakeVertexAttrib0BufferObjectSize = 0;
     }
     gl->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, mFakeVertexAttrib0BufferObject);
 
     ////
 
     switch (mGenericVertexAttribTypes[0]) {
-    case LOCAL_GL_FLOAT:
+    case webgl::AttribBaseType::Bool:
+    case webgl::AttribBaseType::Float:
         gl->fVertexAttribPointer(0, 4, LOCAL_GL_FLOAT, false, 0, 0);
         break;
 
-    case LOCAL_GL_INT:
+    case webgl::AttribBaseType::Int:
         gl->fVertexAttribIPointer(0, 4, LOCAL_GL_INT, 0, 0);
         break;
 
-    case LOCAL_GL_UNSIGNED_INT:
+    case webgl::AttribBaseType::UInt:
         gl->fVertexAttribIPointer(0, 4, LOCAL_GL_UNSIGNED_INT, 0, 0);
         break;
-
-    default:
-        MOZ_CRASH();
     }
 
     ////
 
     const auto bytesPerVert = sizeof(mFakeVertexAttrib0Data);
     const auto checked_dataSize = CheckedUint32(vertexCount) * bytesPerVert;
     if (!checked_dataSize.isValid()) {
         ErrorOutOfMemory("Integer overflow trying to construct a fake vertex attrib 0"
@@ -1011,78 +949,9 @@ WebGLContext::UndoFakeVertexAttrib0()
         attrib0.DoVertexAttribPointer(gl, 0);
     } else {
         gl->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, 0);
     }
 
     gl->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, mBoundArrayBuffer ? mBoundArrayBuffer->mGLName : 0);
 }
 
-static GLuint
-CreateGLTexture(gl::GLContext* gl)
-{
-    MOZ_ASSERT(gl->IsCurrent());
-    GLuint ret = 0;
-    gl->fGenTextures(1, &ret);
-    return ret;
-}
-
-UniquePtr<WebGLContext::FakeBlackTexture>
-WebGLContext::FakeBlackTexture::Create(gl::GLContext* gl, TexTarget target,
-                                       FakeBlackType type)
-{
-    GLenum texFormat;
-    switch (type) {
-    case FakeBlackType::RGBA0000:
-        texFormat = LOCAL_GL_RGBA;
-        break;
-
-    case FakeBlackType::RGBA0001:
-        texFormat = LOCAL_GL_RGB;
-        break;
-
-    default:
-        MOZ_CRASH("GFX: bad type");
-    }
-
-    UniquePtr<FakeBlackTexture> result(new FakeBlackTexture(gl));
-    gl::ScopedBindTexture scopedBind(gl, result->mGLName, target.get());
-
-    gl->fTexParameteri(target.get(), LOCAL_GL_TEXTURE_MIN_FILTER, LOCAL_GL_NEAREST);
-    gl->fTexParameteri(target.get(), LOCAL_GL_TEXTURE_MAG_FILTER, LOCAL_GL_NEAREST);
-
-    const webgl::DriverUnpackInfo dui = {texFormat, texFormat, LOCAL_GL_UNSIGNED_BYTE};
-    UniqueBuffer zeros = moz_xcalloc(1, 4); // Infallible allocation.
-
-    MOZ_ASSERT(gl->IsCurrent());
-
-    if (target == LOCAL_GL_TEXTURE_CUBE_MAP) {
-        for (int i = 0; i < 6; ++i) {
-            const TexImageTarget curTarget = LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_X + i;
-            const GLenum error = DoTexImage(gl, curTarget.get(), 0, &dui, 1, 1, 1,
-                                            zeros.get());
-            if (error) {
-                return nullptr;
-            }
-        }
-    } else {
-        const GLenum error = DoTexImage(gl, target.get(), 0, &dui, 1, 1, 1,
-                                        zeros.get());
-        if (error) {
-            return nullptr;
-        }
-    }
-
-    return result;
-}
-
-WebGLContext::FakeBlackTexture::FakeBlackTexture(gl::GLContext* gl)
-    : mGL(gl)
-    , mGLName(CreateGLTexture(gl))
-{
-}
-
-WebGLContext::FakeBlackTexture::~FakeBlackTexture()
-{
-    mGL->fDeleteTextures(1, &mGLName);
-}
-
 } // namespace mozilla
--- a/dom/canvas/WebGLContextFramebufferOperations.cpp
+++ b/dom/canvas/WebGLContextFramebufferOperations.cpp
@@ -27,32 +27,25 @@ WebGLContext::Clear(GLbitfield mask)
 
     if (mask == 0) {
         GenerateWarning("Calling gl.clear(0) has no effect.");
     } else if (mRasterizerDiscardEnabled) {
         GenerateWarning("Calling gl.clear() with RASTERIZER_DISCARD enabled has no effects.");
     }
 
     if (mask & LOCAL_GL_COLOR_BUFFER_BIT && mBoundDrawFramebuffer) {
-        if (mask & LOCAL_GL_COLOR_BUFFER_BIT) {
-            for (const auto& cur : mBoundDrawFramebuffer->ColorDrawBuffers()) {
-                if (!cur->HasImage())
-                    continue;
+        for (const auto& cur : mBoundDrawFramebuffer->ColorDrawBuffers()) {
+            const auto imageInfo = cur->GetImageInfo();
+            if (!imageInfo || !imageInfo->mFormat)
+                continue;
 
-                switch (cur->Format()->format->componentType) {
-                case webgl::ComponentType::Float:
-                case webgl::ComponentType::NormInt:
-                case webgl::ComponentType::NormUInt:
-                    break;
-
-                default:
-                    ErrorInvalidOperation("Color draw buffers must be floating-point"
-                                          " or fixed-point. (normalized (u)ints)");
-                    return;
-                }
+            if (imageInfo->mFormat->format->baseType != webgl::TextureBaseType::Float) {
+                ErrorInvalidOperation("Color draw buffers must be floating-point"
+                                      " or fixed-point. (normalized (u)ints)");
+                return;
             }
         }
     }
 
     if (!BindCurFBForDraw())
         return;
 
     auto driverMask = mask;
--- a/dom/canvas/WebGLContextGL.cpp
+++ b/dom/canvas/WebGLContextGL.cpp
@@ -409,18 +409,16 @@ WebGLContext::DeleteRenderbuffer(WebGLRe
         return;
 
     if (mBoundDrawFramebuffer)
         mBoundDrawFramebuffer->DetachRenderbuffer(rbuf);
 
     if (mBoundReadFramebuffer)
         mBoundReadFramebuffer->DetachRenderbuffer(rbuf);
 
-    rbuf->InvalidateStatusOfAttachedFBs();
-
     if (mBoundRenderbuffer == rbuf)
         BindRenderbuffer(LOCAL_GL_RENDERBUFFER, nullptr);
 
     rbuf->RequestDelete();
 }
 
 void
 WebGLContext::DeleteTexture(WebGLTexture* tex)
@@ -1929,97 +1927,97 @@ public:
 };
 
 ////////////////////
 
 void
 WebGLContext::Uniform1i(WebGLUniformLocation* loc, GLint a1)
 {
     const FuncScope funcScope(*this, "uniform1i");
-    if (!ValidateUniformSetter(loc, 1, LOCAL_GL_INT))
+    if (!ValidateUniformSetter(loc, 1, webgl::AttribBaseType::Int))
         return;
 
     bool error;
     const ValidateIfSampler validate(this, loc, 1, &a1, &error);
     if (error)
         return;
 
     gl->fUniform1i(loc->mLoc, a1);
 }
 
 void
 WebGLContext::Uniform2i(WebGLUniformLocation* loc, GLint a1, GLint a2)
 {
     const FuncScope funcScope(*this, "uniform2i");
-    if (!ValidateUniformSetter(loc, 2, LOCAL_GL_INT))
+    if (!ValidateUniformSetter(loc, 2, webgl::AttribBaseType::Int))
         return;
 
     gl->fUniform2i(loc->mLoc, a1, a2);
 }
 
 void
 WebGLContext::Uniform3i(WebGLUniformLocation* loc, GLint a1, GLint a2, GLint a3)
 {
     const FuncScope funcScope(*this, "uniform3i");
-    if (!ValidateUniformSetter(loc, 3, LOCAL_GL_INT))
+    if (!ValidateUniformSetter(loc, 3, webgl::AttribBaseType::Int))
         return;
 
     gl->fUniform3i(loc->mLoc, a1, a2, a3);
 }
 
 void
 WebGLContext::Uniform4i(WebGLUniformLocation* loc, GLint a1, GLint a2, GLint a3,
                         GLint a4)
 {
     const FuncScope funcScope(*this, "uniform4i");
-    if (!ValidateUniformSetter(loc, 4, LOCAL_GL_INT))
+    if (!ValidateUniformSetter(loc, 4, webgl::AttribBaseType::Int))
         return;
 
     gl->fUniform4i(loc->mLoc, a1, a2, a3, a4);
 }
 
 //////////
 
 void
 WebGLContext::Uniform1f(WebGLUniformLocation* loc, GLfloat a1)
 {
     const FuncScope funcScope(*this, "uniform1f");
-    if (!ValidateUniformSetter(loc, 1, LOCAL_GL_FLOAT))
+    if (!ValidateUniformSetter(loc, 1, webgl::AttribBaseType::Float))
         return;
 
     gl->fUniform1f(loc->mLoc, a1);
 }
 
 void
 WebGLContext::Uniform2f(WebGLUniformLocation* loc, GLfloat a1, GLfloat a2)
 {
     const FuncScope funcScope(*this, "uniform2f");
-    if (!ValidateUniformSetter(loc, 2, LOCAL_GL_FLOAT))
+    if (!ValidateUniformSetter(loc, 2, webgl::AttribBaseType::Float))
         return;
 
     gl->fUniform2f(loc->mLoc, a1, a2);
 }
 
 void
 WebGLContext::Uniform3f(WebGLUniformLocation* loc, GLfloat a1, GLfloat a2,
                         GLfloat a3)
 {
     const FuncScope funcScope(*this, "uniform3f");
-    if (!ValidateUniformSetter(loc, 3, LOCAL_GL_FLOAT))
+    if (!ValidateUniformSetter(loc, 3, webgl::AttribBaseType::Float))
         return;
 
     gl->fUniform3f(loc->mLoc, a1, a2, a3);
 }
 
 void
 WebGLContext::Uniform4f(WebGLUniformLocation* loc, GLfloat a1, GLfloat a2,
                         GLfloat a3, GLfloat a4)
 {
     const FuncScope funcScope(*this, "uniform4f");
-    if (!ValidateUniformSetter(loc, 4, LOCAL_GL_FLOAT))
+    if (!ValidateUniformSetter(loc, 4, webgl::AttribBaseType::Float))
         return;
 
     gl->fUniform4f(loc->mLoc, a1, a2, a3, a4);
 }
 
 ////////////////////////////////////////
 // Array
 
@@ -2059,17 +2057,17 @@ WebGLContext::UniformNiv(const char* fun
     if (!ValidateArrOffsetAndCount(this, arr.elemCount, elemOffset,
                                    elemCountOverride, &elemCount))
     {
         return;
     }
     const auto elemBytes = arr.elemBytes + elemOffset;
 
     uint32_t numElementsToUpload;
-    if (!ValidateUniformArraySetter(loc, N, LOCAL_GL_INT, elemCount,
+    if (!ValidateUniformArraySetter(loc, N, webgl::AttribBaseType::Int, elemCount,
                                     &numElementsToUpload))
     {
         return;
     }
 
     bool error;
     const ValidateIfSampler samplerValidator(this, loc, numElementsToUpload,
                                              elemBytes, &error);
@@ -2098,17 +2096,17 @@ WebGLContext::UniformNuiv(const char* fu
     if (!ValidateArrOffsetAndCount(this, arr.elemCount, elemOffset,
                                    elemCountOverride, &elemCount))
     {
         return;
     }
     const auto elemBytes = arr.elemBytes + elemOffset;
 
     uint32_t numElementsToUpload;
-    if (!ValidateUniformArraySetter(loc, N, LOCAL_GL_UNSIGNED_INT, elemCount,
+    if (!ValidateUniformArraySetter(loc, N, webgl::AttribBaseType::UInt, elemCount,
                                     &numElementsToUpload))
     {
         return;
     }
     MOZ_ASSERT(!loc->mInfo->mSamplerTexList, "Should not be a sampler.");
 
     static const decltype(&gl::GLContext::fUniform1uiv) kFuncList[] = {
         &gl::GLContext::fUniform1uiv,
@@ -2132,17 +2130,17 @@ WebGLContext::UniformNfv(const char* fun
     if (!ValidateArrOffsetAndCount(this, arr.elemCount, elemOffset,
                                    elemCountOverride, &elemCount))
     {
         return;
     }
     const auto elemBytes = arr.elemBytes + elemOffset;
 
     uint32_t numElementsToUpload;
-    if (!ValidateUniformArraySetter(loc, N, LOCAL_GL_FLOAT, elemCount,
+    if (!ValidateUniformArraySetter(loc, N, webgl::AttribBaseType::Float, elemCount,
                                     &numElementsToUpload))
     {
         return;
     }
     MOZ_ASSERT(!loc->mInfo->mSamplerTexList, "Should not be a sampler.");
 
     static const decltype(&gl::GLContext::fUniform1fv) kFuncList[] = {
         &gl::GLContext::fUniform1fv,
@@ -2179,18 +2177,18 @@ WebGLContext::UniformMatrixAxBfv(const c
     if (!ValidateArrOffsetAndCount(this, arr.elemCount, elemOffset,
                                    elemCountOverride, &elemCount))
     {
         return;
     }
     const auto elemBytes = arr.elemBytes + elemOffset;
 
     uint32_t numMatsToUpload;
-    if (!ValidateUniformMatrixArraySetter(loc, A, B, LOCAL_GL_FLOAT, elemCount,
-                                          transpose, &numMatsToUpload))
+    if (!ValidateUniformMatrixArraySetter(loc, A, B, webgl::AttribBaseType::Float,
+                                          elemCount, transpose, &numMatsToUpload))
     {
         return;
     }
     MOZ_ASSERT(!loc->mInfo->mSamplerTexList, "Should not be a sampler.");
 
     ////
 
     bool uploadTranspose = transpose;
--- a/dom/canvas/WebGLContextState.cpp
+++ b/dom/canvas/WebGLContextState.cpp
@@ -50,26 +50,24 @@ WebGLContext::SetEnabled(const char* con
     }
 }
 
 bool
 WebGLContext::GetStencilBits(GLint* const out_stencilBits) const
 {
     *out_stencilBits = 0;
     if (mBoundDrawFramebuffer) {
-        if (mBoundDrawFramebuffer->StencilAttachment().IsDefined() &&
-            mBoundDrawFramebuffer->DepthStencilAttachment().IsDefined())
-        {
+        if (!mBoundDrawFramebuffer->IsCheckFramebufferStatusComplete()) {
             // Error, we don't know which stencil buffer's bits to use
             ErrorInvalidFramebufferOperation("getParameter: framebuffer has two stencil buffers bound");
             return false;
         }
 
-        if (mBoundDrawFramebuffer->StencilAttachment().IsDefined() ||
-            mBoundDrawFramebuffer->DepthStencilAttachment().IsDefined())
+        if (mBoundDrawFramebuffer->StencilAttachment().HasAttachment() ||
+            mBoundDrawFramebuffer->DepthStencilAttachment().HasAttachment())
         {
             *out_stencilBits = 8;
         }
     } else if (mOptions.stencil) {
         *out_stencilBits = 8;
     }
 
     return true;
@@ -313,33 +311,42 @@ WebGLContext::GetParameter(JSContext* cx
 
         case LOCAL_GL_RED_BITS:
         case LOCAL_GL_GREEN_BITS:
         case LOCAL_GL_BLUE_BITS:
         case LOCAL_GL_ALPHA_BITS:
         case LOCAL_GL_DEPTH_BITS:
         case LOCAL_GL_STENCIL_BITS: {
             const auto format = [&]() -> const webgl::FormatInfo* {
-                if (mBoundDrawFramebuffer) {
-                    const auto& fb = *mBoundDrawFramebuffer;
+                const auto& fb = mBoundDrawFramebuffer;
+                if (fb) {
+                    if (!fb->IsCheckFramebufferStatusComplete())
+                        return nullptr;
+
                     const auto& attachment = [&]() -> const auto& {
                         switch (pname) {
                         case LOCAL_GL_DEPTH_BITS:
-                            return fb.AnyDepthAttachment();
+                            if (fb->DepthStencilAttachment().HasAttachment())
+                                return fb->DepthStencilAttachment();
+                            return fb->DepthAttachment();
 
                         case LOCAL_GL_STENCIL_BITS:
-                            return fb.AnyStencilAttachment();
+                            if (fb->DepthStencilAttachment().HasAttachment())
+                                return fb->DepthStencilAttachment();
+                            return fb->StencilAttachment();
 
                         default:
-                            return fb.ColorAttachment0();
+                            return fb->ColorAttachment0();
                         }
                     }();
-                    if (!attachment.HasImage())
+
+                    const auto imageInfo = attachment.GetImageInfo();
+                    if (!imageInfo)
                         return nullptr;
-                    return attachment.Format()->format;
+                    return imageInfo->mFormat->format;
                 }
 
                 auto effFormat = webgl::EffectiveFormat::RGB8;
                 switch (pname) {
                 case LOCAL_GL_DEPTH_BITS:
                     if (mOptions.depth) {
                         effFormat = webgl::EffectiveFormat::DEPTH24_STENCIL8;
                     }
--- a/dom/canvas/WebGLContextTextures.cpp
+++ b/dom/canvas/WebGLContextTextures.cpp
@@ -182,29 +182,16 @@ WebGLContext::IsTexParamValid(GLenum pna
     case LOCAL_GL_TEXTURE_MAX_ANISOTROPY_EXT:
         return IsExtensionEnabled(WebGLExtensionID::EXT_texture_filter_anisotropic);
 
     default:
         return false;
     }
 }
 
-void
-WebGLContext::InvalidateResolveCacheForTextureWithTexUnit(const GLuint texUnit)
-{
-    if (mBound2DTextures[texUnit])
-        mBound2DTextures[texUnit]->InvalidateResolveCache();
-    if (mBoundCubeMapTextures[texUnit])
-        mBoundCubeMapTextures[texUnit]->InvalidateResolveCache();
-    if (mBound3DTextures[texUnit])
-        mBound3DTextures[texUnit]->InvalidateResolveCache();
-    if (mBound2DArrayTextures[texUnit])
-        mBound2DArrayTextures[texUnit]->InvalidateResolveCache();
-}
-
 //////////////////////////////////////////////////////////////////////////////////////////
 // GL calls
 
 void
 WebGLContext::BindTexture(GLenum rawTarget, WebGLTexture* newTex)
 {
     const FuncScope funcScope(*this, "bindTexture");
     if (IsContextLost())
@@ -258,17 +245,17 @@ WebGLContext::GenerateMipmap(GLenum rawT
     const FuncScope funcScope(*this, "generateMipmap");
     const uint8_t funcDims = 0;
 
     TexTarget texTarget;
     WebGLTexture* tex;
     if (!ValidateTexTarget(this, funcDims, rawTexTarget, &texTarget, &tex))
         return;
 
-    tex->GenerateMipmap(texTarget);
+    tex->GenerateMipmap();
 }
 
 JS::Value
 WebGLContext::GetTexParameter(GLenum rawTexTarget, GLenum pname)
 {
     const FuncScope funcScope(*this, "getTexParameter");
     const uint8_t funcDims = 0;
 
--- a/dom/canvas/WebGLContextValidate.cpp
+++ b/dom/canvas/WebGLContextValidate.cpp
@@ -178,17 +178,17 @@ WebGLContext::ValidateFaceEnum(const GLe
 
     default:
         ErrorInvalidEnumInfo("face", face);
         return false;
     }
 }
 
 bool
-WebGLContext::ValidateUniformLocation(WebGLUniformLocation* loc)
+WebGLContext::ValidateUniformLocation(const WebGLUniformLocation* const loc)
 {
     /* GLES 2.0.25, p38:
      *   If the value of location is -1, the Uniform* commands will silently
      *   ignore the data passed in, and the current uniform values will not be
      *   changed.
      */
     if (!loc)
         return false;
@@ -214,36 +214,37 @@ WebGLContext::ValidateAttribArraySetter(
         ErrorInvalidValue("Array must have >= %d elements.", setterElemSize);
         return false;
     }
 
     return true;
 }
 
 bool
-WebGLContext::ValidateUniformSetter(WebGLUniformLocation* loc,
-                                    uint8_t setterElemSize, GLenum setterType)
+WebGLContext::ValidateUniformSetter(const WebGLUniformLocation* const loc,
+                                    const uint8_t setterElemSize,
+                                    const webgl::AttribBaseType setterType)
 {
     if (IsContextLost())
         return false;
 
     if (!ValidateUniformLocation(loc))
         return false;
 
     if (!loc->ValidateSizeAndType(setterElemSize, setterType))
         return false;
 
     return true;
 }
 
 bool
-WebGLContext::ValidateUniformArraySetter(WebGLUniformLocation* loc,
-                                         uint8_t setterElemSize,
-                                         GLenum setterType,
-                                         uint32_t setterArraySize,
+WebGLContext::ValidateUniformArraySetter(const WebGLUniformLocation* const loc,
+                                         const uint8_t setterElemSize,
+                                         const webgl::AttribBaseType setterType,
+                                         const uint32_t setterArraySize,
                                          uint32_t* const out_numElementsToUpload)
 {
     if (IsContextLost())
         return false;
 
     if (!ValidateUniformLocation(loc))
         return false;
 
@@ -258,22 +259,22 @@ WebGLContext::ValidateUniformArraySetter
     const uint32_t uniformElemCount = elemCount - loc->mArrayIndex;
 
     *out_numElementsToUpload = std::min(uniformElemCount,
                                         setterArraySize / setterElemSize);
     return true;
 }
 
 bool
-WebGLContext::ValidateUniformMatrixArraySetter(WebGLUniformLocation* loc,
-                                               uint8_t setterCols,
-                                               uint8_t setterRows,
-                                               GLenum setterType,
-                                               uint32_t setterArraySize,
-                                               bool setterTranspose,
+WebGLContext::ValidateUniformMatrixArraySetter(const WebGLUniformLocation* const loc,
+                                               const uint8_t setterCols,
+                                               const uint8_t setterRows,
+                                               const webgl::AttribBaseType setterType,
+                                               const uint32_t setterArraySize,
+                                               const bool setterTranspose,
                                                uint32_t* const out_numElementsToUpload)
 {
     const uint8_t setterElemSize = setterCols * setterRows;
 
     if (IsContextLost())
         return false;
 
     if (!ValidateUniformLocation(loc))
@@ -657,18 +658,18 @@ WebGLContext::InitAndValidateGL(FailureR
     mPixelStore_UnpackAlignment = 4;
     mPixelStore_PackRowLength = 0;
     mPixelStore_PackSkipRows = 0;
     mPixelStore_PackSkipPixels = 0;
     mPixelStore_PackAlignment = 4;
 
     mPrimRestartTypeBytes = 0;
 
-    mGenericVertexAttribTypes.reset(new GLenum[mGLMaxVertexAttribs]);
-    std::fill_n(mGenericVertexAttribTypes.get(), mGLMaxVertexAttribs, LOCAL_GL_FLOAT);
+    mGenericVertexAttribTypes.assign(mGLMaxVertexAttribs,
+                                     webgl::AttribBaseType::Float);
     mGenericVertexAttribTypeInvalidator.InvalidateCaches();
 
     static const float kDefaultGenericVertexAttribData[4] = { 0, 0, 0, 1 };
     memcpy(mGenericVertexAttrib0Data, kDefaultGenericVertexAttribData,
            sizeof(mGenericVertexAttrib0Data));
 
     mFakeVertexAttrib0BufferObject = 0;
 
--- a/dom/canvas/WebGLContextVertices.cpp
+++ b/dom/canvas/WebGLContextVertices.cpp
@@ -93,17 +93,17 @@ WebGLContext::VertexAttrib4f(GLuint inde
     ////
 
     if (index || !gl->IsCompatibilityProfile()) {
         gl->fVertexAttrib4f(index, x, y, z, w);
     }
 
     ////
 
-    mGenericVertexAttribTypes[index] = LOCAL_GL_FLOAT;
+    mGenericVertexAttribTypes[index] = webgl::AttribBaseType::Float;
     mGenericVertexAttribTypeInvalidator.InvalidateCaches();
 
     if (!index) {
         const float data[4] = { x, y, z, w };
         memcpy(mGenericVertexAttrib0Data, data, sizeof(data));
     }
 }
 
@@ -120,17 +120,17 @@ WebGL2Context::VertexAttribI4i(GLuint in
     ////
 
     if (index || !gl->IsCompatibilityProfile()) {
         gl->fVertexAttribI4i(index, x, y, z, w);
     }
 
     ////
 
-    mGenericVertexAttribTypes[index] = LOCAL_GL_INT;
+    mGenericVertexAttribTypes[index] = webgl::AttribBaseType::Int;
     mGenericVertexAttribTypeInvalidator.InvalidateCaches();
 
     if (!index) {
         const int32_t data[4] = { x, y, z, w };
         memcpy(mGenericVertexAttrib0Data, data, sizeof(data));
     }
 }
 
@@ -147,17 +147,17 @@ WebGL2Context::VertexAttribI4ui(GLuint i
     ////
 
     if (index || !gl->IsCompatibilityProfile()) {
         gl->fVertexAttribI4ui(index, x, y, z, w);
     }
 
     ////
 
-    mGenericVertexAttribTypes[index] = LOCAL_GL_UNSIGNED_INT;
+    mGenericVertexAttribTypes[index] = webgl::AttribBaseType::UInt;
     mGenericVertexAttribTypeInvalidator.InvalidateCaches();
 
     if (!index) {
         const uint32_t data[4] = { x, y, z, w };
         memcpy(mGenericVertexAttrib0Data, data, sizeof(data));
     }
 }
 
@@ -238,27 +238,30 @@ WebGLContext::GetVertexAttrib(JSContext*
             return JS::Int32Value(mBoundVertexArray->mAttribs[index].mDivisor);
         }
         break;
 
     case LOCAL_GL_CURRENT_VERTEX_ATTRIB:
         {
             JS::RootedObject obj(cx);
             switch (mGenericVertexAttribTypes[index]) {
-            case LOCAL_GL_FLOAT:
+            case webgl::AttribBaseType::Float:
                 obj = GetVertexAttribFloat32Array(cx, index);
                 break;
 
-            case LOCAL_GL_INT:
+            case webgl::AttribBaseType::Int:
                 obj =  GetVertexAttribInt32Array(cx, index);
                 break;
 
-            case LOCAL_GL_UNSIGNED_INT:
+            case webgl::AttribBaseType::UInt:
                 obj = GetVertexAttribUint32Array(cx, index);
                 break;
+
+            case webgl::AttribBaseType::Bool:
+                MOZ_CRASH("impossible");
             }
 
             if (!obj) {
                 rv.Throw(NS_ERROR_OUT_OF_MEMORY);
                 return JS::NullValue();
             }
             return JS::ObjectValue(*obj);
         }
--- a/dom/canvas/WebGLFormats.cpp
+++ b/dom/canvas/WebGLFormats.cpp
@@ -9,16 +9,49 @@
 #include "GLContext.h"
 #include "GLDefs.h"
 #include "mozilla/gfx/Logging.h"
 #include "mozilla/StaticMutex.h"
 
 namespace mozilla {
 namespace webgl {
 
+static TextureBaseType
+ToBaseType(const ComponentType type)
+{
+    switch (type) {
+    case ComponentType::Int:
+        return TextureBaseType::Int;
+    case ComponentType::UInt:
+        return TextureBaseType::UInt;
+    case ComponentType::NormInt:
+    case ComponentType::NormUInt:
+    case ComponentType::Float:
+    //case ComponentType::Depth:
+        return TextureBaseType::Float;
+    }
+    MOZ_CRASH("pacify gcc6 warning");
+}
+
+const char*
+ToString(const TextureBaseType x)
+{
+    switch (x) {
+    case webgl::TextureBaseType::Float:
+        return "FLOAT";
+    case webgl::TextureBaseType::Int:
+        return "INT";
+    case webgl::TextureBaseType::UInt:
+        return "UINT";
+    }
+    MOZ_CRASH("pacify gcc6 warning");
+}
+
+// -
+
 template<typename K, typename V, typename K2, typename V2>
 static inline void
 AlwaysInsert(std::map<K,V>& dest, const K2& key, const V2& val)
 {
     auto res = dest.insert({ key, val });
     bool didInsert = res.second;
     MOZ_ALWAYS_TRUE(didInsert);
 }
@@ -226,17 +259,18 @@ AddFormatInfo(EffectiveFormat format, co
         MOZ_ASSERT(totalBits);
         MOZ_ASSERT(!bytesPerPixel);
     } else {
         MOZ_ASSERT(totalBits == bytesPerPixel*8);
     }
 #endif
 
     const FormatInfo info = { format, name, sizedFormat, unsizedFormat, componentType,
-                              isSRGB, compressedFormatInfo, bytesPerPixel, r,g,b,a,d,s };
+                              ToBaseType(componentType), isSRGB, compressedFormatInfo,
+                              bytesPerPixel, r,g,b,a,d,s };
     AlwaysInsert(gFormatInfoMap, format, info);
 }
 
 static void
 InitFormatInfo()
 {
 #define FOO(x) EffectiveFormat::x, #x, LOCAL_GL_ ## x
 
@@ -297,21 +331,22 @@ InitFormatInfo()
     AddFormatInfo(FOO(RGBA16UI      ),  8, 16,16,16,16,  0,0, UnsizedFormat::RGBA, false, ComponentType::UInt    );
     AddFormatInfo(FOO(RGBA32I       ), 16, 32,32,32,32,  0,0, UnsizedFormat::RGBA, false, ComponentType::Int     );
     AddFormatInfo(FOO(RGBA32UI      ), 16, 32,32,32,32,  0,0, UnsizedFormat::RGBA, false, ComponentType::UInt    );
 
     // GLES 3.0.4, p133, table 3.14
     AddFormatInfo(FOO(DEPTH_COMPONENT16 ), 2, 0,0,0,0, 16,0, UnsizedFormat::D , false, ComponentType::NormUInt);
     AddFormatInfo(FOO(DEPTH_COMPONENT24 ), 3, 0,0,0,0, 24,0, UnsizedFormat::D , false, ComponentType::NormUInt);
     AddFormatInfo(FOO(DEPTH_COMPONENT32F), 4, 0,0,0,0, 32,0, UnsizedFormat::D , false, ComponentType::Float);
-    AddFormatInfo(FOO(DEPTH24_STENCIL8  ), 4, 0,0,0,0, 24,8, UnsizedFormat::DEPTH_STENCIL, false, ComponentType::Special);
-    AddFormatInfo(FOO(DEPTH32F_STENCIL8 ), 5, 0,0,0,0, 32,8, UnsizedFormat::DEPTH_STENCIL, false, ComponentType::Special);
+    // DEPTH_STENCIL types are sampled as their depth component.
+    AddFormatInfo(FOO(DEPTH24_STENCIL8  ), 4, 0,0,0,0, 24,8, UnsizedFormat::DEPTH_STENCIL, false, ComponentType::NormUInt);
+    AddFormatInfo(FOO(DEPTH32F_STENCIL8 ), 5, 0,0,0,0, 32,8, UnsizedFormat::DEPTH_STENCIL, false, ComponentType::Float);
 
     // GLES 3.0.4, p205-206, "Required Renderbuffer Formats"
-    AddFormatInfo(FOO(STENCIL_INDEX8), 1, 0,0,0,0, 0,8, UnsizedFormat::S, false, ComponentType::UInt);
+    AddFormatInfo(FOO(STENCIL_INDEX8), 1, 0,0,0,0, 0,8, UnsizedFormat::S, false, ComponentType::Int);
 
     // GLES 3.0.4, p147, table 3.19
     // GLES 3.0.4  p286+  $C.1 "ETC Compressed Texture Image Formats"
     AddFormatInfo(FOO(COMPRESSED_RGB8_ETC2                     ), 0, 1,1,1,0, 0,0, UnsizedFormat::RGB , false, ComponentType::NormUInt);
     AddFormatInfo(FOO(COMPRESSED_SRGB8_ETC2                    ), 0, 1,1,1,0, 0,0, UnsizedFormat::RGB , true , ComponentType::NormUInt);
     AddFormatInfo(FOO(COMPRESSED_RGBA8_ETC2_EAC                ), 0, 1,1,1,1, 0,0, UnsizedFormat::RGBA, false, ComponentType::NormUInt);
     AddFormatInfo(FOO(COMPRESSED_SRGB8_ALPHA8_ETC2_EAC         ), 0, 1,1,1,1, 0,0, UnsizedFormat::RGBA, true , ComponentType::NormUInt);
     AddFormatInfo(FOO(COMPRESSED_R11_EAC                       ), 0, 1,0,0,0, 0,0, UnsizedFormat::R   , false, ComponentType::NormUInt);
--- a/dom/canvas/WebGLFormats.h
+++ b/dom/canvas/WebGLFormats.h
@@ -202,25 +202,31 @@ enum class UnsizedFormat : uint8_t {
     A,
     D,
     S,
     DEPTH_STENCIL, // `DS` is a macro on Solaris. (regset.h)
 };
 
 // GLES 3.0.4 p114 Table 3.4, p240
 enum class ComponentType : uint8_t {
-    None,
     Int,          // RGBA32I
-    UInt,         // RGBA32UI, STENCIL_INDEX8
+    UInt,         // RGBA32UI
     NormInt,      // RGBA8_SNORM
-    NormUInt,     // RGBA8, DEPTH_COMPONENT16
+    NormUInt,     // RGBA8
     Float,        // RGBA32F
-    Special,      // DEPTH24_STENCIL8
 };
 
+enum class TextureBaseType : uint8_t {
+    Int = uint8_t(ComponentType::Int),
+    UInt = uint8_t(ComponentType::UInt),
+    Float = uint8_t(ComponentType::Float), // Also includes NormU?Int and Depth
+};
+
+const char* ToString(TextureBaseType);
+
 enum class CompressionFamily : uint8_t {
     ASTC,
     BPTC,
     ES3, // ETC2 or EAC
     ETC1,
     PVRTC,
     RGTC,
     S3TC,
@@ -239,16 +245,17 @@ struct CompressedFormatInfo
 
 struct FormatInfo
 {
     const EffectiveFormat effectiveFormat;
     const char* const name;
     const GLenum sizedFormat;
     const UnsizedFormat unsizedFormat;
     const ComponentType componentType;
+    const TextureBaseType baseType;
     const bool isSRGB;
 
     const CompressedFormatInfo* const compression;
 
     const uint8_t estimatedBytesPerPixel; // 0 iff bool(compression).
 
     // In bits. Iff bool(compression), active channels are 1.
     const uint8_t r;
--- a/dom/canvas/WebGLFramebuffer.cpp
+++ b/dom/canvas/WebGLFramebuffer.cpp
@@ -16,287 +16,148 @@
 #include "WebGLContext.h"
 #include "WebGLContextUtils.h"
 #include "WebGLExtensions.h"
 #include "WebGLRenderbuffer.h"
 #include "WebGLTexture.h"
 
 namespace mozilla {
 
-WebGLFBAttachPoint::WebGLFBAttachPoint()
-    : mFB(nullptr)
-    , mAttachmentPoint(0)
-    , mTexImageTarget(LOCAL_GL_NONE)
-    , mTexImageLayer(0)
-    , mTexImageLevel(0)
-{ }
+static bool
+ShouldDeferAttachment(const WebGLContext* const webgl, const GLenum attachPoint)
+{
+    if (webgl->IsWebGL2())
+        return false;
 
-WebGLFBAttachPoint::WebGLFBAttachPoint(WebGLFramebuffer* fb, GLenum attachmentPoint)
-    : mFB(fb)
-    , mAttachmentPoint(attachmentPoint)
-    , mTexImageTarget(LOCAL_GL_NONE)
-    , mTexImageLayer(0)
-    , mTexImageLevel(0)
+    switch (attachPoint) {
+    case LOCAL_GL_DEPTH_ATTACHMENT:
+    case LOCAL_GL_STENCIL_ATTACHMENT:
+    case LOCAL_GL_DEPTH_STENCIL_ATTACHMENT:
+        return true;
+    default:
+        return false;
+    }
+}
+
+WebGLFBAttachPoint::WebGLFBAttachPoint(const WebGLContext* const webgl,
+                                       const GLenum attachmentPoint)
+    : mAttachmentPoint(attachmentPoint)
+    , mDeferAttachment(ShouldDeferAttachment(webgl, mAttachmentPoint))
 { }
 
 WebGLFBAttachPoint::~WebGLFBAttachPoint()
 {
-    MOZ_ASSERT(mFB, "Should have been Init'd.");
     MOZ_ASSERT(!mRenderbufferPtr);
     MOZ_ASSERT(!mTexturePtr);
 }
 
-void
-WebGLFBAttachPoint::Unlink()
-{
-    Clear();
-}
-
 bool
 WebGLFBAttachPoint::IsDeleteRequested() const
 {
     return Texture() ? Texture()->IsDeleteRequested()
          : Renderbuffer() ? Renderbuffer()->IsDeleteRequested()
          : false;
 }
 
-bool
-WebGLFBAttachPoint::IsDefined() const
-{
-    /*
-    return (Renderbuffer() && Renderbuffer()->IsDefined()) ||
-           (Texture() && Texture()->ImageInfoAt(mTexImageTarget,
-                                                mTexImageLevel).IsDefined());
-    */
-    return (Renderbuffer() || Texture());
-}
-
-const webgl::FormatUsageInfo*
-WebGLFBAttachPoint::Format() const
+void
+WebGLFBAttachPoint::Clear()
 {
-    MOZ_ASSERT(IsDefined());
-
-    if (Texture())
-        return Texture()->ImageInfoAt(mTexImageTarget, mTexImageLevel).mFormat;
-
-    if (Renderbuffer())
-        return Renderbuffer()->Format();
-
-    return nullptr;
-}
-
-uint32_t
-WebGLFBAttachPoint::Samples() const
-{
-    MOZ_ASSERT(IsDefined());
-
-    if (mRenderbufferPtr)
-        return mRenderbufferPtr->Samples();
-
-    return 0;
-}
-
-bool
-WebGLFBAttachPoint::HasAlpha() const
-{
-    return Format()->format->a;
-}
-
-bool
-WebGLFBAttachPoint::IsReadableFloat() const
-{
-    auto formatUsage = Format();
-    MOZ_ASSERT(formatUsage);
-
-    auto format = formatUsage->format;
-    if (!format->IsColorFormat())
-        return false;
-
-    return format->componentType == webgl::ComponentType::Float;
+    mRenderbufferPtr = nullptr;
+    mTexturePtr = nullptr;
+    mTexImageTarget = 0;
+    mTexImageLevel = 0;
+    mTexImageLayer = 0;
 }
 
 void
-WebGLFBAttachPoint::Clear()
-{
-    if (mRenderbufferPtr) {
-        MOZ_ASSERT(!mTexturePtr);
-        mRenderbufferPtr->UnmarkAttachment(*this);
-    } else if (mTexturePtr) {
-        mTexturePtr->ImageInfoAt(mTexImageTarget, mTexImageLevel).RemoveAttachPoint(this);
-    }
-
-    mTexturePtr = nullptr;
-    mRenderbufferPtr = nullptr;
-
-    OnBackingStoreRespecified();
-}
-
-void
-WebGLFBAttachPoint::SetTexImage(WebGLTexture* tex,
+WebGLFBAttachPoint::SetTexImage(gl::GLContext* const gl, WebGLTexture* const tex,
                                 TexImageTarget target, GLint level, GLint layer)
 {
     Clear();
 
     mTexturePtr = tex;
     mTexImageTarget = target;
     mTexImageLevel = level;
     mTexImageLayer = layer;
 
-    if (mTexturePtr) {
-        mTexturePtr->ImageInfoAt(mTexImageTarget, mTexImageLevel).AddAttachPoint(this);
+    if (!mDeferAttachment) {
+        DoAttachment(gl);
     }
 }
 
 void
-WebGLFBAttachPoint::SetRenderbuffer(WebGLRenderbuffer* rb)
+WebGLFBAttachPoint::SetRenderbuffer(gl::GLContext* const gl, WebGLRenderbuffer* const rb)
 {
     Clear();
 
     mRenderbufferPtr = rb;
 
-    if (mRenderbufferPtr) {
-        mRenderbufferPtr->MarkAttachment(*this);
+    if (!mDeferAttachment) {
+        DoAttachment(gl);
     }
 }
 
-bool
-WebGLFBAttachPoint::HasUninitializedImageData() const
-{
-    if (!HasImage())
-        return false;
-
-    if (mRenderbufferPtr)
-        return mRenderbufferPtr->HasUninitializedImageData();
-
-    MOZ_ASSERT(mTexturePtr);
-
-    auto& imageInfo = mTexturePtr->ImageInfoAt(mTexImageTarget, mTexImageLevel);
-    MOZ_ASSERT(imageInfo.IsDefined());
-
-    return !imageInfo.IsDataInitialized();
-}
-
-void
-WebGLFBAttachPoint::SetImageDataStatus(WebGLImageDataStatus newStatus) const
-{
-    if (!HasImage())
-        return;
-
-    if (mRenderbufferPtr) {
-        mRenderbufferPtr->mImageDataStatus = newStatus;
-        return;
-    }
-
-    MOZ_ASSERT(mTexturePtr);
-
-    auto& imageInfo = mTexturePtr->ImageInfoAt(mTexImageTarget, mTexImageLevel);
-    MOZ_ASSERT(imageInfo.IsDefined());
-
-    const bool isDataInitialized = (newStatus == WebGLImageDataStatus::InitializedImageData);
-    imageInfo.SetIsDataInitialized(isDataInitialized, mTexturePtr);
-}
-
-bool
-WebGLFBAttachPoint::HasImage() const
+const webgl::ImageInfo*
+WebGLFBAttachPoint::GetImageInfo() const
 {
-    if (Texture() && Texture()->ImageInfoAt(mTexImageTarget, mTexImageLevel).IsDefined())
-        return true;
-
-    if (Renderbuffer() && Renderbuffer()->IsDefined())
-        return true;
-
-    return false;
-}
-
-void
-WebGLFBAttachPoint::Size(uint32_t* const out_width, uint32_t* const out_height) const
-{
-    MOZ_ASSERT(HasImage());
-
-    if (Renderbuffer()) {
-        *out_width = Renderbuffer()->Width();
-        *out_height = Renderbuffer()->Height();
-        return;
-    }
-
-    MOZ_ASSERT(Texture());
-    MOZ_ASSERT(Texture()->ImageInfoAt(mTexImageTarget, mTexImageLevel).IsDefined());
-    const auto& imageInfo = Texture()->ImageInfoAt(mTexImageTarget, mTexImageLevel);
-
-    *out_width = imageInfo.mWidth;
-    *out_height = imageInfo.mHeight;
-}
-
-void
-WebGLFBAttachPoint::OnBackingStoreRespecified() const
-{
-    mFB->InvalidateFramebufferStatus();
-}
-
-void
-WebGLFBAttachPoint::AttachmentName(nsCString* out) const
-{
-    switch (mAttachmentPoint) {
-    case LOCAL_GL_DEPTH_ATTACHMENT:
-        out->AssignLiteral("DEPTH_ATTACHMENT");
-        return;
-
-    case LOCAL_GL_STENCIL_ATTACHMENT:
-        out->AssignLiteral("STENCIL_ATTACHMENT");
-        return;
-
-    case LOCAL_GL_DEPTH_STENCIL_ATTACHMENT:
-        out->AssignLiteral("DEPTH_STENCIL_ATTACHMENT");
-        return;
-
-    default:
-        MOZ_ASSERT(mAttachmentPoint >= LOCAL_GL_COLOR_ATTACHMENT0);
-        out->AssignLiteral("COLOR_ATTACHMENT");
-        const uint32_t n = mAttachmentPoint - LOCAL_GL_COLOR_ATTACHMENT0;
-        out->AppendInt(n);
-        return;
-    }
+    if (mTexturePtr)
+        return &mTexturePtr->ImageInfoAt(mTexImageTarget, mTexImageLevel);
+    if (mRenderbufferPtr)
+        return &mRenderbufferPtr->ImageInfo();
+    return nullptr;
 }
 
 bool
 WebGLFBAttachPoint::IsComplete(WebGLContext* webgl, nsCString* const out_info) const
 {
-    MOZ_ASSERT(IsDefined());
+    MOZ_ASSERT(HasAttachment());
 
-    if (!HasImage()) {
-        AttachmentName(out_info);
-        out_info->AppendLiteral("'s image is not defined");
+    const auto fnWriteErrorInfo = [&](const char* const text) {
+        WebGLContext::EnumName(mAttachmentPoint, out_info);
+        out_info->AppendASCII(text);
+    };
+
+    const auto& imageInfo = *GetImageInfo();
+    if (!imageInfo.mWidth || !imageInfo.mHeight) {
+        fnWriteErrorInfo("Attachment has no width or height.");
         return false;
     }
+    MOZ_ASSERT(imageInfo.IsDefined());
 
-    uint32_t width;
-    uint32_t height;
-    Size(&width, &height);
-    if (!width || !height) {
-        AttachmentName(out_info);
-        out_info->AppendLiteral(" has no width or height");
-        return false;
+    const auto& tex = Texture();
+    if (tex) {
+        // ES 3.0 spec, pg 213 has giant blocks of text that bake down to requiring that
+        // attached tex images are within the valid mip-levels of the texture.
+        // While it draws distinction to only test non-immutable textures, that's because
+        // immutable textures are *always* texture-complete.
+        // We need to check immutable textures though, because checking completeness is
+        // also when we zero invalidated/no-data tex images.
+        const bool complete = [&]() {
+            const auto texCompleteness = tex->CalcCompletenessInfo();
+            if (!texCompleteness) // OOM
+                return false;
+            if (!texCompleteness->levels)
+                return false;
+
+            const auto baseLevel = tex->BaseMipmapLevel();
+            const auto maxLevel = baseLevel + texCompleteness->levels - 1;
+            return baseLevel <= mTexImageLevel && mTexImageLevel <= maxLevel;
+        }();
+        if (!complete) {
+            fnWriteErrorInfo("Attached texture is not texture-complete.");
+            return false;
+        }
     }
 
-    const auto formatUsage = Format();
+    const auto& formatUsage = imageInfo.mFormat;
     if (!formatUsage->IsRenderable()) {
-        nsAutoCString attachName;
-        AttachmentName(&attachName);
-
-        *out_info = nsPrintfCString("%s has an effective format of %s, which is not"
-                                    " renderable",
-                                    attachName.BeginReading(), formatUsage->format->name);
-        return false;
-    }
-
-    if (webgl->IsWebGL2() && Texture() &&
-        Texture()->IsCubeMap() && !Texture()->IsCubeComplete())
-    {
-        AttachmentName(out_info);
-        out_info->AppendLiteral(" is not cube complete");
+        const auto info = nsPrintfCString("Attachment has an effective format of %s,"
+                                          " which is not renderable.",
+                                          formatUsage->format->name);
+        fnWriteErrorInfo(info.BeginReading());
         return false;
     }
 
     const auto format = formatUsage->format;
 
     bool hasRequiredBits;
 
     switch (mAttachmentPoint) {
@@ -315,18 +176,18 @@ WebGLFBAttachPoint::IsComplete(WebGLCont
 
     default:
         MOZ_ASSERT(mAttachmentPoint >= LOCAL_GL_COLOR_ATTACHMENT0);
         hasRequiredBits = format->IsColorFormat();
         break;
     }
 
     if (!hasRequiredBits) {
-        AttachmentName(out_info);
-        out_info->AppendLiteral("'s format is missing required color/depth/stencil bits");
+        fnWriteErrorInfo("Attachment's format is missing required color/depth/stencil"
+                         " bits.");
         return false;
     }
 
     if (!webgl->IsWebGL2()) {
         bool hasSurplusPlanes = false;
 
         switch (mAttachmentPoint) {
         case LOCAL_GL_DEPTH_ATTACHMENT:
@@ -334,84 +195,77 @@ WebGLFBAttachPoint::IsComplete(WebGLCont
             break;
 
         case LOCAL_GL_STENCIL_ATTACHMENT:
             hasSurplusPlanes = format->d;
             break;
         }
 
         if (hasSurplusPlanes) {
-            AttachmentName(out_info);
-            out_info->AppendLiteral("'s format has depth or stencil bits when it"
-                                    " shouldn't");
+            fnWriteErrorInfo("Attachment has depth or stencil bits when it shouldn't.");
             return false;
         }
     }
 
     return true;
 }
 
 void
-WebGLFBAttachPoint::Resolve(gl::GLContext* gl) const
+WebGLFBAttachPoint::DoAttachment(gl::GLContext* const gl) const
 {
-    if (!HasImage())
-        return;
-
     if (Renderbuffer()) {
-        Renderbuffer()->DoFramebufferRenderbuffer(LOCAL_GL_FRAMEBUFFER, mAttachmentPoint);
+        Renderbuffer()->DoFramebufferRenderbuffer(mAttachmentPoint);
         return;
     }
-    MOZ_ASSERT(Texture());
 
-    MOZ_ASSERT(gl == Texture()->mContext->GL());
+    if (!Texture()) {
+        MOZ_ASSERT(mAttachmentPoint != LOCAL_GL_DEPTH_STENCIL_ATTACHMENT);
+        // WebGL 2 doesn't have a real attachment for this, and WebGL 1 is defered and
+        // only DoAttachment if HasAttachment.
+
+        gl->fFramebufferRenderbuffer(LOCAL_GL_FRAMEBUFFER, mAttachmentPoint,
+                                     LOCAL_GL_RENDERBUFFER, 0);
+        return;
+    }
 
     const auto& texName = Texture()->mGLName;
 
-    ////
-
-    const auto fnAttach2D = [&](GLenum attachmentPoint) {
-        gl->fFramebufferTexture2D(LOCAL_GL_FRAMEBUFFER, attachmentPoint,
-                                  mTexImageTarget.get(), texName, mTexImageLevel);
-    };
-
-    ////
-
     switch (mTexImageTarget.get()) {
     case LOCAL_GL_TEXTURE_2D:
     case LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_X:
     case LOCAL_GL_TEXTURE_CUBE_MAP_NEGATIVE_X:
     case LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_Y:
     case LOCAL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Y:
     case LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_Z:
     case LOCAL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Z:
         if (mAttachmentPoint == LOCAL_GL_DEPTH_STENCIL_ATTACHMENT) {
-            fnAttach2D(LOCAL_GL_DEPTH_ATTACHMENT);
-            fnAttach2D(LOCAL_GL_STENCIL_ATTACHMENT);
+            gl->fFramebufferTexture2D(LOCAL_GL_FRAMEBUFFER, LOCAL_GL_DEPTH_ATTACHMENT,
+                                      mTexImageTarget.get(), texName, mTexImageLevel);
+            gl->fFramebufferTexture2D(LOCAL_GL_FRAMEBUFFER, LOCAL_GL_STENCIL_ATTACHMENT,
+                                      mTexImageTarget.get(), texName, mTexImageLevel);
         } else {
-            fnAttach2D(mAttachmentPoint);
+            gl->fFramebufferTexture2D(LOCAL_GL_FRAMEBUFFER, mAttachmentPoint,
+                                      mTexImageTarget.get(), texName, mTexImageLevel);
         }
         break;
 
     case LOCAL_GL_TEXTURE_2D_ARRAY:
     case LOCAL_GL_TEXTURE_3D:
-        // If we have fFramebufferTextureLayer, we can rely on having
-        // DEPTH_STENCIL_ATTACHMENT.
         gl->fFramebufferTextureLayer(LOCAL_GL_FRAMEBUFFER, mAttachmentPoint, texName,
                                      mTexImageLevel, mTexImageLayer);
         break;
     }
 }
 
 JS::Value
 WebGLFBAttachPoint::GetParameter(WebGLContext* webgl, JSContext* cx,
                                  GLenum target, GLenum attachment, GLenum pname,
                                  ErrorResult* const out_error) const
 {
-    const bool hasAttachment = (mTexturePtr || mRenderbufferPtr);
-    if (!hasAttachment) {
+    if (!HasAttachment()) {
         // Divergent between GLES 3 and 2.
 
         // GLES 2.0.25 p127:
         // "If the value of FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE is NONE, then querying any
         //  other pname will generate INVALID_ENUM."
 
         // GLES 3.0.4 p240:
         // "If the value of FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE is NONE, no framebuffer is
@@ -504,17 +358,18 @@ WebGLFBAttachPoint::GetParameter(WebGLCo
         break;
     }
 
     if (!isPNameValid) {
         webgl->ErrorInvalidEnum("Invalid pname: 0x%04x", pname);
         return JS::NullValue();
     }
 
-    const auto usage = Format();
+    const auto& imageInfo = *GetImageInfo();
+    const auto& usage = imageInfo.mFormat;
     if (!usage) {
         if (pname == LOCAL_GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING)
             return JS::NumberValue(LOCAL_GL_LINEAR);
 
         return JS::NullValue();
     }
 
     auto format = usage->format;
@@ -543,19 +398,17 @@ WebGLFBAttachPoint::GetParameter(WebGLCo
     case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING:
         ret = (format->isSRGB ? LOCAL_GL_SRGB
                               : LOCAL_GL_LINEAR);
         break;
 
     case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE:
         MOZ_ASSERT(attachment != LOCAL_GL_DEPTH_STENCIL_ATTACHMENT);
 
-        if (format->componentType == webgl::ComponentType::Special) {
-            // Special format is used for DS mixed format(e.g. D24S8 and D32FS8).
-            MOZ_ASSERT(format->unsizedFormat == webgl::UnsizedFormat::DEPTH_STENCIL);
+        if (format->unsizedFormat == webgl::UnsizedFormat::DEPTH_STENCIL) {
             MOZ_ASSERT(attachment == LOCAL_GL_DEPTH_ATTACHMENT ||
                        attachment == LOCAL_GL_STENCIL_ATTACHMENT);
 
             if (attachment == LOCAL_GL_DEPTH_ATTACHMENT) {
                 switch (format->effectiveFormat) {
                 case webgl::EffectiveFormat::DEPTH24_STENCIL8:
                     format = webgl::GetFormat(webgl::EffectiveFormat::DEPTH_COMPONENT24);
                     break;
@@ -575,37 +428,31 @@ WebGLFBAttachPoint::GetParameter(WebGLCo
                 default:
                     MOZ_ASSERT(false, "no matched DS format");
                     break;
                 }
             }
         }
 
         switch (format->componentType) {
-        case webgl::ComponentType::None:
-            ret = LOCAL_GL_NONE;
-            break;
         case webgl::ComponentType::Int:
             ret = LOCAL_GL_INT;
             break;
         case webgl::ComponentType::UInt:
             ret = LOCAL_GL_UNSIGNED_INT;
             break;
         case webgl::ComponentType::NormInt:
             ret = LOCAL_GL_SIGNED_NORMALIZED;
             break;
         case webgl::ComponentType::NormUInt:
             ret = LOCAL_GL_UNSIGNED_NORMALIZED;
             break;
         case webgl::ComponentType::Float:
             ret = LOCAL_GL_FLOAT;
             break;
-        default:
-            MOZ_ASSERT(false, "No matched component type");
-            break;
         }
         break;
 
     default:
         MOZ_ASSERT(false, "Missing case.");
         break;
     }
 
@@ -614,40 +461,46 @@ WebGLFBAttachPoint::GetParameter(WebGLCo
 
 ////////////////////////////////////////////////////////////////////////////////
 ////////////////////////////////////////////////////////////////////////////////
 // WebGLFramebuffer
 
 WebGLFramebuffer::WebGLFramebuffer(WebGLContext* webgl, GLuint fbo)
     : WebGLRefCountedObject(webgl)
     , mGLName(fbo)
-    , mNumFBStatusInvals(0)
-#ifdef ANDROID
-    , mIsFB(false)
-#endif
-    , mDepthAttachment(this, LOCAL_GL_DEPTH_ATTACHMENT)
-    , mStencilAttachment(this, LOCAL_GL_STENCIL_ATTACHMENT)
-    , mDepthStencilAttachment(this, LOCAL_GL_DEPTH_STENCIL_ATTACHMENT)
+    , mDepthAttachment(webgl, LOCAL_GL_DEPTH_ATTACHMENT)
+    , mStencilAttachment(webgl, LOCAL_GL_STENCIL_ATTACHMENT)
+    , mDepthStencilAttachment(webgl, LOCAL_GL_DEPTH_STENCIL_ATTACHMENT)
 {
     mContext->mFramebuffers.insertBack(this);
 
+    mAttachments.push_back(&mDepthAttachment);
+    mAttachments.push_back(&mStencilAttachment);
+
+    if (!webgl->IsWebGL2()) {
+        // Only WebGL1 has a separate depth+stencil attachment point.
+        mAttachments.push_back(&mDepthStencilAttachment);
+    }
+
     size_t i = 0;
     for (auto& cur : mColorAttachments) {
-        new (&cur) WebGLFBAttachPoint(this, LOCAL_GL_COLOR_ATTACHMENT0 + i);
+        new (&cur) WebGLFBAttachPoint(webgl, LOCAL_GL_COLOR_ATTACHMENT0 + i);
         i++;
+
+        mAttachments.push_back(&cur);
     }
 
     mColorDrawBuffers.push_back(&mColorAttachments[0]);
     mColorReadBuffer = &mColorAttachments[0];
 }
 
 void
 WebGLFramebuffer::Delete()
 {
-    InvalidateFramebufferStatus();
+    InvalidateCaches();
 
     mDepthAttachment.Clear();
     mStencilAttachment.Clear();
     mDepthStencilAttachment.Clear();
 
     for (auto& cur : mColorAttachments) {
         cur.Clear();
     }
@@ -694,165 +547,145 @@ WebGLFramebuffer::GetAttachPoint(GLenum 
     case LOCAL_GL_STENCIL_ATTACHMENT:
         return Some(&mStencilAttachment);
 
     default:
         return GetColorAttachPoint(attachPoint);
     }
 }
 
-#define FOR_EACH_ATTACHMENT(X)            \
-    X(mDepthAttachment);                  \
-    X(mStencilAttachment);                \
-    X(mDepthStencilAttachment);           \
-                                          \
-    for (auto& cur : mColorAttachments) { \
-        X(cur);                           \
-    }
-
 void
 WebGLFramebuffer::DetachTexture(const WebGLTexture* tex)
 {
-    const auto fnDetach = [&](WebGLFBAttachPoint& attach) {
-        if (attach.Texture() == tex) {
-            attach.Clear();
+    for (const auto& attach : mAttachments) {
+        if (attach->Texture() == tex) {
+            attach->Clear();
         }
-    };
-
-    FOR_EACH_ATTACHMENT(fnDetach)
+    }
+    InvalidateCaches();
 }
 
 void
 WebGLFramebuffer::DetachRenderbuffer(const WebGLRenderbuffer* rb)
 {
-    const auto fnDetach = [&](WebGLFBAttachPoint& attach) {
-        if (attach.Renderbuffer() == rb) {
-            attach.Clear();
+    for (const auto& attach : mAttachments) {
+        if (attach->Renderbuffer() == rb) {
+            attach->Clear();
         }
-    };
-
-    FOR_EACH_ATTACHMENT(fnDetach)
+    }
+    InvalidateCaches();
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // Completeness
 
 bool
 WebGLFramebuffer::HasDuplicateAttachments() const
 {
-   std::set<WebGLFBAttachPoint::Ordered> uniqueAttachSet;
+    std::set<WebGLFBAttachPoint::Ordered> uniqueAttachSet;
 
-   for (const auto& attach : mColorAttachments) {
-      if (!attach.IsDefined())
-          continue;
+    for (const auto& attach : mColorAttachments) {
+        if (!attach.HasAttachment())
+            continue;
 
-      const WebGLFBAttachPoint::Ordered ordered(attach);
+        const WebGLFBAttachPoint::Ordered ordered(attach);