diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/xre/dllservices/tests/gtest/TestDLLBlocklist.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
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.cpp | 475 |
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) |