Bug 1238735: Add LSP blocking capabilities to Windows DLL blocklist; r?mhowell, bsmedberg draft
authorAaron Klotz <aklotz@mozilla.com>
Wed, 16 Nov 2016 15:58:22 -0700
changeset 440057 277f375c4582ef7ac4b1582dc333722ca3c18f59
parent 439734 51750761f2c61c64cf0553f6cb5fefd4999d3bc0
child 537306 9ddbcd8afb38f73711c68c23f18a5503953787a6
push id36153
push useraklotz@mozilla.com
push dateThu, 17 Nov 2016 00:40:31 +0000
reviewersmhowell, bsmedberg
bugs1238735
milestone53.0a1
Bug 1238735: Add LSP blocking capabilities to Windows DLL blocklist; r?mhowell, bsmedberg MozReview-Commit-ID: 7izyjX2T7RD
mozglue/build/WindowsDllBlocklist.cpp
mozglue/build/WindowsLSPPassthrough.cpp
mozglue/build/WindowsLSPPassthrough.h
mozglue/build/moz.build
mozglue/build/mozglue.def.in
mozglue/tests/moz.build
mozglue/tests/testlsp/moz.build
mozglue/tests/testlsp/testlsp.cpp
mozglue/tests/testlsp/testlsp.def
mozglue/tests/testlsp2/moz.build
mozglue/tests/testlsp2/testlsp2.def
mozglue/tests/testlspblocklist/TestLspBlocklist.cpp
mozglue/tests/testlspblocklist/moz.build
--- a/mozglue/build/WindowsDllBlocklist.cpp
+++ b/mozglue/build/WindowsDllBlocklist.cpp
@@ -9,32 +9,36 @@
 #define MALLOC_FUNCS MALLOC_FUNCS_MALLOC
 // See mozmemory_wrap.h for more details. This file is part of libmozglue, so
 // it needs to use _impl suffixes.
 #define MALLOC_DECL(name, return_type, ...) \
   extern "C" MOZ_MEMORY_API return_type name ## _impl(__VA_ARGS__);
 #include "malloc_decls.h"
 #endif
 
+#include <ntstatus.h>
+#define WIN32_NO_STATUS // Because we've explicitly including ntstatus.h
+
 #include <windows.h>
 #include <winternl.h>
 #include <io.h>
 
 #pragma warning( push )
 #pragma warning( disable : 4275 4530 ) // See msvc-stl-wrapper.template.h
 #include <map>
 #pragma warning( pop )
 
 #include "nsAutoPtr.h"
 
 #include "nsWindowsDllInterceptor.h"
 #include "mozilla/UniquePtr.h"
 #include "mozilla/WindowsVersion.h"
 #include "nsWindowsHelpers.h"
 #include "WindowsDllBlocklist.h"
+#include "WindowsLSPPassthrough.h"
 
 using namespace mozilla;
 
 #define ALL_VERSIONS   ((unsigned long long)-1LL)
 
 // DLLs sometimes ship without a version number, particularly early
 // releases. Blocking "version <= 0" has the effect of blocking unversioned
 // DLLs (since the call to get version info fails), but not blocking
