Bug 1371888 - cache plugin information in pluginreg.dat to avoid sync startup load, r?Mossop,florian draft
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Tue, 20 Feb 2018 16:53:48 +0000
changeset 766936 a9b2525764a89949684e3c35c6ba5079f54af41e
parent 766212 d6957f004e9cc3d7408ac3a8f2b49ff97556e27f
child 766937 ab994ea0f7c6919286f0dd5445c98fe790c5ccbb
push id102455
push usergijskruitbosch@gmail.com
push dateTue, 13 Mar 2018 17:16:29 +0000
reviewersMossop, florian
bugs1371888, 1439519
milestone61.0a1
Bug 1371888 - cache plugin information in pluginreg.dat to avoid sync startup load, r?Mossop,florian This changes the pluginreg.dat format to include the blocklist state. There is now only the saved blocklist state in a plugin tag instance, rather than looking it up from in there using the blocklist service, so it was renamed from mCachedBlocklistState to mBlocklistState. We pass the 'right' state to the plugin instance when the plugintag is constructed. If we don't have state, we mark it as unblocked. mCachedBlocklistStateChanged was never read so it's being removed. Bug 1439519 adds a 'blocklist-loaded' notification that is fired once the blocklist is loaded. The plugin host implementation will listen to this in the parent process and update the blocklist state of all the plugins, and broadcast changes to the child process, just like when we update the blocklist from the server. We now also avoid re-sending plugin content to the content processes if the plugin state hasn't changed as a result of the blocklist having been loaded. Finally, because new plugins should still get an up-to-date blocklist state, and telemetry should get up-to-date data about which plugins are and aren't enabled once we have that data, we ensure that once we've loaded the blocklist async, we schedule an idle task to parse it and consider it loaded. All this means that plugin blocklist information could be mistaken between the points where a new plugin is installed and we first run Firefox with the new plugin, and the point where we load the blocklist. Given the trade-offs, that size of window (tiny) seems OK, also given that there's already a much larger window in blocklist updates (which only happen once every 24h). MozReview-Commit-ID: 1gsojRkUzTw
dom/base/nsObjectLoadingContent.cpp
dom/base/nsPluginArray.cpp
dom/plugins/base/nsPluginHost.cpp
dom/plugins/base/nsPluginTags.cpp
dom/plugins/base/nsPluginTags.h
toolkit/mozapps/extensions/internal/PluginProvider.jsm
toolkit/mozapps/extensions/nsBlocklistService.js
toolkit/mozapps/extensions/test/xpcshell/test_asyncBlocklistLoad.js
--- a/dom/base/nsObjectLoadingContent.cpp
+++ b/dom/base/nsObjectLoadingContent.cpp
@@ -802,32 +802,28 @@ nsObjectLoadingContent::InstantiatePlugi
 
   RefPtr<nsNPAPIPluginInstance> pluginInstance;
   GetPluginInstance(getter_AddRefs(pluginInstance));
   if (pluginInstance) {
     nsCOMPtr<nsIPluginTag> pluginTag;
     pluginHost->GetPluginTagForInstance(pluginInstance,
                                         getter_AddRefs(pluginTag));
 
-    nsCOMPtr<nsIBlocklistService> blocklist =
-      do_GetService("@mozilla.org/extensions/blocklist;1");
-    if (blocklist) {
-      uint32_t blockState = nsIBlocklistService::STATE_NOT_BLOCKED;
-      blocklist->GetPluginBlocklistState(pluginTag, EmptyString(),
-                                         EmptyString(), &blockState);
-      if (blockState == nsIBlocklistService::STATE_OUTDATED) {
-        // Fire plugin outdated event if necessary
-        LOG(("OBJLC [%p]: Dispatching plugin outdated event for content\n",
-             this));
-        nsCOMPtr<nsIRunnable> ev = new nsSimplePluginEvent(thisContent,
-                                                     NS_LITERAL_STRING("PluginOutdated"));
-        nsresult rv = NS_DispatchToCurrentThread(ev);
-        if (NS_FAILED(rv)) {
-          NS_WARNING("failed to dispatch nsSimplePluginEvent");
-        }
+
+    uint32_t blockState = nsIBlocklistService::STATE_NOT_BLOCKED;
+    pluginTag->GetBlocklistState(&blockState);
+    if (blockState == nsIBlocklistService::STATE_OUTDATED) {
+      // Fire plugin outdated event if necessary
+      LOG(("OBJLC [%p]: Dispatching plugin outdated event for content\n",
+           this));
+      nsCOMPtr<nsIRunnable> ev = new nsSimplePluginEvent(thisContent,
+                                                   NS_LITERAL_STRING("PluginOutdated"));
+      nsresult rv = NS_DispatchToCurrentThread(ev);
+      if (NS_FAILED(rv)) {
+        NS_WARNING("failed to dispatch nsSimplePluginEvent");
       }
     }
 
     // If we have a URI but didn't open a channel yet (eAllowPluginSkipChannel)
     // or we did load with a channel but are re-instantiating, re-open the
     // channel. OpenChannel() performs security checks, and this plugin has
     // already passed content policy in LoadObject.
     if ((mURI && !mChannelLoaded) || (mChannelLoaded && !aIsLoading)) {
--- a/dom/base/nsPluginArray.cpp
+++ b/dom/base/nsPluginArray.cpp
@@ -392,17 +392,18 @@ nsPluginArray::EnsurePlugins()
       } else {
         mCTPPlugins.AppendElement(new nsPluginElement(mWindow, pluginTags[i]));
       }
     }
   }
 
   if (mPlugins.Length() == 0 && mCTPPlugins.Length() != 0) {
     nsCOMPtr<nsPluginTag> hiddenTag = new nsPluginTag("Hidden Plugin", nullptr, "dummy.plugin", nullptr, nullptr,
-                                                      nullptr, nullptr, nullptr, 0, 0, false);
+                                                      nullptr, nullptr, nullptr, 0, 0, false,
+                                                      nsIBlocklistService::STATE_NOT_BLOCKED);
     mPlugins.AppendElement(new nsPluginElement(mWindow, hiddenTag));
   }
 
   // Alphabetize the enumeration order of non-hidden plugins to reduce
   // fingerprintable entropy based on plugins' installation file times.
   mPlugins.Sort();
 }
 // nsPluginElement implementation.
