Bug 1456171 - make getPluginBlocklistState API asynchronous, r=kmag
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Mon, 23 Apr 2018 17:11:34 +0100
changeset 471378 5d34206436c6d78140a9d3a9296b90d8f1c37b98
parent 471377 33f57e147c46b96b1be57a2b4fe10d281a256589
child 471379 f8cac1a891b5a7343b4ae359d4a987a58f025cda
push id1728
push userjlund@mozilla.com
push dateMon, 18 Jun 2018 21:12:27 +0000
treeherdermozilla-release@c296fde26f5f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1456171
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 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;
 };