@@ -64,16 +68,22 @@ struct DllBlockInfo {
   // the IMAGE_FILE_HEADER in lieu of a version number.
   unsigned long long maxVersion;
 
   enum {
     FLAGS_DEFAULT = 0,
     BLOCK_WIN8PLUS_ONLY = 1,
     BLOCK_XP_ONLY = 2,
     USE_TIMESTAMP = 4,
+    /**
+     * IMPORTANT: If SUBSTITUTE_LSP_PASSTHROUGH is specified, the LSP's
+     * Protocol GUID must also be added to the gLayerGuids array in
+     * WindowsLSPPassthough.cpp!
+     */
+    SUBSTITUTE_LSP_PASSTHROUGH = 8,
   } flags;
 };
 
 static DllBlockInfo sWindowsDllBlocklist[] = {
   // EXAMPLE:
   // { "uxtheme.dll", ALL_VERSIONS },
   // { "uxtheme.dll", 0x0000123400000000ULL },
   // The DLL name must be in lowercase!
@@ -203,16 +213,22 @@ static DllBlockInfo sWindowsDllBlocklist
 
   // SS2OSD, bug 1262348
   { "ss2osd.dll", ALL_VERSIONS },
   { "ss2devprops.dll", ALL_VERSIONS },
 
   // NHASUSSTRIXOSD.DLL, bug 1269244
   { "nhasusstrixosd.dll", ALL_VERSIONS },
   { "nhasusstrixdevprops.dll", ALL_VERSIONS },
+  
+  // LSPs for unit testing
+  // IMPORTANT: Do not specify SUBSTITUTE_LSP_PASSTHROUGH without adding
+  // the LSP's Protocol GUID to gLayerGuids in WindowsLSPPassthrough.cpp!
+  { "testlsp.dll", ALL_VERSIONS, DllBlockInfo::SUBSTITUTE_LSP_PASSTHROUGH },
+  { "testlsp2.dll", ALL_VERSIONS, DllBlockInfo::SUBSTITUTE_LSP_PASSTHROUGH },
 
   // Crashes with PremierOpinion/RelevantKnowledge, bug 1277846
   { "opls.dll", ALL_VERSIONS },
   { "opls64.dll", ALL_VERSIONS },
   { "pmls.dll", ALL_VERSIONS },
   { "pmls64.dll", ALL_VERSIONS },
   { "prls.dll", ALL_VERSIONS },
   { "prls64.dll", ALL_VERSIONS },
@@ -663,22 +679,22 @@ patched_LdrLoadDll (PWCHAR filePath, PUL
 
   if (info->name) {
     bool load_ok = false;
 
 #ifdef DEBUG_very_verbose
     printf_stderr("LdrLoadDll: info->name: '%s'\n", info->name);
 #endif
 
-    if ((info->flags == DllBlockInfo::BLOCK_WIN8PLUS_ONLY) &&
+    if ((info->flags & DllBlockInfo::BLOCK_WIN8PLUS_ONLY) &&
         !IsWin8OrLater()) {
       goto continue_loading;
     }
 
-    if ((info->flags == DllBlockInfo::BLOCK_XP_ONLY) &&
+    if ((info->flags & DllBlockInfo::BLOCK_XP_ONLY) &&
         IsWin2003OrLater()) {
       goto continue_loading;
     }
 
     unsigned long long fVersion = ALL_VERSIONS;
 
     if (info->maxVersion != ALL_VERSIONS) {
       ReentrancySentinel sentinel(dllName);
@@ -721,16 +737,50 @@ patched_LdrLoadDll (PWCHAR filePath, PUL
             if (fVersion > info->maxVersion)
               load_ok = true;
           }
         }
       }
     }
 
     if (!load_ok) {
+      if (info->flags & DllBlockInfo::SUBSTITUTE_LSP_PASSTHROUGH) {
+        if (!full_fname) {
+          full_fname = getFullPath(filePath, fname);
+          if (!full_fname) {
+            // uh, we couldn't find the DLL at all, so...
+            printf_stderr("LdrLoadDll: SearchPathW didn't find DLL '%s', unable to substitute LSP\n", dllName);
+            return STATUS_DLL_NOT_FOUND;
+          }
+        }
+
+        if (!mozilla::lsp::IsRegisteredBlockedLSP(full_fname.get())) {
+          // We want to block this LSP, however this DLL does not match any
+          // registered LSP layer providers whose GUIDs are blocked!
+          // Failing to allow the DLL load under these conditions will likely
+          // break Winsock, so we must allow the LSP to load.
+          goto continue_loading;
+        }
+
+        printf_stderr("LdrLoadDll: Blocking LSP load of '%s' -- see http://www.mozilla.com/en-US/blocklist/\n", dllName);
+        DllBlockSet::Add(info->name, fVersion);
+
+        // We substitute our own module (ie, the one containing this function)
+        // for the LSP's module. We also pin this module's reference count so
+        // that nobody can accidentally FreeLibrary() us.
+        HMODULE ourModule = NULL;
+        if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_PIN |
+                               GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
+                               reinterpret_cast<LPCWSTR>(&patched_LdrLoadDll),
+                               &ourModule)) {
+          *handle = ourModule;
+          return STATUS_SUCCESS;
+        }
+      }
+
       printf_stderr("LdrLoadDll: Blocking load of '%s' -- see http://www.mozilla.com/en-US/blocklist/\n", dllName);
       DllBlockSet::Add(info->name, fVersion);
       return STATUS_DLL_NOT_FOUND;
     }
   }
 
 continue_loading:
 #ifdef DEBUG_very_verbose