--- a/dom/plugins/base/nsPluginHost.cpp
+++ b/dom/plugins/base/nsPluginHost.cpp
@@ -129,17 +129,17 @@ static const char *kPrefWhitelist = "plu
 static const char *kPrefLoadInParentPrefix = "plugin.load_in_parent_process.";
 static const char *kPrefDisableFullPage = "plugin.disable_full_page_plugin_for_types";
 
 // How long we wait before unloading an idle plugin process.
 // Defaults to 30 seconds.
 static const char *kPrefUnloadPluginTimeoutSecs = "dom.ipc.plugins.unloadTimeoutSecs";
 static const uint32_t kDefaultPluginUnloadingTimeout = 30;
 
-static const char *kPluginRegistryVersion = "0.18";
+static const char *kPluginRegistryVersion = "0.19";
 
 static const char kDirectoryServiceContractID[] = "@mozilla.org/file/directory_service;1";
 
 #define kPluginRegistryFilename NS_LITERAL_CSTRING("pluginreg.dat")
 
 LazyLogModule nsPluginLogging::gNPNLog(NPN_LOG_NAME);
 LazyLogModule nsPluginLogging::gNPPLog(NPP_LOG_NAME);
 LazyLogModule nsPluginLogging::gPluginLog(PLUGIN_LOG_NAME);
@@ -261,17 +261,20 @@ nsPluginHost::nsPluginHost()
   mPluginsDisabled = Preferences::GetBool("plugin.disable", false);
 
   Preferences::AddStrongObserver(this, "plugin.disable");
 
   nsCOMPtr<nsIObserverService> obsService =
     mozilla::services::GetObserverService();
   if (obsService) {
     obsService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
-    obsService->AddObserver(this, "blocklist-updated", 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"));
 
   PLUGIN_LOG(PLUGIN_LOG_ALWAYS,("nsPluginHost::ctor\n"));
@@ -1994,16 +1997,23 @@ 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;
 
@@ -2085,22 +2095,27 @@ nsresult nsPluginHost::ScanPluginsDirect
         }
         mInvalidPlugins = invalidTag;
 
         // Mark aPluginsChanged so pluginreg is rewritten
         *aPluginsChanged = true;
         continue;
       }
 
