Bug 1456171 - make getPluginBlocklistState API asynchronous, r?kmag draft
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Mon, 23 Apr 2018 17:11:34 +0100
changeset 787123 a806b956df2a6b75b0e4bb6a58758be753c6b1f9
parent 786343 378a8a64401f765bfd0706ff678a4f5db7c05385
push id107654
push usergijskruitbosch@gmail.com
push dateTue, 24 Apr 2018 10:08:59 +0000
reviewerskmag
bugs1456171
milestone61.0a1
Bug 1456171 - make getPluginBlocklistState API asynchronous, r?kmag MozReview-Commit-ID: KcDWtkdkNKs
browser/base/content/test/plugins/blocklist_proxy.js
dom/plugins/base/nsPluginHost.cpp
dom/plugins/base/nsPluginHost.h
toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
toolkit/mozapps/extensions/nsBlocklistService.js
toolkit/mozapps/extensions/test/xpcshell/test_blocklist_appversion.js
toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_flashonly.js
toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_outdated.js
toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_regexp.js
toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_severities.js
toolkit/mozapps/extensions/test/xpcshell/test_blocklist_severities.js
toolkit/mozapps/extensions/test/xpcshell/test_pluginBlocklistCtp.js
toolkit/mozapps/extensions/test/xpcshell/test_pluginInfoURL.js
xpcom/system/nsIBlocklistService.idl
--- a/browser/base/content/test/plugins/blocklist_proxy.js
+++ b/browser/base/content/test/plugins/blocklist_proxy.js
@@ -2,16 +2,18 @@ var Cm = Components.manager;
 
 const kBlocklistServiceUUID = "{66354bc9-7ed1-4692-ae1d-8da97d6b205e}";
 const kBlocklistServiceContractID = "@mozilla.org/extensions/blocklist;1";
 const kBlocklistServiceFactory = Cm.getClassObject(Cc[kBlocklistServiceContractID], Ci.nsIFactory);
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Timer.jsm");
 