new file mode 100644
--- /dev/null
+++ b/mozglue/build/WindowsLSPPassthrough.cpp
@@ -0,0 +1,602 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifdef MOZ_MEMORY
+#define MOZ_MEMORY_IMPL
+#include "mozmemory_wrap.h"
+#define MALLOC_FUNCS MALLOC_FUNCS_MALLOC
+// See mozmemory_wrap.h for more details. This file is part of libmozglue, so
+// it needs to use _impl suffixes.
+#define MALLOC_DECL(name, return_type, ...) \
+  extern "C" MOZ_MEMORY_API return_type name ## _impl(__VA_ARGS__);
+#include "malloc_decls.h"
+#endif
+
+#define __INLINE_ISEQUAL_GUID
+#define _WINSOCKAPI_ // Prevent windows.h from including winsock 1.0 headers
+#include <windows.h>
+#undef _WINSOCKAPI_
+#include <winsock2.h>
+#include <ws2spi.h>
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Move.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Vector.h"
+#include "nsWindowsHelpers.h"
+
+/**
+ * IMPORTANT: How the passthrough LSP knows which LSP it is substituting for:
+ *
+ * winsock2 is architected such that its API is exposed by ws2_32.dll but is
+ * actually implemented in another DLL. This implementation DLL is called the
+ * "Base Protocol."
+ *
+ * In addition, winsock2 permits various filter DLLs to be installed between
+ * ws2_32.dll and its Base Protocol. These filter DLLs are called "Layered
+ * Protocols." Layered Protocols are the LSPs that we are interested in
+ * substituting.
+ *
+ * Since normally the Layered Protocols know who they are, winsock2 does not
+ * provide that information when it calls back into the DLL. In our case, we can
+ * substitute for any Layered Protocol DLL. Since winsock2 does not tell us
+ * for whom we are substituting, we must figure that out ourselves. This is
+ * important because we are responsible for loading the next layer in the stack.
+ *
+ * We learn who we are supposed to be by obtaining the list of registered LSPs
+ * and searching it for the GUID of a blocked LSP. Once we have found that GUID,
+ * we have found the LSP that we are substituting for.
+ *
+ * Further note that, unlike normal LSPs, it is possible (however unlikely)
+ * that a user might have multiple LSPs installed that we want to substitute. We
+ * use the same technique in this case as well, but we count the number of times
+ * that our WSPStartup has been reentered. If it has been reentered N times,
+ * then the LSP we are substituting for is the Nth blocked GUID found in the
+ * protocol search.
+ */
+
+namespace {
+
+const GUID gLayerGuids[] = {
+  // testlsp GUID. This must match gTestLspGuid declared in TestDllBlocklist.cpp
+  { 0x9414bc49, 0x7f64, 0x4a70, { 0x98, 0xc2, 0xba, 0x0, 0xec, 0xe, 0x43, 0x60 } },
+  // testlsp2 GUID. This must match gTestLsp2Guid declared in TestDllBlocklist.cpp
+  { 0x6fc98c7c, 0x48ee, 0x41a0, { 0xa3, 0xa4, 0x7b, 0x9e, 0x9f, 0x39, 0xb7, 0xb6 } }
+};
+
+class MOZ_RAII ProtocolList
+{
+public:
+  ProtocolList()
+    : mCount(0)
+  {
+    DWORD size;
+    INT error;
+    int result = ::WSCEnumProtocols(nullptr, nullptr, &size, &error);
+    if (result == SOCKET_ERROR && error != WSAENOBUFS) {
+      return;
+    }
+    int count = size / sizeof(WSAPROTOCOL_INFOW);
+    MOZ_ASSERT(size % sizeof(WSAPROTOCOL_INFOW) == 0);
+    auto providerData = mozilla::MakeUnique<WSAPROTOCOL_INFOW[]>(count);
+    // Make sure that if the calculation of count rounded down (it shouldn't
+    // have, but...) that we have adjusted size accordingly to avoid overrun.
+    size = count * sizeof(WSAPROTOCOL_INFOW);
+    count = WSCEnumProtocols(nullptr, providerData.get(), &size, &error);
+    if (count == SOCKET_ERROR) {
+      return;
+    }
+    mProtocolData = mozilla::Move(providerData);
+    mCount = count;
+  }
+
+  WSAPROTOCOL_INFOW*
+  Find(DWORD aCatId)
+  {
+    for (int i = 0; i < mCount; ++i) {
+      if (mProtocolData[i].dwCatalogEntryId == aCatId) {
+        return &mProtocolData[i];
+      }
+    }
+    return nullptr;
+  }
+
+  WSAPROTOCOL_INFOW*
+  FindLayerNamed(const wchar_t* aFullyQualifiedFileName)
+  {
+    for (int i = 0; i < mCount; ++i) {
+      WSAPROTOCOL_INFOW& info = mProtocolData[i];
+      if (info.ProtocolChain.ChainLen != LAYERED_PROTOCOL) {
+        continue;
+      }
+
+      wchar_t provPath[MAX_PATH + 1] = {};
+      INT provPathLen = mozilla::ArrayLength(provPath);
+      INT errorCode;
+      int status = ::WSCGetProviderPath(&info.ProviderId, provPath,
+                                        &provPathLen, &errorCode);
+      if (status) {
+        continue;
+      }
+
+      DWORD expandedProvPathLen = ::ExpandEnvironmentStringsW(provPath, nullptr,
+                                                              0);
+      if (!expandedProvPathLen) {
+        continue;
+      }
+
+      auto expandedProvPath = mozilla::MakeUnique<wchar_t[]>(expandedProvPathLen);
+      DWORD expandResult = ::ExpandEnvironmentStringsW(provPath,
+                                                       expandedProvPath.get(),
+                                                       expandedProvPathLen);
+      if (expandResult != expandedProvPathLen) {
+        continue;
+      }
+
+      if (!_wcsnicmp(aFullyQualifiedFileName,
+                     expandedProvPath.get(), expandedProvPathLen)) {
+        return &info;
+      }
+    }
+
+    return nullptr;
+  }
+
+private:
+  ProtocolList(const ProtocolList&) = delete;
+  ProtocolList& operator=(const ProtocolList&) = delete;
+
+  int                                     mCount;
+  mozilla::UniquePtr<WSAPROTOCOL_INFOW[]> mProtocolData;
+};
+
+class MOZ_RAII ReentryCounter
+{
+public:
+  ReentryCounter()
+  {
+    MOZ_ASSERT(sReentryCount >= 0);
+    ++sReentryCount;
+  }
+
+  ~ReentryCounter()
+  {
+    --sReentryCount;
+    MOZ_ASSERT(sReentryCount >= 0);
+  }
+
+  static int GetCount()
+  {
+    return sReentryCount;
+  }
+
+private:
+  ReentryCounter(const ReentryCounter&) = delete;
+  ReentryCounter& operator=(const ReentryCounter&) = delete;
+
+  static int sReentryCount;
+};
+
+int ReentryCounter::sReentryCount = 0;
+
+bool IsGuidBlocked(REFGUID aGuid)
+{
+  for (unsigned int i = 0; i < mozilla::ArrayLength(gLayerGuids); ++i) {
+    if (aGuid == gLayerGuids[i]) {
+      return true;
+    }
+  }
+  return false;
+}
+
+// We need to follow chain entries all the way to their layer catalog entry.
+// This is basically a depth-first search using the catalog id as a reference
+// to the next node in the graph.
+bool IsProviderBlocked(ProtocolList& aProtocolList, DWORD aCatalogId,
+                       int& aBlockCount, int& aChainIndex)
+{
+  WSAPROTOCOL_INFOW* catEntry = aProtocolList.Find(aCatalogId);
+  if (!catEntry) {
+    return false;
+  }
+  // If we've reached a base protocol node, return false; we don't block those
+  if (catEntry->ProtocolChain.ChainLen == BASE_PROTOCOL) {
+    return false;
+  }
+  // If we've reached a layer node, check the GUID against the blocklist
+  if (catEntry->ProtocolChain.ChainLen == LAYERED_PROTOCOL) {
+    if (IsGuidBlocked(catEntry->ProviderId)) {
+      ++aBlockCount;
+      // Since we have reentered N times, our 'this' catalog entry will
+      // correspond to the Nth blocked entry. Otherwise we keep searching.
+      if (aBlockCount == ReentryCounter::GetCount()) {
+        aChainIndex = 0;
+        return true;
+      }
+    }
+    return false;
+  }
+  // Otherwise we need to walk the next chain
+  WSAPROTOCOLCHAIN& chain = catEntry->ProtocolChain;
+  for (int i = 0; i < chain.ChainLen; ++i) {
+    int chainIndex = 0; // chainIndex is just a dummy placeholder
+    if (IsProviderBlocked(aProtocolList, chain.ChainEntries[i], aBlockCount,
+                          chainIndex)) {
+      // We save the index into the top-level chain before returning
+      aChainIndex = i;
+      return true;
+    }
+  }
+  return false;
+}
+
+int GetNextChainIndex(ProtocolList& aProtocolList, DWORD aCatalogId)
+{
+  int blockCount = 0;
+  int chainIndex = 0;
+  if (!IsProviderBlocked(aProtocolList, aCatalogId, blockCount, chainIndex)) {
+    // This shouldn't be possible because our LSP is by definition not a
+    // base provider -- there must be at least one protocol chain.
+    MOZ_ASSERT(false);
+    return MAX_PROTOCOL_CHAIN;
+  }
+  return chainIndex + 1;
+}
+
+class Mutex
+{
+public:
+  Mutex()
+  {
+    const DWORD kNSPRSpinCount = 1500;
+    ::InitializeCriticalSectionAndSpinCount(&mCS, kNSPRSpinCount);
+  }
+
+  ~Mutex()
+  {
+    ::DeleteCriticalSection(&mCS);
+  }
+
+private:
+  void Enter()
+  {
+    ::EnterCriticalSection(&mCS);
+  }
+
+  void Leave()
+  {
+    ::LeaveCriticalSection(&mCS);
+  }
+
+  Mutex(const Mutex&) = delete;
+  Mutex& operator=(const Mutex&) = delete;
+
+  CRITICAL_SECTION  mCS;
+
+  friend class CriticalRegion;
+};
+
+class MOZ_RAII CriticalRegion
+{
+public:
+  explicit CriticalRegion(Mutex& aMutex)
+    : mMutex(aMutex)
+  {
+    aMutex.Enter();
+  }
+
+  ~CriticalRegion()
+  {
+    mMutex.Leave();
+  }
+
+private:
+  CriticalRegion(const CriticalRegion&) = delete;
+  CriticalRegion& operator=(const CriticalRegion&) = delete;
+
+  Mutex& mMutex;
+};
+
+class LSP
+{
+public:
+  LSP(const wchar_t* aLibraryPath, const WSAPROTOCOL_INFOW& aProtocolInfo)
+    : mNextLSP(::LoadLibraryW(aLibraryPath))
+    , mNextLSPStartupFn(nullptr)
+    , mNextLSPCleanupFn(nullptr)
+  {
+    if (!mNextLSP) {
+      return;
+    }
+    mNextLSPStartupFn = (LPWSPSTARTUP)::GetProcAddress(mNextLSP, "WSPStartup");
+    if (!mNextLSPStartupFn) {
+      return;
+    }
+    ZeroMemory(&mNextLSPData, sizeof(WSPDATA));
+    memcpy(&mNextLSPProtocolInfo, &aProtocolInfo, sizeof(WSAPROTOCOL_INFOW));
+  }
+
+  static int
+  TryStartup(WORD aVersionRequested,
+             LPWSPDATA aWSPData,
+             LPWSAPROTOCOL_INFOW aProtocolInfo,
+             WSPUPCALLTABLE aUpcallTable,
+             LPWSPPROC_TABLE aProcTable);
+
+  int Cleanup(INT& aErrno)
+  {
+    MOZ_ASSERT(mNextLSPCleanupFn);
+    return mNextLSPCleanupFn(&aErrno);
+  }
+
+private:
+  int
+  Startup(WORD aVersionRequested, LPWSPDATA aData, WSPUPCALLTABLE& aUpcallTable,
+          LPWSPPROC_TABLE aProcTable)
+  {
+    int result = mNextLSPStartupFn(aVersionRequested, &mNextLSPData,
+                                   &mNextLSPProtocolInfo, aUpcallTable,
+                                   aProcTable);
+    if (result) {
+      return result;
+    }
+    // We'll populate our own aData with some of the same info as the next
+    // entry in the chain
+    ZeroMemory(aData, sizeof(WSPDATA));
+    aData->wVersion = mNextLSPData.wVersion;
+    aData->wHighVersion = mNextLSPData.wHighVersion;
+    // Save the LSP's cleanup function and substitute ours for upstream
+    mNextLSPCleanupFn = aProcTable->lpWSPCleanup;
+    aProcTable->lpWSPCleanup = &WSPCleanup;
+    return 0;
+  }
+
+  inline bool
+  ok() const
+  {
+    return !!mNextLSPStartupFn;
+  }
+
+  nsModuleHandle    mNextLSP;
+  WSAPROTOCOL_INFOW mNextLSPProtocolInfo;
+  LPWSPSTARTUP      mNextLSPStartupFn;
+  LPWSPCLEANUP      mNextLSPCleanupFn;
+  WSPDATA           mNextLSPData;
+
+  static Mutex            sMutex;
+
+  static int WSPAPI WSPCleanup(LPINT aErrno);
+
+  static mozilla::UniquePtr<LSP>
+  VerifyProtocolInfoAndLoadNextLSP(WSPUPCALLTABLE& aUpcallTable,
+                                   LPWSAPROTOCOL_INFOW aProtocolInfo)
+  {
+    if (aProtocolInfo->ProtocolChain.ChainLen < BASE_PROTOCOL) {
+      // We should not be seeing info for a base provider
+      return nullptr;
+    }
+    ProtocolList protocolList;
+    // Find the entry in the protocol chain that corresponds to us
+    int nextChainIndex = GetNextChainIndex(protocolList,
+                                           aProtocolInfo->dwCatalogEntryId);
+    if (nextChainIndex >= aProtocolInfo->ProtocolChain.ChainLen) {
+      // Uh-oh, nextChainIndex is out of bounds! This shouldn't be possible
+      // for layer providers.
+      return nullptr;
+    }
+    if (nextChainIndex == MAX_PROTOCOL_CHAIN) {
+      // We didn't find the next link in the chain, something is wrong
+      return nullptr;
+    }
+    DWORD nextChainId = aProtocolInfo->ProtocolChain.ChainEntries[nextChainIndex];
+    bool isNextLinkBase = nextChainIndex == aProtocolInfo->ProtocolChain.ChainLen - 1;
+    // Find the GUID for the provider corresponding to nextChainId
+    WSAPROTOCOL_INFOW* useProvider = protocolList.Find(nextChainId);
+    if (!useProvider) {
+      return nullptr;
+    }
+    INT error;
+    wchar_t provPath[MAX_PATH + 1] = {0};
+    INT provPathLen = MAX_PATH;
+    int result = aUpcallTable.lpWPUGetProviderPath(&useProvider->ProviderId,
+                                                   provPath, &provPathLen,
+                                                   &error);
+    if (result == SOCKET_ERROR) {
+      return nullptr;
+    }
+    DWORD desiredLen = ExpandEnvironmentStringsW(provPath, nullptr, 0);
+    if (!desiredLen) {
+      return nullptr;
+    }
+    auto expandedProvPath = mozilla::MakeUnique<wchar_t[]>(desiredLen);
+    DWORD expandedLen = ExpandEnvironmentStringsW(provPath,
+                                                  expandedProvPath.get(),
+                                                  desiredLen);
+    if (!expandedLen || expandedLen > desiredLen) {
+      return nullptr;
+    }
+    /* Subtle: If the next LSP in the chain is a base provider, we should specify
+               its base WSAPROTOCOL_INFOW, not the chain. OTOH we *do* need to
+               supply the dwProviderReserved field that was included with the
+               chain. */
+    if (isNextLinkBase) {
+      useProvider->dwProviderReserved = aProtocolInfo->dwProviderReserved;
+    } else {
+      useProvider = aProtocolInfo;
+    }
+    return mozilla::MakeUnique<LSP>(expandedProvPath.get(), *useProvider);
+  }
+};
+
+Mutex LSP::sMutex;
+
+struct LSPInfo
+{
+  LSPInfo()
+    : mRefCnt(0)
+  {
+  }
+
+  explicit LSPInfo(mozilla::UniquePtr<LSP> aLsp)
+    : mLsp(mozilla::Move(aLsp))
+    , mRefCnt(1)
+  {
+  }
+
+  explicit LSPInfo(LSPInfo&& aOther)
+    : mLsp(mozilla::Move(aOther.mLsp))
+    , mRefCnt(aOther.mRefCnt)
+  {
+  }
+
+  LSPInfo(const LSPInfo&) = delete;
+  LSPInfo& operator=(const LSPInfo&) = delete;
+
+  mozilla::UniquePtr<LSP>  mLsp;
+  unsigned int    mRefCnt;
+};
+
+class LSPManager
+{
+public:
+  // NB: Use default constructors and destructors
+
+  bool IncrementLSP()
+  {
+    LSPInfo& info = GetLSPInfo();
+    if (!info.mLsp) {
+      return false;
+    }
+    ++info.mRefCnt;
+    return true;
+  }
+
+  bool DecrementLSP(INT& aErrno)
+  {
+    LSPInfo& info = GetLSPInfo();
+    if (!info.mLsp) {
+      aErrno = WSANOTINITIALISED;
+      return false;
+    }
+    if (--info.mRefCnt > 0) {
+      return true;
+    }
+    MOZ_ASSERT(info.mRefCnt == 0);
+    int wsResult = info.mLsp->Cleanup(aErrno);
+    info.mLsp = nullptr;
+    return wsResult == 0;
+  }
+
+  void SetLSPInfo(mozilla::UniquePtr<LSP> aLsp)
+  {
+    MOZ_ASSERT(aLsp);
+    LSPInfo& info = GetLSPInfo();
+    info.mLsp = mozilla::Move(aLsp);
+    info.mRefCnt = 1;
+  }
+
+  void ClearLSPInfo()
+  {
+    LSPInfo& info = GetLSPInfo();
+    info.mLsp = nullptr;
+    info.mRefCnt = 0;
+  }
+
+  LSPInfo& GetLSPInfo()
+  {
+    if (!mLspInfoVec) {
+      mLspInfoVec = new mozilla::Vector<LSPInfo>();
+    }
+    int rcount = ReentryCounter::GetCount();
+    MOZ_ASSERT(rcount > 0);
+    if (size_t(rcount) > mLspInfoVec->length()) {
+      MOZ_ASSERT(size_t(rcount) == mLspInfoVec->length() + 1);
+      mLspInfoVec->resize(rcount);
+    }
+    LSPInfo& info = mLspInfoVec->begin()[rcount - 1];
+    MOZ_ASSERT((info.mLsp && info.mRefCnt > 0) ||
+               (!info.mLsp && info.mRefCnt == 0));
+    return info;
+  }
+
+private:
+  mozilla::Vector<LSPInfo>* mLspInfoVec;
+};
+
+LSPManager sLspManager;
+
+int
+LSP::TryStartup(WORD aVersionRequested,
+                LPWSPDATA aWSPData,
+                LPWSAPROTOCOL_INFOW aProtocolInfo,
+                WSPUPCALLTABLE aUpcallTable,
+                LPWSPPROC_TABLE aProcTable)
+{
+  CriticalRegion lock(sMutex);
+  ReentryCounter countme;
+  if (sLspManager.IncrementLSP()) {
+    return 0;
+  }
+  auto lsp = VerifyProtocolInfoAndLoadNextLSP(aUpcallTable, aProtocolInfo);
+  if (!lsp || !lsp->ok()) {
+    return WSASYSNOTREADY;
+  }
+  int result = lsp->Startup(aVersionRequested, aWSPData, aUpcallTable,
+                            aProcTable);
+  if (!result) {
+    sLspManager.SetLSPInfo(mozilla::Move(lsp));
+  }
+  return result;
+}
+
+int WSPAPI
+LSP::WSPCleanup(LPINT aErrno)
+{
+  CriticalRegion lock(sMutex);
+  ReentryCounter countme;
+  INT error;
+  if (!sLspManager.DecrementLSP(error)) {
+    if (aErrno) {
+      *aErrno = error;
+    }
+    return SOCKET_ERROR;
+  }
+  return 0;
+}
+
+} // anonymous namespace
+
+namespace mozilla {
+namespace lsp {
+
+bool
+IsRegisteredBlockedLSP(const wchar_t* aFullyQualifiedFileName)
+{
+  ProtocolList registeredProtocols;
+
+  WSAPROTOCOL_INFOW* protocol =
+    registeredProtocols.FindLayerNamed(aFullyQualifiedFileName);
+  if (!protocol) {
+    return false;
+  }
+
+  return IsGuidBlocked(protocol->ProviderId);
+}
+
+} // namespace lsp
+} // namespace mozilla
+
+extern "C" int WSPAPI
+WSPStartup(WORD aVersionRequested,
+           LPWSPDATA aWSPData,
+           LPWSAPROTOCOL_INFOW aProtocolInfo,
+           WSPUPCALLTABLE aUpcallTable,
+           LPWSPPROC_TABLE aProcTable)
+{
+  return LSP::TryStartup(aVersionRequested, aWSPData, aProtocolInfo,
+                         aUpcallTable, aProcTable);
+}
new file mode 100644
--- /dev/null
+++ b/mozglue/build/WindowsLSPPassthrough.h
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_lsp_WindowsLSPPassthrough_h
+#define mozilla_lsp_WindowsLSPPassthrough_h
+
+namespace mozilla {
+namespace lsp {
+
+bool IsRegisteredBlockedLSP(const wchar_t* aFullyQualifiedFileName);
+
+} // namespace lsp
+} // namespace mozilla
+
+#endif // mozilla_lsp_WindowsLSPPassthrough_h
+
--- a/mozglue/build/moz.build
+++ b/mozglue/build/moz.build
@@ -43,20 +43,22 @@ if not CONFIG['JS_STANDALONE']:
         ]
 
     if CONFIG['OS_TARGET'] == 'WINNT':
         LOCAL_INCLUDES += [
             '/memory/build',
         ]
         SOURCES += [
             'WindowsDllBlocklist.cpp',
+            'WindowsLSPPassthrough.cpp',
         ]
         DISABLE_STL_WRAPPING = True
         OS_LIBS += [
             'version',
+            'ws2_32',
         ]
 
     EXPORTS.mozilla += [
         'arm.h',
         'mips.h',
         'SSE.h',
         'WindowsDllBlocklist.h',
     ]