-      pluginTag = new nsPluginTag(&info, fileModTime, fromExtension);
-      pluginFile.FreePluginInfo(info);
+      uint32_t state = nsIBlocklistService::STATE_NOT_BLOCKED;
+      pluginTag = new nsPluginTag(&info, fileModTime, fromExtension, state);
       pluginTag->mLibrary = library;
-      uint32_t state;
-      rv = pluginTag->GetBlocklistState(&state);
-      NS_ENSURE_SUCCESS(rv, rv);
+      // 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);
       }
 
       // Plugin unloading is tag-based. If we created a new tag and loaded
@@ -2739,26 +2754,28 @@ nsPluginHost::WritePluginInfo()
       PLUGIN_REGISTRY_END_OF_LINE_MARKER,
       (tag->mFullPath.get()),
       PLUGIN_REGISTRY_FIELD_DELIMITER,
       PLUGIN_REGISTRY_END_OF_LINE_MARKER,
       (tag->Version().get()),
       PLUGIN_REGISTRY_FIELD_DELIMITER,
       PLUGIN_REGISTRY_END_OF_LINE_MARKER);
 
-    // lastModifiedTimeStamp|canUnload|tag->mFlags|fromExtension
-    PR_fprintf(fd, "%lld%c%d%c%lu%c%d%c%c\n",
+    // lastModifiedTimeStamp|canUnload|tag->mFlags|fromExtension|blocklistState
+    PR_fprintf(fd, "%lld%c%d%c%lu%c%d%c%d%c%c\n",
       tag->mLastModifiedTime,
       PLUGIN_REGISTRY_FIELD_DELIMITER,
       false, // did store whether or not to unload in-process plugins
       PLUGIN_REGISTRY_FIELD_DELIMITER,
       0, // legacy field for flags
       PLUGIN_REGISTRY_FIELD_DELIMITER,
       tag->IsFromExtension(),
       PLUGIN_REGISTRY_FIELD_DELIMITER,
+      tag->BlocklistState(),
+      PLUGIN_REGISTRY_FIELD_DELIMITER,
       PLUGIN_REGISTRY_END_OF_LINE_MARKER);
 
     //description, name & mtypecount are on separate line
     PR_fprintf(fd, "%s%c%c\n%s%c%c\n%d\n",
       (tag->Description().get()),
       PLUGIN_REGISTRY_FIELD_DELIMITER,
       PLUGIN_REGISTRY_END_OF_LINE_MARKER,
       (tag->Name().get()),
@@ -2969,22 +2986,23 @@ nsPluginHost::ReadPluginInfo()
     if (!reader.NextLine())
       return rv;
 
     const char *version;
     version = reader.LinePtr();
     if (!reader.NextLine())
       return rv;
 
-    // lastModifiedTimeStamp|canUnload|tag.mFlag|fromExtension
-    if (4 != reader.ParseLine(values, 4))
+    // lastModifiedTimeStamp|canUnload|tag.mFlag|fromExtension|blocklistState
+    if (5 != reader.ParseLine(values, 5))
       return rv;
 
     int64_t lastmod = nsCRT::atoll(values[0]);
     bool fromExtension = atoi(values[3]);
+    uint16_t blocklistState = atoi(values[4]);
     if (!reader.NextLine())
       return rv;
 
     char *description = reader.LinePtr();
     if (!reader.NextLine())
       return rv;
 
     const char *name = reader.LinePtr();
@@ -3035,17 +3053,17 @@ nsPluginHost::ReadPluginInfo()
     RefPtr<nsPluginTag> tag = new nsPluginTag(name,
       description,
       filename,
       fullpath,
       version,
       (const char* const*)mimetypes,
       (const char* const*)mimedescriptions,
       (const char* const*)extensions,
-      mimetypecount, lastmod, fromExtension, true);
+      mimetypecount, lastmod, fromExtension, blocklistState, true);
 
     delete [] heapalloced;
 
     // Import flags from registry into prefs for old registry versions
     MOZ_LOG(nsPluginLogging::gPluginLog, PLUGIN_LOG_BASIC,
       ("LoadCachedPluginsInfo : Loading Cached plugininfo for %s\n", tag->FileName().get()));
 
     tag->mNext = mCachedPlugins;
@@ -3384,25 +3402,42 @@ NS_IMETHODIMP nsPluginHost::Observe(nsIS
     mPluginsDisabled = Preferences::GetBool("plugin.disable", false);
     // Unload or load plugins as needed
     if (mPluginsDisabled) {
       UnloadPlugins();
     } else {
       LoadPlugins();
     }
   }
-  if (!strcmp("blocklist-updated", aTopic)) {
+  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;
+    }
     nsPluginTag* plugin = mPlugins;
+    bool blocklistAlteredPlugins = false;
     while (plugin) {
-      plugin->InvalidateBlocklistState();
+      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);
       plugin = plugin->mNext;
     }
