summaryrefslogtreecommitdiffstats
path: root/toolkit/xre/dllservices/tests/gtest/TestDLLBlocklist.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /toolkit/xre/dllservices/tests/gtest/TestDLLBlocklist.cpp
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/xre/dllservices/tests/gtest/TestDLLBlocklist.cpp')
-rw-r--r--toolkit/xre/dllservices/tests/gtest/TestDLLBlocklist.cpp475
1 files changed, 475 insertions, 0 deletions
diff --git a/toolkit/xre/dllservices/tests/gtest/TestDLLBlocklist.cpp b/toolkit/xre/dllservices/tests/gtest/TestDLLBlocklist.cpp
new file mode 100644
index 0000000000..938f63a524
--- /dev/null
+++ b/toolkit/xre/dllservices/tests/gtest/TestDLLBlocklist.cpp
@@ -0,0 +1,475 @@
+/* -*- 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/. */
+
+#include <windows.h>
+#include <winternl.h>
+
+#include <process.h>
+
+#include <array>
+#include <functional>
+
+#include "gtest/gtest.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Char16.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "mozilla/Services.h"
+#include "mozilla/WinDllServices.h"
+#include "mozilla/WindowsStackCookie.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsIThread.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsUnicharUtils.h"
+#include "nsWindowsHelpers.h"
+
+static nsString GetFullPath(const nsAString& aLeaf) {
+ nsCOMPtr<nsIFile> f;
+
+ EXPECT_TRUE(NS_SUCCEEDED(
+ NS_GetSpecialDirectory(NS_OS_CURRENT_WORKING_DIR, getter_AddRefs(f))));
+
+ EXPECT_NS_SUCCEEDED(f->Append(aLeaf));
+
+ bool exists;
+ EXPECT_TRUE(NS_SUCCEEDED(f->Exists(&exists)) && exists);
+
+ nsString ret;
+ EXPECT_NS_SUCCEEDED(f->GetPath(ret));
+ return ret;
+}
+
+void FlushMainThreadLoop() {
+ nsCOMPtr<nsIThread> mainThread;
+ nsresult rv = NS_GetMainThread(getter_AddRefs(mainThread));
+ ASSERT_NS_SUCCEEDED(rv);
+
+ rv = NS_OK;
+ bool processed = true;
+ while (processed && NS_SUCCEEDED(rv)) {
+ rv = mainThread->ProcessNextEvent(false, &processed);
+ }
+}
+
+class TestDLLLoadObserver : public nsIObserver {
+ public:
+ using DLLFilter = std::function<bool(const char16_t*)>;
+
+ explicit TestDLLLoadObserver(DLLFilter dllFilter)
+ : mDllFilter(std::move(dllFilter)),
+ mMainThreadNotificationsCount(0),
+ mNonMainThreadNotificationsCount(0){};
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) override {
+ if (!mDllFilter(aData)) {
+ return NS_OK;
+ }
+ if (0 == strcmp(aTopic, mozilla::DllServices::kTopicDllLoadedMainThread)) {
+ ++mMainThreadNotificationsCount;
+ } else {
+ EXPECT_TRUE(
+ 0 ==
+ strcmp(aTopic, mozilla::DllServices::kTopicDllLoadedNonMainThread));
+ ++mNonMainThreadNotificationsCount;
+ }
+ return NS_OK;
+ }
+
+ void Init() {
+ nsCOMPtr<nsIObserverService> obsServ(
+ mozilla::services::GetObserverService());
+
+ EXPECT_TRUE(obsServ);
+
+ obsServ->AddObserver(this, mozilla::DllServices::kTopicDllLoadedMainThread,
+ false);
+ obsServ->AddObserver(
+ this, mozilla::DllServices::kTopicDllLoadedNonMainThread, false);
+ }
+
+ void Exit() {
+ // Observe only gets called if/when we flush the main thread loop.
+ FlushMainThreadLoop();
+
+ nsCOMPtr<nsIObserverService> obsServ(
+ mozilla::services::GetObserverService());
+
+ EXPECT_TRUE(obsServ);
+
+ obsServ->RemoveObserver(this,
+ mozilla::DllServices::kTopicDllLoadedMainThread);
+ obsServ->RemoveObserver(this,
+ mozilla::DllServices::kTopicDllLoadedNonMainThread);
+ }
+
+ int MainThreadNotificationsCount() { return mMainThreadNotificationsCount; }
+
+ int NonMainThreadNotificationsCount() {
+ return mNonMainThreadNotificationsCount;
+ }
+
+ private:
+ virtual ~TestDLLLoadObserver() = default;
+
+ DLLFilter mDllFilter;
+ int mMainThreadNotificationsCount;
+ int mNonMainThreadNotificationsCount;
+};
+
+NS_IMPL_ISUPPORTS(TestDLLLoadObserver, nsIObserver)
+
+TEST(TestDllBlocklist, BlockDllByName)
+{
+ // The DLL name has capital letters, so this also tests that the comparison
+ // is case-insensitive.
+ constexpr auto kLeafName = u"TestDllBlocklist_MatchByName.dll"_ns;
+ nsString dllPath = GetFullPath(kLeafName);
+
+ nsModuleHandle hDll(::LoadLibraryW(dllPath.get()));
+
+ EXPECT_TRUE(!hDll);
+ EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get()));
+
+ hDll.own(::LoadLibraryExW(dllPath.get(), nullptr, LOAD_LIBRARY_AS_DATAFILE));
+ // Mapped as MEM_MAPPED + PAGE_READONLY
+ EXPECT_TRUE(hDll);
+}
+
+TEST(TestDllBlocklist, BlockDllByVersion)
+{
+ constexpr auto kLeafName = u"TestDllBlocklist_MatchByVersion.dll"_ns;
+ nsString dllPath = GetFullPath(kLeafName);
+
+ nsModuleHandle hDll(::LoadLibraryW(dllPath.get()));
+
+ EXPECT_TRUE(!hDll);
+ EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get()));
+
+ hDll.own(
+ ::LoadLibraryExW(dllPath.get(), nullptr, LOAD_LIBRARY_AS_IMAGE_RESOURCE));
+ // Mapped as MEM_IMAGE + PAGE_READONLY
+ EXPECT_TRUE(hDll);
+}
+
+TEST(TestDllBlocklist, AllowDllByVersion)
+{
+ constexpr auto kLeafName = u"TestDllBlocklist_AllowByVersion.dll"_ns;
+ nsString dllPath = GetFullPath(kLeafName);
+
+ nsModuleHandle hDll(::LoadLibraryW(dllPath.get()));
+
+ EXPECT_TRUE(!!hDll);
+ EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get()));
+}
+
+TEST(TestDllBlocklist, GPUProcessOnly_AllowInMainProcess)
+{
+ constexpr auto kLeafName = u"TestDllBlocklist_GPUProcessOnly.dll"_ns;
+ nsString dllPath = GetFullPath(kLeafName);
+
+ nsModuleHandle hDll(::LoadLibraryW(dllPath.get()));
+
+ EXPECT_TRUE(!!hDll);
+ EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get()));
+}
+
+TEST(TestDllBlocklist, SocketProcessOnly_AllowInMainProcess)
+{
+ constexpr auto kLeafName = u"TestDllBlocklist_SocketProcessOnly.dll"_ns;
+ nsString dllPath = GetFullPath(kLeafName);
+
+ nsModuleHandle hDll(::LoadLibraryW(dllPath.get()));
+
+ EXPECT_TRUE(!!hDll);
+ EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get()));
+}
+
+TEST(TestDllBlocklist, UtilityProcessOnly_AllowInMainProcess)
+{
+ constexpr auto kLeafName = u"TestDllBlocklist_UtilityProcessOnly.dll"_ns;
+ nsString dllPath = GetFullPath(kLeafName);
+
+ nsModuleHandle hDll(::LoadLibraryW(dllPath.get()));
+
+ EXPECT_TRUE(!!hDll);
+ EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get()));
+}
+
+TEST(TestDllBlocklist, GMPluginProcessOnly_AllowInMainProcess)
+{
+ constexpr auto kLeafName = u"TestDllBlocklist_GMPluginProcessOnly.dll"_ns;
+ nsString dllPath = GetFullPath(kLeafName);
+
+ nsModuleHandle hDll(::LoadLibraryW(dllPath.get()));
+
+ EXPECT_TRUE(!!hDll);
+ EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get()));
+}
+
+// This DLL has two entries; it's blocked for unversioned (i.e. DLLs that
+// have no version information) everywhere and blocked for versions 5.5.5.5 and
+// earlier only in the GPU process. Since the version we're trying to load
+// is 5.5.5.5, it should load in the main process.
+TEST(TestDllBlocklist, MultipleEntriesDifferentProcesses_AllowInMainProcess)
+{
+ constexpr auto kLeafName =
+ u"TestDllBlocklist_MultipleEntries_DifferentProcesses.dll"_ns;
+ nsString dllPath = GetFullPath(kLeafName);
+
+ nsModuleHandle hDll(::LoadLibraryW(dllPath.get()));
+
+ EXPECT_TRUE(!!hDll);
+ EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get()));
+}
+
+TEST(TestDllBlocklist, MultipleEntriesSameProcessBackward_Block)
+{
+ // One entry matches by version and many others do not, so
+ // we should block.
+ constexpr auto kLeafName =
+ u"TestDllBlocklist_MultipleEntries_SameProcessBackward.dll"_ns;
+ nsString dllPath = GetFullPath(kLeafName);
+
+ nsModuleHandle hDll(::LoadLibraryW(dllPath.get()));
+
+ EXPECT_TRUE(!hDll);
+ EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get()));
+
+ hDll.own(
+ ::LoadLibraryExW(dllPath.get(), nullptr, LOAD_LIBRARY_AS_IMAGE_RESOURCE));
+ // Mapped as MEM_IMAGE + PAGE_READONLY
+ EXPECT_TRUE(hDll);
+}
+
+TEST(TestDllBlocklist, MultipleEntriesSameProcessForward_Block)
+{
+ // One entry matches by version and many others do not, so
+ // we should block.
+ constexpr auto kLeafName =
+ u"TestDllBlocklist_MultipleEntries_SameProcessForward.dll"_ns;
+ nsString dllPath = GetFullPath(kLeafName);
+
+ nsModuleHandle hDll(::LoadLibraryW(dllPath.get()));
+
+ EXPECT_TRUE(!hDll);
+ EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get()));
+
+ hDll.own(
+ ::LoadLibraryExW(dllPath.get(), nullptr, LOAD_LIBRARY_AS_IMAGE_RESOURCE));
+ // Mapped as MEM_IMAGE + PAGE_READONLY
+ EXPECT_TRUE(hDll);
+}
+
+#if defined(MOZ_LAUNCHER_PROCESS)
+// RedirectToNoOpEntryPoint needs the launcher process.
+// This test will fail in debug x64 if we mistakenly reintroduce stack buffers
+// in patched_NtMapViewOfSection (see bug 1733532).
+TEST(TestDllBlocklist, NoOpEntryPoint)
+{
+ // DllMain of this dll has MOZ_RELEASE_ASSERT. This test makes sure we load
+ // the module successfully without running DllMain.
+ constexpr auto kLeafName = u"TestDllBlocklist_NoOpEntryPoint.dll"_ns;
+ nsString dllPath = GetFullPath(kLeafName);
+
+ nsModuleHandle hDll(::LoadLibraryW(dllPath.get()));
+
+# if defined(MOZ_ASAN)
+ // With ASAN, the test uses mozglue's blocklist where
+ // REDIRECT_TO_NOOP_ENTRYPOINT is ignored. So LoadLibraryW
+ // is expected to fail.
+ EXPECT_TRUE(!hDll);
+ EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get()));
+# else
+ EXPECT_TRUE(!!hDll);
+ EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get()));
+# endif
+}
+
+// User blocklist needs the launcher process.
+// This test will fail in debug x64 if we mistakenly reintroduce stack buffers
+// in patched_NtMapViewOfSection (see bug 1733532).
+TEST(TestDllBlocklist, UserBlocked)
+{
+ constexpr auto kLeafName = u"TestDllBlocklist_UserBlocked.dll"_ns;
+ nsString dllPath = GetFullPath(kLeafName);
+
+ nsModuleHandle hDll(::LoadLibraryW(dllPath.get()));
+
+// With ASAN, the test uses mozglue's blocklist where
+// the user blocklist is not used.
+# if !defined(MOZ_ASAN)
+ EXPECT_TRUE(!hDll);
+ EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get()));
+# endif
+ hDll.own(::LoadLibraryExW(dllPath.get(), nullptr, LOAD_LIBRARY_AS_DATAFILE));
+ // Mapped as MEM_MAPPED + PAGE_READONLY
+ EXPECT_TRUE(hDll);
+}
+#endif // defined(MOZ_LAUNCHER_PROCESS)
+
+#define DLL_BLOCKLIST_ENTRY(name, ...) {name, __VA_ARGS__},
+#define DLL_BLOCKLIST_STRING_TYPE const char*
+#include "mozilla/WindowsDllBlocklistLegacyDefs.h"
+
+TEST(TestDllBlocklist, BlocklistIntegrity)
+{
+ DECLARE_POINTER_TO_FIRST_DLL_BLOCKLIST_ENTRY(pFirst);
+ DECLARE_POINTER_TO_LAST_DLL_BLOCKLIST_ENTRY(pLast);
+
+ EXPECT_FALSE(pLast->mName || pLast->mMaxVersion || pLast->mFlags);
+
+ for (size_t i = 0; i < mozilla::ArrayLength(gWindowsDllBlocklist) - 1; ++i) {
+ auto pEntry = pFirst + i;
+
+ // Validate name
+ EXPECT_TRUE(!!pEntry->mName);
+ EXPECT_GT(strlen(pEntry->mName), 3U);
+
+ // Check the filename for valid characters.
+ for (auto pch = pEntry->mName; *pch != 0; ++pch) {
+ EXPECT_FALSE(*pch >= 'A' && *pch <= 'Z');
+ }
+ }
+}
+
+TEST(TestDllBlocklist, BlockThreadWithLoadLibraryEntryPoint)
+{
+ // Only supported on Nightly
+#if defined(NIGHTLY_BUILD)
+ using ThreadProc = unsigned(__stdcall*)(void*);
+
+ constexpr auto kLeafNameW = u"TestDllBlocklist_MatchByVersion.dll"_ns;
+
+ nsString fullPathW = GetFullPath(kLeafNameW);
+ EXPECT_FALSE(fullPathW.IsEmpty());
+
+ nsAutoHandle threadW(reinterpret_cast<HANDLE>(
+ _beginthreadex(nullptr, 0, reinterpret_cast<ThreadProc>(&::LoadLibraryW),
+ (void*)fullPathW.get(), 0, nullptr)));
+
+ EXPECT_TRUE(!!threadW);
+ EXPECT_EQ(::WaitForSingleObject(threadW, INFINITE), WAIT_OBJECT_0);
+
+# if !defined(MOZ_ASAN)
+ // ASAN builds under Windows 11 can have unexpected thread exit codes.
+ // See bug 1798796
+ DWORD exitCode;
+ EXPECT_TRUE(::GetExitCodeThread(threadW, &exitCode) && !exitCode);
+# endif // !defined(MOZ_ASAN)
+ EXPECT_TRUE(!::GetModuleHandleW(kLeafNameW.get()));
+
+ const NS_LossyConvertUTF16toASCII fullPathA(fullPathW);
+ EXPECT_FALSE(fullPathA.IsEmpty());
+
+ nsAutoHandle threadA(reinterpret_cast<HANDLE>(
+ _beginthreadex(nullptr, 0, reinterpret_cast<ThreadProc>(&::LoadLibraryA),
+ (void*)fullPathA.get(), 0, nullptr)));
+
+ EXPECT_TRUE(!!threadA);
+ EXPECT_EQ(::WaitForSingleObject(threadA, INFINITE), WAIT_OBJECT_0);
+# if !defined(MOZ_ASAN)
+ // ASAN builds under Windows 11 can have unexpected thread exit codes.
+ // See bug 1798796
+ EXPECT_TRUE(::GetExitCodeThread(threadA, &exitCode) && !exitCode);
+# endif // !defined(MOZ_ASAN)
+ EXPECT_TRUE(!::GetModuleHandleW(kLeafNameW.get()));
+#endif // defined(NIGHTLY_BUILD)
+}
+
+constexpr auto kSingleNotificationDll1Loads = 4;
+constexpr auto kSingleNotificationDll2Loads = 3;
+using DllFullPathsArray =
+ std::array<nsString,
+ kSingleNotificationDll1Loads + kSingleNotificationDll2Loads>;
+
+DWORD __stdcall LoadSingleNotificationModules(LPVOID aThreadParameter) {
+ auto* dllFullPaths = reinterpret_cast<DllFullPathsArray*>(aThreadParameter);
+
+ for (const auto& dllFullPath : *dllFullPaths) {
+ nsModuleHandle hDll(::LoadLibraryW(dllFullPath.get()));
+ EXPECT_TRUE(!hDll);
+ EXPECT_TRUE(!::GetModuleHandleW(dllFullPath.get()));
+ }
+
+ return 0;
+}
+
+// The next test is only relevant if we hook LdrLoadDll, so we reflect the
+// hooking condition from browser/app/winlauncher/DllBlocklistInit.cpp.
+#if !defined(MOZ_ASAN) && !defined(_M_ARM64)
+
+// This test relies on the fact that blocked DLL loads generate a DLL load
+// notification.
+TEST(TestDllBlocklist, SingleNotification)
+{
+ // We will block-load the two DLLs multiple times, with variations on case.
+ std::array<nsLiteralString, kSingleNotificationDll1Loads> dll1Variations{
+ u"TestDllBlocklist_SingleNotification1.dll"_ns,
+ u"TestDllBlocklist_SingleNotification1.dll"_ns,
+ u"testdllblocklist_singlenotification1.dll"_ns,
+ u"TeStDlLbLoCkLiSt_SiNgLeNoTiFiCaTiOn1.DlL"_ns,
+ };
+ std::array<nsLiteralString, kSingleNotificationDll2Loads> dll2Variations{
+ u"TestDllBlocklist_SingleNotification2.dll"_ns,
+ u"testdllblocklist_singlenotification2.dll"_ns,
+ u"TESTDLLBLOCKLIST_SINGLENOTIFICATION2.dll"_ns,
+ };
+ DllFullPathsArray dllFullPaths;
+ size_t i = 0;
+ for (const auto& dllName : dll1Variations) {
+ dllFullPaths[i] = GetFullPath(dllName);
+ ++i;
+ }
+ for (const auto& dllName : dll2Variations) {
+ dllFullPaths[i] = GetFullPath(dllName);
+ ++i;
+ }
+
+ // Register our observer.
+ TestDLLLoadObserver::DLLFilter dllFilter(
+ [](const char16_t* aLoadedPath) -> bool {
+ nsDependentString loadedPath(aLoadedPath);
+ return StringEndsWith(loadedPath,
+ u"\\testdllblocklist_singlenotification1.dll"_ns,
+ nsCaseInsensitiveStringComparator) ||
+ StringEndsWith(loadedPath,
+ u"\\testdllblocklist_singlenotification2.dll"_ns,
+ nsCaseInsensitiveStringComparator);
+ });
+ RefPtr<TestDLLLoadObserver> obs(new TestDLLLoadObserver(dllFilter));
+ obs->Init();
+
+ // Load DllServices. This is required for notifications to get dispatched.
+ RefPtr<mozilla::DllServices> dllSvc(mozilla::DllServices::Get());
+ EXPECT_TRUE(dllSvc);
+
+ // Block-load the two DLLs multiple times on the main thread.
+ LoadSingleNotificationModules(reinterpret_cast<void*>(&dllFullPaths));
+
+ // Block-load the two DLLs multiple times on a different thread.
+ HANDLE thread =
+ ::CreateThread(nullptr, 0, LoadSingleNotificationModules,
+ reinterpret_cast<void*>(&dllFullPaths), 0, nullptr);
+ EXPECT_EQ(::WaitForSingleObject(thread, 5000), WAIT_OBJECT_0);
+
+ // Unregister our observer and flush the main thread loop.
+ obs->Exit();
+
+ // Check how many notifications we received
+ EXPECT_EQ(obs->MainThreadNotificationsCount(), 2);
+ EXPECT_EQ(obs->NonMainThreadNotificationsCount(),
+ kSingleNotificationDll1Loads + kSingleNotificationDll2Loads);
+}
+
+#endif // !defined(MOZ_ASAN) && !defined(_M_ARM64)