+SimpleTest.requestFlakyTimeout("Need to simulate blocklist calls actually taking non-0 time to return");
+
 /*
  * A lightweight blocklist proxy for the testing purposes.
  */
 var BlocklistProxy = {
   _uuid: null,
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsIBlocklistService,
@@ -41,21 +43,22 @@ var BlocklistProxy = {
 
   notify(aTimer) {
   },
 
   observe(aSubject, aTopic, aData) {
   },
 
   async getAddonBlocklistState(aAddon, aAppVersion, aToolkitVersion) {
-    await new Promise(r => setTimeout(r, 0));
+    await new Promise(r => setTimeout(r, 150));
     return 0; // STATE_NOT_BLOCKED
   },
 
-  getPluginBlocklistState(aPluginTag, aAppVersion, aToolkitVersion) {
+  async getPluginBlocklistState(aPluginTag, aAppVersion, aToolkitVersion) {
+    await new Promise(r => setTimeout(r, 150));
     return 0; // STATE_NOT_BLOCKED
   },
 
   getPluginBlocklistURL(aPluginTag) {
     return "";
   },
 
   getPluginInfoURL(aPluginTag) {
--- a/dom/plugins/base/nsPluginHost.cpp
+++ b/dom/plugins/base/nsPluginHost.cpp
@@ -93,31 +93,34 @@
 #include "nsContentPolicyUtils.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/Telemetry.h"
 #include "nsIImageLoadingContent.h"
 #include "mozilla/Preferences.h"
 #include "nsVersionComparator.h"
 #include "NullPrincipal.h"
 
+#include "mozilla/dom/Promise.h"
+
 #if defined(XP_WIN)
 #include "nsIWindowMediator.h"
 #include "nsIBaseWindow.h"
 #include "windows.h"
 #include "winbase.h"
 #endif
 
 #include "npapi.h"
 
 using namespace mozilla;
 using mozilla::TimeStamp;
 using mozilla::plugins::FakePluginTag;
 using mozilla::plugins::PluginTag;
 using mozilla::dom::FakePluginTagInit;
 using mozilla::dom::FakePluginMimeEntry;
+using mozilla::dom::Promise;
 
 // Null out a strong ref to a linked list iteratively to avoid
 // exhausting the stack (bug 486349).
 #define NS_ITERATIVE_UNREF_LIST(type_, list_, mNext_)                \
   {                                                                  \
     while (list_) {                                                  \
       type_ temp = list_->mNext_;                                    \
       list_->mNext_ = nullptr;                                       \
@@ -242,16 +245,120 @@ bool ReadSectionHeader(nsPluginManifestL
   return false;
 }
 
 static bool UnloadPluginsASAP()
 {
   return (Preferences::GetUint(kPrefUnloadPluginTimeoutSecs, kDefaultPluginUnloadingTimeout) == 0);
 }
 
+namespace mozilla {
+namespace plugins {
+class BlocklistPromiseHandler final : public mozilla::dom::PromiseNativeHandler
+{
+  public:
+    NS_DECL_ISUPPORTS
+
+    BlocklistPromiseHandler(nsPluginTag *aTag, const bool aShouldSoftblock)
+      : mTag(aTag)
+      , mShouldDisableWhenSoftblocked(aShouldSoftblock)
+    {
+      MOZ_ASSERT(mTag, "Should always be passed a plugin tag");
+      sPendingBlocklistStateRequests++;
+    }
+
+    void
+    MaybeWriteBlocklistChanges()
+    {
+      // We're called immediately when the promise resolves/rejects, and (as a backup)
+      // when the handler is destroyed. To ensure we only run once, we use mTag as a
+      // sentinel, setting it to nullptr when we run.
+      if (!mTag) {
+        return;
+      }
+      mTag = nullptr;
+      sPendingBlocklistStateRequests--;
+      // If this was the only remaining pending request, check if we need to write
+      // state and if so update the child processes.
+      if (!sPendingBlocklistStateRequests &&
+          sPluginBlocklistStatesChangedSinceLastWrite) {
+        sPluginBlocklistStatesChangedSinceLastWrite = false;
+
+        RefPtr<nsPluginHost> host = nsPluginHost::GetInst();
+        // Write the changed list to disk:
+        host->WritePluginInfo();
+
+        // We update blocklist info in content processes asynchronously
+        // by just sending a new plugin list to content.
+        host->IncrementChromeEpoch();
+        host->SendPluginsToContent();
+      }
+    }
+
+    void
+    ResolvedCallback(JSContext *aCx, JS::Handle<JS::Value> aValue) override
+    {
+      if (!aValue.isInt32()) {
+        MOZ_ASSERT(false, "Blocklist should always return int32");
+        return;
+      }
+      int32_t newState = aValue.toInt32();
+      MOZ_ASSERT(newState >= 0 && newState < nsIBlocklistService::STATE_MAX,
+        "Shouldn't get an out of bounds blocklist state");
+
+      // Check the old and new state and see if there was a change:
+      uint32_t oldState = nsIBlocklistService::STATE_NOT_BLOCKED;
+      MOZ_ALWAYS_SUCCEEDS(mTag->GetBlocklistState(&oldState));
+      bool changed = oldState != (uint32_t)newState;
+      mTag->SetBlocklistState(newState);
+
+      if (newState == nsIBlocklistService::STATE_SOFTBLOCKED && mShouldDisableWhenSoftblocked) {
+        mTag->SetEnabledState(nsIPluginTag::STATE_DISABLED);
+        changed = true;
+      }
+      sPluginBlocklistStatesChangedSinceLastWrite |= changed;
+
+      MaybeWriteBlocklistChanges();
+    }
+    void
+    RejectedCallback(JSContext *aCx, JS::Handle<JS::Value> aValue) override
+    {
+      MOZ_ASSERT(false, "Shouldn't reject plugin blocklist state request");
+      MaybeWriteBlocklistChanges();
+    }
+
+  private:
+    ~BlocklistPromiseHandler() {
+      // If we have multiple plugins and the last pending request is GC'd
+      // and so never resolves/rejects, ensure we still write the blocklist.
+      MaybeWriteBlocklistChanges();
+    }
+
+    RefPtr<nsPluginTag> mTag;
+    bool mShouldDisableWhenSoftblocked;
+
+    // Whether we changed any of the plugins' blocklist states since
+    // we last started fetching them (async). This is reset to false
+    // every time we finish fetching plugin blocklist information.
+    // When this happens, if the previous value was true, we store the
+    // updated list on disk and send it to child processes.
+    static bool sPluginBlocklistStatesChangedSinceLastWrite;
+    // How many pending blocklist state requests we've got
+    static uint32_t sPendingBlocklistStateRequests;
+};
+
+NS_IMPL_ISUPPORTS0(BlocklistPromiseHandler)
+
+
+bool BlocklistPromiseHandler::sPluginBlocklistStatesChangedSinceLastWrite = false;
+uint32_t BlocklistPromiseHandler::sPendingBlocklistStateRequests = 0;
+} // namespace plugins
+} // namespace mozilla
+
+
 nsPluginHost::nsPluginHost()
   : mPluginsLoaded(false)
   , mOverrideInternalTypes(false)
   , mPluginsDisabled(false)
   , mPluginEpoch(0)
 {
   // check to see if pref is set at startup to let plugins take over in
   // full page mode for certain image mime types that we handle internally
@@ -263,17 +370,16 @@ nsPluginHost::nsPluginHost()
   Preferences::AddStrongObserver(this, "plugin.disable");
 
   nsCOMPtr<nsIObserverService> obsService =
     mozilla::services::GetObserverService();
   if (obsService) {
     obsService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
     if (XRE_IsParentProcess()) {
       obsService->AddObserver(this, "blocklist-updated", false);
-      obsService->AddObserver(this, "blocklist-loaded", false);
     }
   }
 
 #ifdef PLUGIN_LOGGING
   MOZ_LOG(nsPluginLogging::gNPNLog, PLUGIN_LOG_ALWAYS,("NPN Logging Active!\n"));
   MOZ_LOG(nsPluginLogging::gPluginLog, PLUGIN_LOG_ALWAYS,("General Plugin Logging Active! (nsPluginHost::ctor)\n"));
   MOZ_LOG(nsPluginLogging::gNPPLog, PLUGIN_LOG_ALWAYS,("NPP Logging Active!\n"));
 
@@ -1997,23 +2103,16 @@ nsresult nsPluginHost::ScanPluginsDirect
     }
   }
 
   pluginFiles.Sort(CompareFilesByTime());
 
   nsCOMArray<nsIFile> extensionDirs;
   GetExtensionDirectories(extensionDirs);
 
-  nsCOMPtr<nsIBlocklistService> blocklist =
-    do_GetService("@mozilla.org/extensions/blocklist;1");
-
-  bool isBlocklistLoaded = false;
-  if (blocklist && NS_FAILED(blocklist->GetIsLoaded(&isBlocklistLoaded))) {
-    isBlocklistLoaded = false;
-  }
   for (int32_t i = (pluginFiles.Length() - 1); i >= 0; i--) {
     nsCOMPtr<nsIFile>& localfile = pluginFiles[i];
 
     nsString utf16FilePath;
     rv = localfile->GetPath(utf16FilePath);
     if (NS_FAILED(rv))
       continue;
 
@@ -2098,30 +2197,20 @@ nsresult nsPluginHost::ScanPluginsDirect
         // Mark aPluginsChanged so pluginreg is rewritten
         *aPluginsChanged = true;
         continue;
       }
 
       uint32_t state = nsIBlocklistService::STATE_NOT_BLOCKED;
       pluginTag = new nsPluginTag(&info, fileModTime, fromExtension, state);
       pluginTag->mLibrary = library;
-      // If the blocklist is loaded, get the blocklist state now.
-      // If it isn't loaded yet, we'll update it once it loads.
-      if (isBlocklistLoaded &&
-          NS_SUCCEEDED(blocklist->GetPluginBlocklistState(pluginTag, EmptyString(),
-                                                          EmptyString(), &state))) {
-        pluginTag->SetBlocklistState(state);
-      }
       pluginFile.FreePluginInfo(info);
-
-      // If the blocklist says it is risky and we have never seen this
-      // plugin before, then disable it.
-      if (state == nsIBlocklistService::STATE_SOFTBLOCKED && !seenBefore) {
-        pluginTag->SetEnabledState(nsIPluginTag::STATE_DISABLED);
-      }
+      // Pass whether we've seen this plugin before. If the plugin is
+      // softblocked and new (not seen before), it will be disabled.
+      UpdatePluginBlocklistState(pluginTag, !seenBefore);
 
       // Plugin unloading is tag-based. If we created a new tag and loaded
       // the library in the process then we want to attempt to unload it here.
       // Only do this if the pref is set for aggressive unloading.
       if (UnloadPluginsASAP()) {
         pluginTag->TryUnloadPlugin(false);
       }
     }
@@ -2146,16 +2235,36 @@ nsresult nsPluginHost::ScanPluginsDirect
     }
 
     AddPluginTag(pluginTag);
   }
 
   return NS_OK;
 }
 
+void
+nsPluginHost::UpdatePluginBlocklistState(nsPluginTag* aPluginTag, bool aShouldSoftblock)
+{
+  nsCOMPtr<nsIBlocklistService> blocklist =
+    do_GetService("@mozilla.org/extensions/blocklist;1");
+  MOZ_ASSERT(blocklist, "Should be able to access the blocklist");
+  if (!blocklist) {
+    return;
+  }
+  // Asynchronously get the blocklist state.
+  nsCOMPtr<nsISupports> result;
+  blocklist->GetPluginBlocklistState(aPluginTag, EmptyString(),
+                                     EmptyString(), getter_AddRefs(result));
+  RefPtr<Promise> promise = do_QueryObject(result);
+  MOZ_ASSERT(promise, "Should always get a promise for plugin blocklist state.");
+  if (promise) {
+    promise->AppendNativeHandler(new mozilla::plugins::BlocklistPromiseHandler(aPluginTag, aShouldSoftblock));
+  }
+}
+
 nsresult nsPluginHost::ScanPluginsDirectoryList(nsISimpleEnumerator *dirEnum,
                                                 bool aCreatePluginList,
                                                 bool *aPluginsChanged)
 {
   MOZ_ASSERT(XRE_IsParentProcess());
 
     bool hasMore;
     while (NS_SUCCEEDED(dirEnum->HasMoreElements(&hasMore)) && hasMore) {
@@ -3402,46 +3511,25 @@ NS_IMETHODIMP nsPluginHost::Observe(nsIS
     mPluginsDisabled = Preferences::GetBool("plugin.disable", false);
     // Unload or load plugins as needed
     if (mPluginsDisabled) {
       UnloadPlugins();
     } else {
       LoadPlugins();
     }
   }
-  if (XRE_IsParentProcess() &&
-      (!strcmp("blocklist-updated", aTopic) || !strcmp("blocklist-loaded", aTopic))) {
-    nsCOMPtr<nsIBlocklistService> blocklist =
-      do_GetService("@mozilla.org/extensions/blocklist;1");
-    if (!blocklist) {
-      return NS_OK;
-    }
+  if (XRE_IsParentProcess() && !strcmp("blocklist-updated", aTopic)) {
+    // The blocklist has updated. Asynchronously get blocklist state for all items.
+    // The promise resolution handler takes care of checking if anything changed,
+    // and writing an updated state to file, as well as sending data to child processes.
     nsPluginTag* plugin = mPlugins;
-    bool blocklistAlteredPlugins = false;
     while (plugin) {
-      uint32_t blocklistState = nsIBlocklistService::STATE_NOT_BLOCKED;
-      nsresult rv = blocklist->GetPluginBlocklistState(plugin, EmptyString(),
-                                                       EmptyString(), &blocklistState);
-      NS_ENSURE_SUCCESS(rv, rv);
-      uint32_t oldBlocklistState;
-      plugin->GetBlocklistState(&oldBlocklistState);
-      plugin->SetBlocklistState(blocklistState);
-      blocklistAlteredPlugins |= (oldBlocklistState != blocklistState);
+      UpdatePluginBlocklistState(plugin);
       plugin = plugin->mNext;
     }
-    if (blocklistAlteredPlugins) {
-      // Write the changed list to disk:
-      WritePluginInfo();
-
-      // We update blocklists asynchronously by just sending a new plugin list to
-      // content.
-      // We'll need to repack our tags and send them to content again.
-      IncrementChromeEpoch();
-      SendPluginsToContent();
-    }
   }
   return NS_OK;
 }
 
 nsresult
 nsPluginHost::ParsePostBufferToFixHeaders(const char *inPostData, uint32_t inPostDataLen,
                                           char **outPostData, uint32_t *outPostDataLen)
 {
--- a/dom/plugins/base/nsPluginHost.h
+++ b/dom/plugins/base/nsPluginHost.h
@@ -23,26 +23,28 @@
 #include "nsTArray.h"
 #include "nsINamed.h"
 #include "nsTObserverArray.h"
 #include "nsITimer.h"
 #include "nsPluginTags.h"
 #include "nsIEffectiveTLDService.h"
 #include "nsIIDNService.h"
 #include "nsCRT.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
 
 #ifdef XP_WIN
 #include <minwindef.h>
 #include "nsIWindowsRegKey.h"
 #endif
 
 namespace mozilla {
 namespace plugins {
 class FakePluginTag;
 class PluginTag;
+class BlocklistPromiseHandler;
 } // namespace plugins
 } // namespace mozilla
 
 class nsNPAPIPlugin;
 class nsIFile;
 class nsIChannel;
 class nsPluginNativeWindow;
 class nsObjectLoadingContent;
@@ -251,16 +253,17 @@ public:
                              bool firstMatchOnly);
 
   nsresult SendPluginsToContent();
   nsresult SetPluginsInContent(uint32_t aPluginEpoch,
                                nsTArray<mozilla::plugins::PluginTag>& aPlugins,
                                nsTArray<mozilla::plugins::FakePluginTag>& aFakePlugins);
 private:
   friend class nsPluginUnloadRunnable;
+  friend class mozilla::plugins::BlocklistPromiseHandler;
 
   void DestroyRunningInstances(nsPluginTag* aPluginTag);
 
   // Writes updated plugins settings to disk and unloads the plugin
   // if it is now disabled. Should only be called by the plugin tag in question
   void UpdatePluginInfo(nsPluginTag* aPluginTag);
 
   nsresult TrySetUpPluginInstance(const nsACString &aMimeType, nsIURI *aURL,
@@ -312,16 +315,19 @@ private:
                         ePluginUnregister,
                         // Checks if this type should still be registered first
                         ePluginMaybeUnregister };
   void RegisterWithCategoryManager(const nsCString& aMimeType,
                                    nsRegisterType aType);
 
   void AddPluginTag(nsPluginTag* aPluginTag);
 
+  void UpdatePluginBlocklistState(nsPluginTag* aPluginTag,
+                                  bool aShouldSoftblock = false);
+
   nsresult
   ScanPluginsDirectory(nsIFile *pluginsDir,
                        bool aCreatePluginList,
                        bool *aPluginsChanged);
 
   nsresult
   ScanPluginsDirectoryList(nsISimpleEnumerator *dirEnum,
                            bool aCreatePluginList,
--- a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
@@ -133,32 +133,33 @@ class MockBlocklist {
   }
 
   unregister() {
     MockRegistrar.unregister(this.originalCID);
     this._reLazifyService();
   }
 
   async getAddonBlocklistState(addon, appVersion, toolkitVersion) {
-    await new Promise(r => setTimeout(r, 0));
+    await new Promise(r => setTimeout(r, 150));
     return this.addons.get(addon.id) || Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
   }
 
   async getAddonBlocklistEntry(addon, appVersion, toolkitVersion) {
     let state = await this.getAddonBlocklistState(addon, appVersion, toolkitVersion);
     if (state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
       return {
         state,
         url: "http://example.com/",
       };
     }
     return null;
   }
 
-  getPluginBlocklistState(plugin, version, appVersion, toolkitVersion) {
+  async getPluginBlocklistState(plugin, version, appVersion, toolkitVersion) {
+    await new Promise(r => setTimeout(r, 150));
     return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
   }
 }
 
 MockBlocklist.prototype.QueryInterface = XPCOMUtils.generateQI(["nsIBlocklistService"]);
 
 
 /**
--- a/toolkit/mozapps/extensions/nsBlocklistService.js
+++ b/toolkit/mozapps/extensions/nsBlocklistService.js
@@ -855,18 +855,19 @@ Blocklist.prototype = {
     }
     if (!this._preloadPromise) {
       this._preloadPromise = this._loadBlocklistAsyncInternal();
     }
     await this._preloadPromise;
   },
 
   async _loadBlocklistAsyncInternal() {
-    let profPath = OS.Path.join(OS.Constants.Path.profileDir, FILE_BLOCKLIST);
     try {
+      // Get the path inside the try...catch because there's no profileDir in e.g. xpcshell tests.
+      let profPath = OS.Path.join(OS.Constants.Path.profileDir, FILE_BLOCKLIST);
       await this._preloadBlocklistFile(profPath);
       return;
     } catch (e) {
       LOG("Blocklist::loadBlocklistAsync: Failed to load XML file " + e);
     }
 
     var appFile = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]);
     try {
@@ -1138,22 +1139,21 @@ Blocklist.prototype = {
       if (value) {
         blockEntry[matchElement.localName] = value;
       }
     }
     result.push(blockEntry);
   },
 
   /* See nsIBlocklistService */
-  getPluginBlocklistState(plugin, appVersion, toolkitVersion) {
+  async getPluginBlocklistState(plugin, appVersion, toolkitVersion) {
     if (AppConstants.platform == "android") {
       return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
     }
-    if (!this.isLoaded)
-      this._loadBlocklist();
+    await this.loadBlocklistAsync();
     return this._getPluginBlocklistState(plugin, this._pluginEntries,
                                          appVersion, toolkitVersion);
   },
 
   /**
    * Private helper to get the blocklist entry for a plugin given a set of
    * blocklist entries and versions.
    *
@@ -1305,17 +1305,16 @@ Blocklist.prototype = {
         return `${key}:${value}`;
       }).join("\t");
     }).join("\n");
     Services.obs.notifyObservers(null, "blocklist-data-gfxItems", payload);
   },
 
   _notifyObserversBlocklistUpdated() {
     Services.obs.notifyObservers(this, "blocklist-updated");
-    Services.ppmm.broadcastAsyncMessage("Blocklist:blocklistInvalidated", {});
   },
 
   async _blocklistUpdated(oldAddonEntries, oldPluginEntries) {
     var addonList = [];
 
     // A helper function that reverts the prefs passed to default values.
     function resetPrefs(prefs) {
       for (let pref of prefs)
@@ -1325,17 +1324,17 @@ Blocklist.prototype = {
     let addons = await AddonManager.getAddonsByTypes(types);
     for (let addon of addons) {
       let oldState = addon.blocklistState;
       if (addon.updateBlocklistState) {
         await addon.updateBlocklistState(false);
       } else if (oldAddonEntries) {
         oldState = this._getAddonBlocklistState(addon, oldAddonEntries);
       } else {
-        oldState = Ci.nsIBlocklistService.STATE_NOTBLOCKED;
+        oldState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
       }
       let state = addon.blocklistState;
 
       LOG("Blocklist state for " + addon.id + " changed from " +
           oldState + " to " + state);
 
       // We don't want to re-warn about add-ons
       if (state == oldState)
@@ -1392,17 +1391,17 @@ Blocklist.prototype = {
     var phs = Cc["@mozilla.org/plugin/host;1"].
               getService(Ci.nsIPluginHost);
     var plugins = phs.getPluginTags();
 
     for (let plugin of plugins) {
       let oldState = -1;
       if (oldPluginEntries)
         oldState = this._getPluginBlocklistState(plugin, oldPluginEntries);
-      let state = this.getPluginBlocklistState(plugin);
+      let state = this._getPluginBlocklistState(plugin, this._pluginEntries);
       LOG("Blocklist state for " + plugin.name + " changed from " +
           oldState + " to " + state);
       // We don't want to re-warn about items
       if (state == oldState)
         continue;
 
       if (oldState == Ci.nsIBlocklistService.STATE_BLOCKED) {
         if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
--- a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_appversion.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_appversion.js
@@ -187,21 +187,22 @@ var ADDONS = [{
 
 function MockPluginTag(name, version, start, appBlocks, toolkitBlocks) {
   this.name = name;
   this.version = version;
   this.start = start;
   this.appBlocks = appBlocks;
   this.toolkitBlocks = toolkitBlocks;
 }
-Object.defineProperty(MockPluginTag.prototype, "blocklisted", {
-  get: function MockPluginTag_getBlocklisted() {
-    return Services.blocklist.getPluginBlocklistState(this) == Services.blocklist.STATE_BLOCKED;
+MockPluginTag.prototype = {
+  async isBlocklisted() {
+    let state = await Services.blocklist.getPluginBlocklistState(this);
+    return state == Services.blocklist.STATE_BLOCKED;
   }
-});
+};
 
 var PLUGINS = [
   new MockPluginTag("test_bug449027_1", "5", false, false, false),
   new MockPluginTag("test_bug449027_2", "5", false, true, false),
   new MockPluginTag("test_bug449027_3", "5", false, true, false),
   new MockPluginTag("test_bug449027_4", "5", false, false, false),
   new MockPluginTag("test_bug449027_5", "5", false, false, false),
   new MockPluginTag("test_bug449027_6", "5", false, true, false),
@@ -283,32 +284,35 @@ function createAddon(addon) {
  * Checks that items are blocklisted correctly according to the current test.
  * If a lastTest is provided checks that the notification dialog got passed
  * the newly blocked items compared to the previous test.
  */
 async function checkState(test, lastTest, callback) {
   let addons = await AddonManager.getAddonsByIDs(ADDONS.map(a => a.id));
 
   for (var i = 0; i < ADDONS.length; i++) {
+    await TestUtils.waitForCondition(() => {
+      return (addons[i].blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) == ADDONS[i][test];
+    }).catch(() => { /* ignore exceptions; the following test will fail anyway. */ });
     var blocked = addons[i].blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED;
     equal(blocked, ADDONS[i][test],
-          `Blocklist state should match expected for extension ${i + 1}, test ${test}`);
+          `Blocklist state should match expected for extension ${addons[i].id}, test ${test}`);
   }
 
   for (i = 0; i < PLUGINS.length; i++) {
-    equal(PLUGINS[i].blocklisted, PLUGINS[i][test],
-          `Blocklist state should match expected for plugin ${i + 1}, test ${test}`);
+    equal(await PLUGINS[i].isBlocklisted(), PLUGINS[i][test],
+          `Blocklist state should match expected for plugin ${PLUGINS[i].name}, test ${test}`);
   }
 
   if (lastTest) {
     var expected = 0;
     for (i = 0; i < PLUGINS.length; i++) {
       if (PLUGINS[i][test] && !PLUGINS[i][lastTest]) {
         ok(gNewBlocks.includes(`${PLUGINS[i].name} ${PLUGINS[i].version}`),
-           `Plugin ${i + 1} should have been listed in the blocklist notification for test ${test}`);
+           `Plugin ${PLUGINS[i].name} should have been listed in the blocklist notification for test ${test}`);
         expected++;
       }
     }
 
     Assert.equal(expected, gNewBlocks.length);
   }
 }
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_flashonly.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_flashonly.js
@@ -10,26 +10,25 @@ function get_test_plugintag() {
   var tags = host.getPluginTags();
   for (let tag of tags) {
     if (tag.name == "Test Plug-in")
       return tag;
   }
   return null;
 }
 
-function run_test() {
+add_task(async function checkFlashOnlyPluginState() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
 
   copyBlocklistToProfile(do_get_file("data/test_bug514327_2.xml"));
 
   Services.prefs.setBoolPref("plugin.load_flash_only", false);
 
   var plugin = get_test_plugintag();
   if (!plugin)
     do_throw("Plugin tag not found");
 
   // run the code after the blocklist is closed
   Services.obs.notifyObservers(null, "addon-blocklist-closed");
-  executeSoon(function() {
-    // should be marked as outdated by the blocklist
-    Assert.ok(Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9") == nsIBLS.STATE_OUTDATED);
-  });
-}
+  await new Promise(executeSoon);
+  // should be marked as outdated by the blocklist
+  Assert.equal(await Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9"), nsIBLS.STATE_OUTDATED);
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_outdated.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_outdated.js
@@ -84,27 +84,27 @@ add_task(async function setup() {
   // initialize the blocklist with no entries
   copyBlocklistToProfile(do_get_file("data/test_bug514327_3_empty.xml"));
 
   await promiseStartupManager();
 
   gBlocklist = Services.blocklist;
 
   // should NOT be marked as outdated by the blocklist
-  Assert.ok(gBlocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9") == nsIBLS.STATE_NOT_BLOCKED);
+  Assert.equal(await gBlocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9"), nsIBLS.STATE_NOT_BLOCKED);
 });
 
 add_task(async function test_part_1() {
   // update blocklist with data that marks the plugin as outdated
   await loadBlocklist("test_bug514327_3_outdated_1.xml");
 
   // plugin should now be marked as outdated
-  Assert.ok(gBlocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9") == nsIBLS.STATE_OUTDATED);
+  Assert.equal(await gBlocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9"), nsIBLS.STATE_OUTDATED);
 
 });
 
 add_task(async function test_part_2() {
   // update blocklist with data that marks the plugin as outdated
   await loadBlocklist("test_bug514327_3_outdated_2.xml");
 
   // plugin should still be marked as outdated
-  Assert.ok(gBlocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9") == nsIBLS.STATE_OUTDATED);
+  Assert.equal(await gBlocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9"), nsIBLS.STATE_OUTDATED);
 });
--- a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_regexp.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_regexp.js
@@ -1,55 +1,54 @@
 /* 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/. */
 
 var PLUGINS = [{
-  // Normal blacklisted plugin, before an invalid regexp
+  // Normal blocklisted plugin, before an invalid regexp
   name: "test_bug468528_1",
   version: "5",
   disabled: false,
   blocklisted: false
 },
 {
-  // Normal blacklisted plugin, with an invalid regexp
+  // Normal blocklisted plugin, with an invalid regexp
   name: "test_bug468528_2",
   version: "5",
   disabled: false,
   blocklisted: false
 },
 {
-  // Normal blacklisted plugin, after an invalid regexp
+  // Normal blocklisted plugin, after an invalid regexp
   name: "test_bug468528_3",
   version: "5",
   disabled: false,
   blocklisted: false
 },
 {
   // Non-blocklisted plugin
   name: "test_bug468528_4",
   version: "5",
   disabled: false,
   blocklisted: false
 }];
 
 
-function run_test() {
+add_task(async function checkBlocklistForRegexes() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
 
   // We cannot force the blocklist to update so just copy our test list to the profile
   copyBlocklistToProfile(do_get_file("data/test_bug468528.xml"));
 
   var {blocklist} = Services;
 
   // blocked (sanity check)
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9") == blocklist.STATE_BLOCKED);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9"), blocklist.STATE_BLOCKED);
 
   // not blocked - won't match due to invalid regexp
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[1], "1", "1.9") == blocklist.STATE_NOT_BLOCKED);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[1], "1", "1.9"), blocklist.STATE_NOT_BLOCKED);
 
   // blocked - the invalid regexp for the previous item shouldn't affect this one
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[2], "1", "1.9") == blocklist.STATE_BLOCKED);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[2], "1", "1.9"), blocklist.STATE_BLOCKED);
 
   // not blocked - the previous invalid regexp shouldn't act as a wildcard
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[3], "1", "1.9") == blocklist.STATE_NOT_BLOCKED);
-
-}
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[3], "1", "1.9"), blocklist.STATE_NOT_BLOCKED);
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_severities.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_severities.js
@@ -28,27 +28,27 @@ var PLUGINS = [{
   name: "test_bug514327_4",
   version: "5",
   disabled: false,
   blocklisted: false,
   outdated: false
 }];
 
 