-    // We update blocklists asynchronously by just sending a new plugin list to
-    // content.
-    if (XRE_IsParentProcess()) {
+    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;
 }
 
--- a/dom/plugins/base/nsPluginTags.cpp
+++ b/dom/plugins/base/nsPluginTags.cpp
@@ -218,32 +218,32 @@ nsIInternalPluginTag::HasMimeType(const 
 }
 
 /* nsPluginTag */
 
 uint32_t nsPluginTag::sNextId;
 
 nsPluginTag::nsPluginTag(nsPluginInfo* aPluginInfo,
                          int64_t aLastModifiedTime,
-                         bool fromExtension)
+                         bool fromExtension,
+                         uint32_t aBlocklistState)
   : nsIInternalPluginTag(aPluginInfo->fName, aPluginInfo->fDescription,
                          aPluginInfo->fFileName, aPluginInfo->fVersion),
     mId(sNextId++),
     mContentProcessRunningCount(0),
     mHadLocalInstance(false),
     mLibrary(nullptr),
     mIsFlashPlugin(false),
     mSupportsAsyncRender(false),
     mFullPath(aPluginInfo->fFullPath),
     mLastModifiedTime(aLastModifiedTime),
     mSandboxLevel(0),
     mIsSandboxLoggingEnabled(false),
-    mCachedBlocklistState(nsIBlocklistService::STATE_NOT_BLOCKED),
-    mCachedBlocklistStateValid(false),
-    mIsFromExtension(fromExtension)
+    mIsFromExtension(fromExtension),
+    mBlocklistState(aBlocklistState)
 {
   InitMime(aPluginInfo->fMimeTypeArray,
            aPluginInfo->fMimeDescriptionArray,
            aPluginInfo->fExtensionArray,
            aPluginInfo->fVariantCount);
   InitSandboxLevel();
   EnsureMembersAreUTF8();
   FixupVersion();
@@ -255,31 +255,31 @@ nsPluginTag::nsPluginTag(const char* aNa
                          const char* aFullPath,
                          const char* aVersion,
                          const char* const* aMimeTypes,
                          const char* const* aMimeDescriptions,
                          const char* const* aExtensions,
                          int32_t aVariants,
                          int64_t aLastModifiedTime,
                          bool fromExtension,
+                         uint32_t aBlocklistState,
                          bool aArgsAreUTF8)
   : nsIInternalPluginTag(aName, aDescription, aFileName, aVersion),
     mId(sNextId++),
     mContentProcessRunningCount(0),
     mHadLocalInstance(false),
     mLibrary(nullptr),
     mIsFlashPlugin(false),
     mSupportsAsyncRender(false),
     mFullPath(aFullPath),
     mLastModifiedTime(aLastModifiedTime),
     mSandboxLevel(0),
     mIsSandboxLoggingEnabled(false),
-    mCachedBlocklistState(nsIBlocklistService::STATE_NOT_BLOCKED),
-    mCachedBlocklistStateValid(false),
-    mIsFromExtension(fromExtension)
+    mIsFromExtension(fromExtension),
+    mBlocklistState(aBlocklistState)
 {
   InitMime(aMimeTypes, aMimeDescriptions, aExtensions,
            static_cast<uint32_t>(aVariants));
   InitSandboxLevel();
   if (!aArgsAreUTF8)
     EnsureMembersAreUTF8();
   FixupVersion();
 }
@@ -293,31 +293,30 @@ nsPluginTag::nsPluginTag(uint32_t aId,
                          nsTArray<nsCString> aMimeTypes,
                          nsTArray<nsCString> aMimeDescriptions,
                          nsTArray<nsCString> aExtensions,
                          bool aIsFlashPlugin,
                          bool aSupportsAsyncRender,
                          int64_t aLastModifiedTime,
                          bool aFromExtension,
                          int32_t aSandboxLevel,
-                         uint16_t aBlocklistState)
+                         uint32_t aBlocklistState)
   : nsIInternalPluginTag(aName, aDescription, aFileName, aVersion, aMimeTypes,
                          aMimeDescriptions, aExtensions),
     mId(aId),
     mContentProcessRunningCount(0),
     mLibrary(nullptr),
     mIsFlashPlugin(aIsFlashPlugin),
     mSupportsAsyncRender(aSupportsAsyncRender),
     mLastModifiedTime(aLastModifiedTime),
     mSandboxLevel(aSandboxLevel),
     mIsSandboxLoggingEnabled(false),
     mNiceFileName(),