--- a/mozglue/build/mozglue.def.in
+++ b/mozglue/build/mozglue.def.in
@@ -32,8 +32,9 @@ EXPORTS
   _strdup=wrap_strdup
   wcsdup=wrap_wcsdup
   _wcsdup=wrap_wcsdup
   jemalloc_stats
   jemalloc_free_dirty_pages
   ; A hack to work around the CRT (see giant comment in Makefile.in)
   frex=dumb_free_thunk
 #endif
+  WSPStartup
--- a/mozglue/tests/moz.build
+++ b/mozglue/tests/moz.build
@@ -1,11 +1,19 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+if CONFIG['OS_TARGET'] == 'WINNT':
+    DIRS += [
+        'testdllblocklist',
+        'testlspblocklist',
+        'testlsp',
+        'testlsp2',
+    ]
+
 DISABLE_STL_WRAPPING = True
 
 GeckoCppUnitTests([
     'ShowSSEConfig',
 ], linkage=None)
new file mode 100644
--- /dev/null
+++ b/mozglue/tests/testlsp/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+SharedLibrary('testlsp')
+DEFFILE = SRCDIR + '/testlsp.def'
+SOURCES += [
+    'testlsp.cpp',
+]
+OS_LIBS += [
+    'ws2_32',
+]
new file mode 100644
--- /dev/null
+++ b/mozglue/tests/testlsp/testlsp.cpp
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 40; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#define _WINSOCKAPI_ // Prevent windows.h from including winsock 1.0 headers
+#include <windows.h>
+#undef _WINSOCKAPI_
+#include <winsock2.h>
+#include <ws2spi.h>
+
+extern "C" int WSPAPI
+WSPStartup(WORD aVersionRequested,
+           LPWSPDATA aWSPData,
+           LPWSAPROTOCOL_INFOW aProtocolInfo,
+           WSPUPCALLTABLE aUpcallTable,
+           LPWSPPROC_TABLE aProcTable)
+{
+  return 0;
+}
+
+BOOL WINAPI
+DllMain(HINSTANCE aInstance, DWORD aReason, LPVOID aReserved)
+{
+  return TRUE;
+}
new file mode 100644
--- /dev/null
+++ b/mozglue/tests/testlsp/testlsp.def
@@ -0,0 +1,8 @@
+; 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/.
+
+LIBRARY testlsp.dll
+
+EXPORTS
+  WSPStartup
new file mode 100644
--- /dev/null
+++ b/mozglue/tests/testlsp2/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+SharedLibrary('testlsp2')
+DEFFILE = SRCDIR + '/testlsp2.def'
+SOURCES += [
+    '../testlsp/testlsp.cpp',
+]
+OS_LIBS += [
+    'ws2_32',
+]
new file mode 100644
--- /dev/null
+++ b/mozglue/tests/testlsp2/testlsp2.def
@@ -0,0 +1,8 @@
+; 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/.
+
+LIBRARY testlsp2.dll
+
+EXPORTS
+  WSPStartup
new file mode 100644
--- /dev/null
+++ b/mozglue/tests/testlspblocklist/TestLspBlocklist.cpp
@@ -0,0 +1,166 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#undef _WIN32_WINNT
+#define _WIN32_WINNT 0x0600
+#define _WINSOCKAPI_
+#include <windows.h>
+#undef _WINSOCKAPI_
+#include <winsock2.h>
+#include <ws2spi.h>
+
+#include <stdlib.h>
+
+#include "mozilla/WindowsDLLBlocklist.h"
+
+// None of these globals are const-qualified because the WSC API declarations
+// are horrible and are missing const qualifiers all over the place :-(
+
+static wchar_t gTestLspDllName[] = L"testlsp.dll";
+static wchar_t gTestLsp2DllName[] = L"testlsp2.dll";
+static wchar_t gTestLspProvName[] = L"Mozilla DLL Blocklist Test Provider";
+static wchar_t gTestLsp2ProvName[] = L"Mozilla DLL Blocklist Test Provider 2";
+
+// The following GUIDs must match their corresponding entries in gLSPLayerGuids
+// in WindowsLSPPassthrough.cpp:
+
+// {9414BC49-7F64-4A70-98C2-BA00EC0E4360}
+static GUID gTestLspGuid =
+{ 0x9414bc49, 0x7f64, 0x4a70, { 0x98, 0xc2, 0xba, 0x0, 0xec, 0xe, 0x43, 0x60 } };
+// {6FC98C7C-48EE-41A0-A3A4-7B9E9F39B7B6}
+static GUID gTestLsp2Guid =
+{ 0x6fc98c7c, 0x48ee, 0x41a0, { 0xa3, 0xa4, 0x7b, 0x9e, 0x9f, 0x39, 0xb7, 0xb6 } };
+
+bool
+installLib(wchar_t* aLspDllName, GUID& aLayerGuid, wchar_t* aLspDesc)
+{
+  wchar_t dllPath[MAX_PATH + 1] = {0};
+  if (!_wfullpath(dllPath, aLspDllName, MAX_PATH)) {
+    printf("_wfullpath failed\n");
+    return false;
+  }
+
+  DWORD catalogId = 0;
+  INT errorCode;
+  int wsResult = WSCInstallProviderAndChains(&aLayerGuid, dllPath, aLspDesc, 0,
+                                             nullptr, 0, &catalogId, &errorCode);
+  if (wsResult == SOCKET_ERROR) {
+    printf("WSCInstallProvder failed for \"%S\" with code %d\n", aLspDllName,
+           errorCode);
+    return false;
+  }
+  return true;
+}
+
+bool
+deinstallLib(GUID& aLayerGuid)
+{
+  INT errorCode;
+  int wsResult = WSCDeinstallProvider(&aLayerGuid, &errorCode);
+  if (wsResult == SOCKET_ERROR) {
+    printf("WSCDeinstallProvider failed with code %d\n", errorCode);
+    return false;
+  }
+  return true;
+}
+
+bool
+deinstall()
+{
+  bool result = deinstallLib(gTestLspGuid);
+  result &= deinstallLib(gTestLsp2Guid);
+  return result;
+}
+
+bool
+install()
+{
+  int failures = 0;
+  failures += !installLib(gTestLsp2DllName, gTestLsp2Guid, gTestLsp2ProvName);
+  failures += !installLib(gTestLspDllName, gTestLspGuid, gTestLspProvName);
+  if (failures) {
+    deinstall();
+    return false;
+  }
+  return true;
+}
+
+int main(int argc, char *argv[])
+{
+  if (argc == 2) {
+    if (strlen(argv[1]) >= 2) {
+      if (argv[1][1] == 'd') {
+        return !deinstall();
+      }
+      if (argv[1][1] == 'i') {
+        return !install();
+      }
+    }
+    printf("argument \"%s\" ignored\n", argv[1]);
+  }
+
+  if (!install()) {
+    return 1;
+  }
+
+  bool failed = false;
+  DllBlocklist_Initialize();
+  HMODULE testLoad = ::LoadLibraryW(gTestLspDllName);
+  if (!testLoad) {
+    printf("Loading the test LSP DLL unexpectedly failed with code %u\n",
+           GetLastError());
+    failed = true;
+  }
+  if (testLoad) {
+    wchar_t modulePath[MAX_PATH + 1] = {0};
+    DWORD ok = GetModuleFileNameW(testLoad, modulePath, MAX_PATH);
+    if (!ok) {
+      printf("GetModuleFileName failed with code %u\n", GetLastError());
+      failed = true;
+    }
+    wchar_t moduleFileName[_MAX_FNAME] = {0};
+    size_t moduleFileNameLen = _MAX_FNAME;
+    if (_wsplitpath_s(modulePath, nullptr, 0, nullptr, 0, moduleFileName,
+                      moduleFileNameLen, nullptr, 0)) {
+      printf("_wsplitpath_s failed\n");
+      failed = true;
+    }
+    if (wcsnicmp(moduleFileName, L"mozglue", moduleFileNameLen)) {
+      printf("mozglue passthrough LSP was not substituted\n");
+      failed = true;
+    }
+  }
+
+  WSADATA wsaData;
+  int wsResult = WSAStartup(WINSOCK_VERSION, &wsaData);
+  if (wsResult) {
+    printf("WSAStartup failed with code %d\n", wsResult);
+    failed = true;
+  }
+
+  SOCKET testSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+  if (testSocket == INVALID_SOCKET) {
+    printf("socket failed with code %d\n", WSAGetLastError());
+    failed = true;
+  }
+  if (GetModuleHandleW(gTestLspDllName)) {
+    printf("test LSP was loaded as part of protocol chain\n");
+    failed = true;
+  }
+  if (testSocket != INVALID_SOCKET) {
+    closesocket(testSocket);
+  }
+
+  WSACleanup();
+
+  failed |= !deinstall();
+  int result = 0;
+  if (failed) {
+    result = 1;
+  }
+  return result;
+}
+
new file mode 100644
--- /dev/null
+++ b/mozglue/tests/testlspblocklist/moz.build
@@ -0,0 +1,20 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This cannot currently run as part of a test harness because it requires
+# full administrator privileges.
+GeckoSimplePrograms([
+    'TestLspBlocklist',
+], linkage=None, mozglue='program')
+
+LDFLAGS += [
+    '-MANIFEST:embed',
+    '-MANIFESTUAC:level="requireAdministrator"',
+]
+
+OS_LIBS += [
+    'ws2_32',
+]