-function run_test() {
+add_task(async function checkBlocklistSeverities() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
 
   copyBlocklistToProfile(do_get_file("data/test_bug514327_1.xml"));
 
   var {blocklist} = Services;
 
   // blocked (sanity check)
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9") == blocklist.STATE_BLOCKED);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9"), blocklist.STATE_BLOCKED);
 
   // outdated
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[1], "1", "1.9") == blocklist.STATE_OUTDATED);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[1], "1", "1.9"), blocklist.STATE_OUTDATED);
 
   // outdated
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[2], "1", "1.9") == blocklist.STATE_OUTDATED);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[2], "1", "1.9"), blocklist.STATE_OUTDATED);
 
   // not blocked
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[3], "1", "1.9") == blocklist.STATE_NOT_BLOCKED);
-}
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[3], "1", "1.9"), blocklist.STATE_NOT_BLOCKED);
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_severities.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_severities.js
@@ -76,19 +76,16 @@ var ADDONS = [{
 }];
 
 class MockPlugin {
   constructor(name, version, enabledState) {
     this.name = name;
     this.version = version;
     this.enabledState = enabledState;
   }
-  get blocklisted() {
-    return Services.blocklist.getPluginBlocklistState(this) == Services.blocklist.STATE_BLOCKED;
-  }
   get disabled() {
     return this.enabledState == Ci.nsIPluginTag.STATE_DISABLED;
   }
 }
 
 var PLUGINS = [
   // Tests how the blocklist affects a disabled plugin
   new MockPlugin("test_bug455906_1", "5", Ci.nsIPluginTag.STATE_DISABLED),
@@ -157,18 +154,19 @@ async function loadBlocklist(file, callb
 
   Services.prefs.setCharPref("extensions.blocklist.url",
                              "http://example.com/data/" + file);
   Services.blocklist.QueryInterface(Ci.nsITimerCallback).notify(null);
 
   await blocklistUpdated;
 }
 
-function check_plugin_state(plugin) {
-  return plugin.disabled + "," + plugin.blocklisted;
+async function check_plugin_state(plugin) {
+  let blocklistState = await Services.blocklist.getPluginBlocklistState(plugin);
+  return `${plugin.disabled},${blocklistState == Services.blocklist.STATE_BLOCKED}`;
 }
 
 function create_blocklistURL(blockID) {
   let url = Services.urlFormatter.formatURLPref(PREF_BLOCKLIST_ITEM_URL);
   url = url.replace(/%blockID%/g, blockID);
   return url;
 }
 
@@ -179,22 +177,22 @@ async function checkInitialState() {
   checkAddonState(addons[0], {userDisabled: true, softDisabled: false, appDisabled: false});
   checkAddonState(addons[1], {userDisabled: false, softDisabled: false, appDisabled: false});
   checkAddonState(addons[2], {userDisabled: false, softDisabled: false, appDisabled: false});
   checkAddonState(addons[3], {userDisabled: true, softDisabled: true, appDisabled: false});
   checkAddonState(addons[4], {userDisabled: false, softDisabled: false, appDisabled: false});
   checkAddonState(addons[5], {userDisabled: false, softDisabled: false, appDisabled: true});
   checkAddonState(addons[6], {userDisabled: false, softDisabled: false, appDisabled: true});
 
-  equal(check_plugin_state(PLUGINS[0]), "true,false");
-  equal(check_plugin_state(PLUGINS[1]), "false,false");
-  equal(check_plugin_state(PLUGINS[2]), "false,false");
-  equal(check_plugin_state(PLUGINS[3]), "true,false");
-  equal(check_plugin_state(PLUGINS[4]), "false,false");
-  equal(check_plugin_state(PLUGINS[5]), "false,true");
+  equal(await check_plugin_state(PLUGINS[0]), "true,false");
+  equal(await check_plugin_state(PLUGINS[1]), "false,false");
+  equal(await check_plugin_state(PLUGINS[2]), "false,false");
+  equal(await check_plugin_state(PLUGINS[3]), "true,false");
+  equal(await check_plugin_state(PLUGINS[4]), "false,false");
+  equal(await check_plugin_state(PLUGINS[5]), "false,true");
 }
 
 function checkAddonState(addon, state) {
   return checkAddon(addon.id, addon, state);
 }
 
 add_task(async function setup() {
   // Copy the initial blocklist into the profile to check add-ons start in the
@@ -273,32 +271,32 @@ add_task(async function test_1() {
 
   await promiseRestartManager();
   dump("Checking results pt 2\n");
 
   addons = await AddonManager.getAddonsByIDs(ADDONS.map(a => a.id));
 
   // Should have disabled this add-on as requested
   checkAddonState(addons[2], {userDisabled: true, softDisabled: true, appDisabled: false});
-  equal(check_plugin_state(PLUGINS[2]), "true,false");
+  equal(await check_plugin_state(PLUGINS[2]), "true,false");
 
   // The blocked add-on should have changed to soft disabled
   checkAddonState(addons[5], {userDisabled: true, softDisabled: true, appDisabled: false});
   checkAddonState(addons[6], {userDisabled: true, softDisabled: true, appDisabled: true});
-  equal(check_plugin_state(PLUGINS[5]), "true,false");
+  equal(await check_plugin_state(PLUGINS[5]), "true,false");
 
   // These should have been unchanged
   checkAddonState(addons[0], {userDisabled: true, softDisabled: false, appDisabled: false});
   checkAddonState(addons[1], {userDisabled: false, softDisabled: false, appDisabled: false});
   checkAddonState(addons[3], {userDisabled: true, softDisabled: true, appDisabled: false});
   checkAddonState(addons[4], {userDisabled: false, softDisabled: false, appDisabled: false});
-  equal(check_plugin_state(PLUGINS[0]), "true,false");
-  equal(check_plugin_state(PLUGINS[1]), "false,false");
-  equal(check_plugin_state(PLUGINS[3]), "true,false");
-  equal(check_plugin_state(PLUGINS[4]), "false,false");
+  equal(await check_plugin_state(PLUGINS[0]), "true,false");
+  equal(await check_plugin_state(PLUGINS[1]), "false,false");
+  equal(await check_plugin_state(PLUGINS[3]), "true,false");
+  equal(await check_plugin_state(PLUGINS[4]), "false,false");
 
   // Back to starting state
   addons[2].userDisabled = false;
   addons[5].userDisabled = false;
   PLUGINS[2].enabledState = Ci.nsIPluginTag.STATE_ENABLED;
   PLUGINS[5].enabledState = Ci.nsIPluginTag.STATE_ENABLED;
 
   await promiseRestartManager();
@@ -351,21 +349,21 @@ add_task(async function test_pt3() {
 
   let addons = await AddonManager.getAddonsByIDs(ADDONS.map(a => a.id));
 
   // All should have gained the blocklist state, user disabled as previously
   checkAddonState(addons[0], {userDisabled: true, softDisabled: false, appDisabled: true});
   checkAddonState(addons[1], {userDisabled: false, softDisabled: false, appDisabled: true});
   checkAddonState(addons[2], {userDisabled: false, softDisabled: false, appDisabled: true});
   checkAddonState(addons[4], {userDisabled: false, softDisabled: false, appDisabled: true});
-  equal(check_plugin_state(PLUGINS[0]), "true,true");
-  equal(check_plugin_state(PLUGINS[1]), "false,true");
-  equal(check_plugin_state(PLUGINS[2]), "false,true");
-  equal(check_plugin_state(PLUGINS[3]), "true,true");
-  equal(check_plugin_state(PLUGINS[4]), "false,true");
+  equal(await check_plugin_state(PLUGINS[0]), "true,true");
+  equal(await check_plugin_state(PLUGINS[1]), "false,true");
+  equal(await check_plugin_state(PLUGINS[2]), "false,true");
+  equal(await check_plugin_state(PLUGINS[3]), "true,true");
+  equal(await check_plugin_state(PLUGINS[4]), "false,true");
 
   // Should have gained the blocklist state but no longer be soft disabled
   checkAddonState(addons[3], {userDisabled: false, softDisabled: false, appDisabled: true});
 
   // Check blockIDs are correct
   equal(await getAddonBlocklistURL(addons[0]), create_blocklistURL(addons[0].id));
   equal(await getAddonBlocklistURL(addons[1]), create_blocklistURL(addons[1].id));
   equal(await getAddonBlocklistURL(addons[2]), create_blocklistURL(addons[2].id));
@@ -377,17 +375,17 @@ add_task(async function test_pt3() {
   equal(Services.blocklist.getPluginBlocklistURL(PLUGINS[1]), create_blocklistURL("test_bug455906_plugin"));
   equal(Services.blocklist.getPluginBlocklistURL(PLUGINS[2]), create_blocklistURL("test_bug455906_plugin"));
   equal(Services.blocklist.getPluginBlocklistURL(PLUGINS[3]), create_blocklistURL("test_bug455906_plugin"));
   equal(Services.blocklist.getPluginBlocklistURL(PLUGINS[4]), create_blocklistURL("test_bug455906_plugin"));
 
   // Shouldn't be changed
   checkAddonState(addons[5], {userDisabled: false, softDisabled: false, appDisabled: true});
   checkAddonState(addons[6], {userDisabled: false, softDisabled: false, appDisabled: true});
-  equal(check_plugin_state(PLUGINS[5]), "false,true");
+  equal(await check_plugin_state(PLUGINS[5]), "false,true");
 
   // Back to starting state
   await loadBlocklist("bug455906_start.xml");
 });
 
 add_task(async function test_pt4() {
   let addon = await AddonManager.getAddonByID(ADDONS[4].id);
   addon.userDisabled = false;
@@ -406,25 +404,25 @@ add_task(async function test_pt4() {
   });
 
   await promiseRestartManager();
   dump("Checking results pt 4\n");
 
   let addons = await AddonManager.getAddonsByIDs(ADDONS.map(a => a.id));
   // This should have become unblocked
   checkAddonState(addons[5], {userDisabled: false, softDisabled: false, appDisabled: false});
-  equal(check_plugin_state(PLUGINS[5]), "false,false");
+  equal(await check_plugin_state(PLUGINS[5]), "false,false");
 
   // Should get re-enabled
   checkAddonState(addons[3], {userDisabled: false, softDisabled: false, appDisabled: false});
 
   // No change for anything else
   checkAddonState(addons[0], {userDisabled: true, softDisabled: false, appDisabled: false});
   checkAddonState(addons[1], {userDisabled: false, softDisabled: false, appDisabled: false});
   checkAddonState(addons[2], {userDisabled: false, softDisabled: false, appDisabled: false});
   checkAddonState(addons[4], {userDisabled: false, softDisabled: false, appDisabled: false});
   checkAddonState(addons[6], {userDisabled: false, softDisabled: false, appDisabled: true});
-  equal(check_plugin_state(PLUGINS[0]), "true,false");
-  equal(check_plugin_state(PLUGINS[1]), "false,false");
-  equal(check_plugin_state(PLUGINS[2]), "false,false");
-  equal(check_plugin_state(PLUGINS[3]), "true,false");
-  equal(check_plugin_state(PLUGINS[4]), "false,false");
+  equal(await check_plugin_state(PLUGINS[0]), "true,false");
+  equal(await check_plugin_state(PLUGINS[1]), "false,false");
+  equal(await check_plugin_state(PLUGINS[2]), "false,false");
+  equal(await check_plugin_state(PLUGINS[3]), "true,false");
+  equal(await check_plugin_state(PLUGINS[4]), "false,false");
 });
--- a/toolkit/mozapps/extensions/test/xpcshell/test_pluginBlocklistCtp.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_pluginBlocklistCtp.js
@@ -1,16 +1,15 @@
 /* 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/. */
 
 const nsIBLS = Ci.nsIBlocklistService;
 
 var gNotifier = null;
-var gNextTest = null;
 var gPluginHost = null;
 
 var gTestserver = AddonTestUtils.createHttpServer({hosts: ["example.com"]});
 gTestserver.registerDirectory("/data/", do_get_file("data"));
 
 var PLUGINS = [{
   // severity=0, vulnerabilitystatus=0 -> outdated
   name: "test_plugin_0",
@@ -49,128 +48,119 @@ var PLUGINS = [{
 {
   // not in the blocklist -> not blocked
   name: "test_plugin_5",
   version: "5",
   disabled: false,
   blocklisted: false
 }];
 
-function test_basic() {
+async function updateBlocklist(blocklistURL) {
+  if (blocklistURL) {
+    Services.prefs.setCharPref("extensions.blocklist.url", blocklistURL);
+  }
+  let blocklistUpdated = TestUtils.topicObserved("blocklist-updated");
+  gNotifier.notify(null);
+  return blocklistUpdated;
+}
+
+add_task(async function setup() {
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+
+  Services.prefs.setCharPref("extensions.blocklist.url", "http://example.com/data/test_pluginBlocklistCtp.xml");
+  Services.prefs.setBoolPref("plugin.load_flash_only", false);
+  await promiseStartupManager();
+
+  gPluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
+  gNotifier = Cc["@mozilla.org/extensions/blocklist;1"].getService(Ci.nsITimerCallback);
+
+  registerCleanupFunction(function() {
+    Services.prefs.clearUserPref("extensions.blocklist.url");
+    Services.prefs.clearUserPref("extensions.blocklist.enabled");
+    Services.prefs.clearUserPref("plugins.click_to_play");
+  });
+});
+
+add_task(async function basic() {
+  await updateBlocklist();
   var {blocklist} = Services;
 
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9") == nsIBLS.STATE_OUTDATED);
-
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[1], "1", "1.9") == nsIBLS.STATE_VULNERABLE_UPDATE_AVAILABLE);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9"),
+               nsIBLS.STATE_OUTDATED);
 
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[2], "1", "1.9") == nsIBLS.STATE_VULNERABLE_NO_UPDATE);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[1], "1", "1.9"),
+               nsIBLS.STATE_VULNERABLE_UPDATE_AVAILABLE);
 
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[3], "1", "1.9") == nsIBLS.STATE_BLOCKED);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[2], "1", "1.9"),
+               nsIBLS.STATE_VULNERABLE_NO_UPDATE);
 
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[4], "1", "1.9") == nsIBLS.STATE_SOFTBLOCKED);
-
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[5], "1", "1.9") == nsIBLS.STATE_NOT_BLOCKED);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[3], "1", "1.9"),
+               nsIBLS.STATE_BLOCKED);
 