-    mCachedBlocklistState(aBlocklistState),
-    mCachedBlocklistStateValid(true),
-    mIsFromExtension(aFromExtension)
+    mIsFromExtension(aFromExtension),
+    mBlocklistState(aBlocklistState)
 {
 }
 
 nsPluginTag::~nsPluginTag()
 {
   NS_ASSERTION(!mNext, "Risk of exhausting the stack space, bug 486349");
 }
 
@@ -543,19 +542,17 @@ nsPluginTag::GetDisabled(bool* aDisabled
 {
   *aDisabled = !IsEnabled();
   return NS_OK;
 }
 
 bool
 nsPluginTag::IsBlocklisted()
 {
-  uint32_t blocklistState;
-  nsresult rv = GetBlocklistState(&blocklistState);
-  return NS_FAILED(rv) || blocklistState == nsIBlocklistService::STATE_BLOCKED;
+  return mBlocklistState == nsIBlocklistService::STATE_BLOCKED;
 }
 
 NS_IMETHODIMP
 nsPluginTag::GetBlocklisted(bool* aBlocklisted)
 {
   *aBlocklisted = IsBlocklisted();
   return NS_OK;
 }
@@ -717,58 +714,30 @@ nsPluginTag::GetNiceName(nsACString & aR
 {
   aResult = GetNiceFileName();
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsPluginTag::GetBlocklistState(uint32_t *aResult)
 {
-  // If we're in the content process, assume our cache state to always be valid,
-  // as the only way it can be updated is via a plugin list push from the
-  // parent process.
-  if (!XRE_IsParentProcess()) {
-    *aResult = mCachedBlocklistState;
-    return NS_OK;
-  }
-
-  nsCOMPtr<nsIBlocklistService> blocklist =
-    do_GetService("@mozilla.org/extensions/blocklist;1");
-
-  if (!blocklist) {
-    *aResult = nsIBlocklistService::STATE_NOT_BLOCKED;
-  }
-  // The EmptyString()s are so we use the currently running application
-  // and toolkit versions
-  else if (NS_FAILED(blocklist->GetPluginBlocklistState(this, EmptyString(),
-                                                   EmptyString(), aResult))) {
-    *aResult = nsIBlocklistService::STATE_NOT_BLOCKED;
-  }
-
-  MOZ_ASSERT(*aResult <= UINT16_MAX);
-  mCachedBlocklistState = (uint16_t) *aResult;
-  mCachedBlocklistStateValid = true;
+  *aResult = mBlocklistState;
   return NS_OK;
 }
 
 void
-nsPluginTag::SetBlocklistState(uint16_t aBlocklistState)
+nsPluginTag::SetBlocklistState(uint32_t aBlocklistState)
 {
-  // We should only ever call this on content processes. Any calls in the parent
-  // process should route through GetBlocklistState since we'll have the
-  // blocklist service there.
-  MOZ_ASSERT(!XRE_IsParentProcess());
-  mCachedBlocklistState = aBlocklistState;
-  mCachedBlocklistStateValid = true;
+  mBlocklistState = aBlocklistState;
 }
 
-void
-nsPluginTag::InvalidateBlocklistState()
+uint32_t
+nsPluginTag::BlocklistState()
 {
-  mCachedBlocklistStateValid = false;
+  return mBlocklistState;
 }
 
 NS_IMETHODIMP
 nsPluginTag::GetLastModifiedTime(PRTime* aLastModifiedTime)
 {
   MOZ_ASSERT(aLastModifiedTime);
   *aLastModifiedTime = mLastModifiedTime;
   return NS_OK;
--- a/dom/plugins/base/nsPluginTags.h
+++ b/dom/plugins/base/nsPluginTags.h
@@ -5,16 +5,17 @@
 
 #ifndef nsPluginTags_h_
 #define nsPluginTags_h_
 
 #include "mozilla/Attributes.h"
 #include "nscore.h"
 #include "nsCOMPtr.h"
 #include "nsCOMArray.h"
+#include "nsIBlocklistService.h"
 #include "nsIPluginTag.h"
 #include "nsITimer.h"
 #include "nsString.h"
 
 class nsIURI;
 struct PRLibrary;
 struct nsPluginInfo;
 class nsNPAPIPlugin;
@@ -103,58 +104,61 @@ public:
     ePluginState_Disabled = 0,
     ePluginState_Clicktoplay = 1,
     ePluginState_Enabled = 2,
     ePluginState_MaxValue = 3,
   };
 
   nsPluginTag(nsPluginInfo* aPluginInfo,
               int64_t aLastModifiedTime,
-              bool fromExtension);
+              bool fromExtension,
+              uint32_t aBlocklistState);
   nsPluginTag(const char* aName,
               const char* aDescription,
               const char* aFileName,
               const char* aFullPath,
               const char* aVersion,
               const char* const* aMimeTypes,
               const char* const* aMimeDescriptions,
               const char* const* aExtensions,
               int32_t aVariants,
               int64_t aLastModifiedTime,
               bool fromExtension,
+              uint32_t aBlocklistState,
               bool aArgsAreUTF8 = false);
   nsPluginTag(uint32_t aId,
               const char* aName,
               const char* aDescription,
               const char* aFileName,
               const char* aFullPath,
               const char* aVersion,
               nsTArray<nsCString> aMimeTypes,
               nsTArray<nsCString> aMimeDescriptions,
               nsTArray<nsCString> aExtensions,
               bool aIsFlashPlugin,
               bool aSupportsAsyncRender,
               int64_t aLastModifiedTime,
               bool aFromExtension,
               int32_t aSandboxLevel,
-              uint16_t aBlocklistState);
+              uint32_t aBlocklistState);
 
   void TryUnloadPlugin(bool inShutdown);
 
   // plugin is enabled and not blocklisted
   bool IsActive();
 
   bool IsEnabled() override;
   void SetEnabled(bool enabled);
   bool IsClicktoplay();
   bool IsBlocklisted();
+  uint32_t BlocklistState();
 
   PluginState GetPluginState();
   void SetPluginState(PluginState state);
-  void SetBlocklistState(uint16_t aBlocklistState);
+  void SetBlocklistState(uint32_t aBlocklistState);
 
   bool HasSameNameAndMimes(const nsPluginTag *aPluginTag) const;
   const nsCString& GetNiceFileName() override;
 
   bool IsFromExtension() const;
 
   RefPtr<nsPluginTag> mNext;
   uint32_t      mId;
@@ -170,25 +174,22 @@ public:
   bool          mIsFlashPlugin;
   bool          mSupportsAsyncRender;
   nsCString     mFullPath; // UTF-8
   int64_t       mLastModifiedTime;
   nsCOMPtr<nsITimer> mUnloadTimer;
   int32_t       mSandboxLevel;
   bool          mIsSandboxLoggingEnabled;
 
-  void          InvalidateBlocklistState();
-
 private:
   virtual ~nsPluginTag();
 
   nsCString     mNiceFileName; // UTF-8
-  uint16_t      mCachedBlocklistState;
-  bool          mCachedBlocklistStateValid;
   bool          mIsFromExtension;
+  uint32_t      mBlocklistState;
 
   void InitMime(const char* const* aMimeTypes,
                 const char* const* aMimeDescriptions,
                 const char* const* aExtensions,
                 uint32_t aVariantCount);
   void InitSandboxLevel();
   nsresult EnsureMembersAreUTF8();
   void FixupVersion();
--- a/toolkit/mozapps/extensions/internal/PluginProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/PluginProvider.jsm
@@ -372,17 +372,17 @@ PluginWrapper.prototype = {
       AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["userDisabled"]);
     }
 
     return val;
   },
 
   get blocklistState() {
     let { tags: [tag] } = pluginFor(this);
-    return Services.blocklist.getPluginBlocklistState(tag);
+    return tag.blocklistState;
   },
 
   get blocklistURL() {
     let { tags: [tag] } = pluginFor(this);
     return Services.blocklist.getPluginBlocklistURL(tag);
   },
 
   get size() {
--- a/toolkit/mozapps/extensions/nsBlocklistService.js
+++ b/toolkit/mozapps/extensions/nsBlocklistService.js
@@ -623,19 +623,16 @@ Blocklist.prototype = {
     }
 
     // Save current blocklist timestamp to pref.
     const lastModified = request.getResponseHeader("Last-Modified") || "";
     Services.prefs.setCharPref(PREF_BLOCKLIST_LAST_MODIFIED, lastModified);
 
     var oldAddonEntries = this._addonEntries;
     var oldPluginEntries = this._pluginEntries;
-    this._addonEntries = [];
-    this._gfxEntries = [];
-    this._pluginEntries = [];
 
     this._loadBlocklistFromXML(responseXML);
     // We don't inform the users when the graphics blocklist changed at runtime.
     // However addons and plugins blocking status is refreshed.
     this._blocklistUpdated(oldAddonEntries, oldPluginEntries);
 
     try {
       let path = OS.Path.join(OS.Constants.Path.profileDir, FILE_BLOCKLIST);
@@ -670,23 +667,16 @@ Blocklist.prototype = {
    * Finds the newest blocklist file from the application and the profile and
    * load it or does nothing if neither exist.
    */
   _loadBlocklist() {
     this._addonEntries = [];
     this._gfxEntries = [];
     this._pluginEntries = [];
 
-    if (this._isBlocklistPreloaded()) {
-      Services.telemetry.getHistogramById("BLOCKLIST_SYNC_FILE_LOAD").add(false);
-      this._loadBlocklistFromString(this._preloadedBlocklistContent);
-      delete this._preloadedBlocklistContent;
-      return;
-    }
-
     Services.telemetry.getHistogramById("BLOCKLIST_SYNC_FILE_LOAD").add(true);
 
     var profFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_BLOCKLIST]);
     try {
       this._loadBlocklistFromFile(profFile);
     } catch (ex) {
       LOG("Blocklist::_loadBlocklist: couldn't load file from profile, trying app dir");
       try {
@@ -801,26 +791,21 @@ Blocklist.prototype = {
     if (text)
       this._loadBlocklistFromString(text);
   },
 
   get isLoaded() {
     return this._addonEntries != null && this._gfxEntries != null && this._pluginEntries != null;
   },
 
-  _isBlocklistPreloaded() {
-    return this._preloadedBlocklistContent != null;
-  },
-
   /* Used for testing */
   _clear() {
     this._addonEntries = null;
     this._gfxEntries = null;
     this._pluginEntries = null;
-    this._preloadedBlocklistContent = null;
   },
 
   async _preloadBlocklist() {
     let profPath = OS.Path.join(OS.Constants.Path.profileDir, FILE_BLOCKLIST);
     try {
       await this._preloadBlocklistFile(profPath);
       return;
     } catch (e) {
@@ -846,20 +831,25 @@ Blocklist.prototype = {
 
     if (!gBlocklistEnabled) {
       LOG("Blocklist::_preloadBlocklistFile: blocklist is disabled");
       return;
     }
 
     let text = await OS.File.read(path, { encoding: "utf-8" });
 
-    if (!this._addonEntries) {
-      // Store the content only if a sync load has not been performed in the meantime.
-      this._preloadedBlocklistContent = text;
-    }
+    await new Promise(resolve => {
+      Services.tm.idleDispatchToMainThread(() => {
+        if (!this.isLoaded) {
+          Services.telemetry.getHistogramById("BLOCKLIST_SYNC_FILE_LOAD").add(false);
+          this._loadBlocklistFromString(text);
+        }
+        resolve();
+      });
+    });
   },
 
   _loadBlocklistFromString(text) {
     try {
       var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
                    createInstance(Ci.nsIDOMParser);
       var doc = parser.parseFromString(text, "text/xml");
       if (doc.documentElement.namespaceURI != XMLURI_BLOCKLIST) {
@@ -871,16 +861,19 @@ Blocklist.prototype = {
     } catch (e) {
       LOG("Blocklist::_loadBlocklistFromString: Error constructing blocklist " + e);
       return;
     }
     this._loadBlocklistFromXML(doc);
   },
 
   _loadBlocklistFromXML(doc) {
+    this._addonEntries = [];
+    this._gfxEntries = [];
+    this._pluginEntries = [];
     try {
       var childNodes = doc.documentElement.childNodes;
       for (let element of childNodes) {
         if (!(element instanceof Ci.nsIDOMElement))
           continue;
         switch (element.localName) {
         case "emItems":
           this._addonEntries = this._processItemNodes(element.childNodes, "emItem",
--- a/toolkit/mozapps/extensions/test/xpcshell/test_asyncBlocklistLoad.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_asyncBlocklistLoad.js
@@ -2,39 +2,46 @@
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 add_task(async function() {
   let blocklist = AM_Cc["@mozilla.org/extensions/blocklist;1"].
                   getService().wrappedJSObject;
   let scope = ChromeUtils.import("resource://gre/modules/osfile.jsm", {});
 
-  // sync -> async
+  // sync -> async. Check that async code doesn't try to read the file
+  // once it's already been read synchronously.
+  let read = scope.OS.File.read;
+  let triedToRead = false;
+  scope.OS.File.read = () => triedToRead = true;
   blocklist._loadBlocklist();
   Assert.ok(blocklist.isLoaded);
   await blocklist._preloadBlocklist();
-  Assert.ok(!blocklist._isBlocklistPreloaded());
+  Assert.ok(!triedToRead);
+  scope.OS.File.read = read;
   blocklist._clear();
 
-  // async -> sync
+  info("sync -> async complete");
+
+  // async first. Check that once we preload the content, that is sufficient.
   await blocklist._preloadBlocklist();
-  Assert.ok(!blocklist.isLoaded);
-  Assert.ok(blocklist._isBlocklistPreloaded());
-  blocklist._loadBlocklist();
   Assert.ok(blocklist.isLoaded);
-  Assert.ok(!blocklist._isBlocklistPreloaded());
+  // Calling _loadBlocklist now would just re-load the list sync.
+
+  info("async test complete");
   blocklist._clear();
 
   // async -> sync -> async
-  let read = scope.OS.File.read;
   scope.OS.File.read = function(...args) {
     return new Promise((resolve, reject) => {
       executeSoon(() => {
         blocklist._loadBlocklist();
+        // Now do the async bit after all:
         resolve(read(...args));
       });
     });
   };
 
   await blocklist._preloadBlocklist();
+  // We're mostly just checking this doesn't error out.
   Assert.ok(blocklist.isLoaded);
-  Assert.ok(!blocklist._isBlocklistPreloaded());
+  info("mixed async/sync test complete");
 });