-  gNextTest = test_is_not_clicktoplay;
-  executeSoon(gNextTest);
-}
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[4], "1", "1.9"),
+               nsIBLS.STATE_SOFTBLOCKED);
+
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[5], "1", "1.9"),
+               nsIBLS.STATE_NOT_BLOCKED);
+
+});
 
 function get_test_plugin() {
-  var pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
-  for (var plugin of pluginHost.getPluginTags()) {
+  for (var plugin of gPluginHost.getPluginTags()) {
     if (plugin.name == "Test Plug-in")
       return plugin;
   }
   Assert.ok(false);
   return null;
 }
 
 // At this time, the blocklist does not have an entry for the test plugin,
 // so it shouldn't be click-to-play.
-function test_is_not_clicktoplay() {
+add_task(async function test_is_not_clicktoplay() {
   var plugin = get_test_plugin();
-  var blocklistState = Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
+  var blocklistState = await Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
   Assert.notEqual(blocklistState, Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE);
   Assert.notEqual(blocklistState, Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE);
-
-  Services.prefs.setCharPref("extensions.blocklist.url", "http://example.com/data/test_pluginBlocklistCtpUndo.xml");
-  gNextTest = test_is_clicktoplay;
-  gNotifier.notify(null);
-}
+});
 
 // Here, we've updated the blocklist to have a block for the test plugin,
 // so it should be click-to-play.
-function test_is_clicktoplay() {
+add_task(async function test_is_clicktoplay() {
+  await updateBlocklist("http://example.com/data/test_pluginBlocklistCtpUndo.xml");
   var plugin = get_test_plugin();
-  var blocklistState = Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
+  var blocklistState = await Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
   Assert.equal(blocklistState, Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE);
-
-  Services.prefs.setCharPref("extensions.blocklist.url", "http://example.com/data/test_pluginBlocklistCtp.xml");
-  gNextTest = test_is_not_clicktoplay2;
-  gNotifier.notify(null);
-}
+});
 
 // But now we've removed that entry from the blocklist (really we've gone back
 // to the old one), so the plugin shouldn't be click-to-play any more.
-function test_is_not_clicktoplay2() {
+add_task(async function test_is_not_clicktoplay2() {
+  await updateBlocklist("http://example.com/data/test_pluginBlocklistCtp.xml");
   var plugin = get_test_plugin();
-  var blocklistState = Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
+  var blocklistState = await Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
   Assert.notEqual(blocklistState, Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE);
   Assert.notEqual(blocklistState, Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE);
 
-  Services.prefs.setCharPref("extensions.blocklist.url", "http://example.com/data/test_pluginBlocklistCtpUndo.xml");
-  gNextTest = test_disable_blocklist;
-  gNotifier.notify(null);
-}
+});
 
 // Test that disabling the blocklist when a plugin is ctp-blocklisted will
 // result in the plugin not being click-to-play.
-function test_disable_blocklist() {
+add_task(async function test_disable_blocklist() {
+  await updateBlocklist("http://example.com/data/test_pluginBlocklistCtpUndo.xml");
   var plugin = get_test_plugin();
-  var blocklistState = Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
+  var blocklistState = await Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
   Assert.equal(blocklistState, Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE);
 
-  gNextTest = null;
   Services.prefs.setBoolPref("extensions.blocklist.enabled", false);
-  blocklistState = Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
+  blocklistState = await Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
   Assert.notEqual(blocklistState, Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE);
   Assert.notEqual(blocklistState, Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE);
 
   // it should still be possible to make a plugin click-to-play via the pref
   // and setting that plugin's enabled state to click-to-play
   Services.prefs.setBoolPref("plugins.click_to_play", true);
   let previousEnabledState = plugin.enabledState;
   plugin.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
   Assert.equal(gPluginHost.getStateForType("application/x-test"), Ci.nsIPluginTag.STATE_CLICKTOPLAY);
   // clean up plugin state
   plugin.enabledState = previousEnabledState;
-
-  do_test_finished();
-}
-
-// Observe "blocklist-updated" so we know when to advance to the next test
-function observer() {
-  if (gNextTest)
-    executeSoon(gNextTest);
-}
-
-function run_test() {
-  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+});
 
-  Services.prefs.setCharPref("extensions.blocklist.url", "http://example.com/data/test_pluginBlocklistCtp.xml");
-  Services.prefs.setBoolPref("plugin.load_flash_only", false);
-  startupManager();
-
-  gPluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
-  gNotifier = Cc["@mozilla.org/extensions/blocklist;1"].getService(Ci.nsITimerCallback);
-  Services.obs.addObserver(observer, "blocklist-updated");
-
-  registerCleanupFunction(function() {
-    Services.prefs.clearUserPref("extensions.blocklist.url");
-    Services.prefs.clearUserPref("extensions.blocklist.enabled");
-    Services.prefs.clearUserPref("plugins.click_to_play");
-    Services.obs.removeObserver(observer, "blocklist-updated");
-  });
-
-  gNextTest = test_basic;
-  do_test_pending();
-  gNotifier.notify(null);
-}
--- a/toolkit/mozapps/extensions/test/xpcshell/test_pluginInfoURL.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_pluginInfoURL.js
@@ -10,21 +10,16 @@ ChromeUtils.import("resource://gre/modul
  */
 function MockPlugin(name, version, enabledState) {
   this.name = name;
   this.version = version;
   this.enabledState = enabledState;
 }
 
 MockPlugin.prototype = {
-  get blocklisted() {
-    let bls = Services.blocklist;
-    return bls.getPluginBlocklistState(this) == bls.STATE_BLOCKED;
-  },
-
   get disabled() {
     return this.enabledState == Ci.nsIPluginTag.STATE_DISABLED;
   }
 };
 
 // The mocked blocked plugin used to test the blocklist.
 const PLUGINS = [
   new MockPlugin("test_with_infoURL", "5", Ci.nsIPluginTag.STATE_ENABLED),
--- a/xpcom/system/nsIBlocklistService.idl
+++ b/xpcom/system/nsIBlocklistService.idl
@@ -22,28 +22,31 @@ interface nsIBlocklistService : nsISuppo
   // Indicates that the item is considered outdated, and there is a known
   // update available.
   const unsigned long STATE_OUTDATED    = 3;
   // Indicates that the item is vulnerable and there is an update.
   const unsigned long STATE_VULNERABLE_UPDATE_AVAILABLE = 4;
   // Indicates that the item is vulnerable and there is no update.
   const unsigned long STATE_VULNERABLE_NO_UPDATE = 5;
 
+  // Unused; Please increment if we add more blocklist states.
+  const unsigned long STATE_MAX = 6;
+
   /**
    * Determine the blocklist state of a plugin
    * @param   plugin
    *          The plugin to get the state for
    * @param   appVersion
    *          The version of the application we are checking in the blocklist.
    *          If this parameter is null, the version of the running application
    *          is used.
    * @param   toolkitVersion
    *          The version of the toolkit we are checking in the blocklist.
    *          If this parameter is null, the version of the running toolkit
    *          is used.
-   * @returns The STATE constant.
+   * @returns Promise that resolves to the STATE constant.
    */
-  unsigned long getPluginBlocklistState(in nsIPluginTag plugin,
-                                        [optional] in AString appVersion,
-                                        [optional] in AString toolkitVersion);
+  nsISupports getPluginBlocklistState(in nsIPluginTag plugin,
+                                      [optional] in AString appVersion,
+                                      [optional] in AString toolkitVersion);
 
   readonly attribute boolean isLoaded;
 };