/* -*- 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 #include #include #include #include #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 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 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; 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 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 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( _beginthreadex(nullptr, 0, reinterpret_cast(&::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( _beginthreadex(nullptr, 0, reinterpret_cast(&::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; DWORD __stdcall LoadSingleNotificationModules(LPVOID aThreadParameter) { auto* dllFullPaths = reinterpret_cast(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 dll1Variations{ u"TestDllBlocklist_SingleNotification1.dll"_ns, u"TestDllBlocklist_SingleNotification1.dll"_ns, u"testdllblocklist_singlenotification1.dll"_ns, u"TeStDlLbLoCkLiSt_SiNgLeNoTiFiCaTiOn1.DlL"_ns, }; std::array 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 obs(new TestDLLLoadObserver(dllFilter)); obs->Init(); // Load DllServices. This is required for notifications to get dispatched. RefPtr dllSvc(mozilla::DllServices::Get()); EXPECT_TRUE(dllSvc); // Block-load the two DLLs multiple times on the main thread. LoadSingleNotificationModules(reinterpret_cast(&dllFullPaths)); // Block-load the two DLLs multiple times on a different thread. HANDLE thread = ::CreateThread(nullptr, 0, LoadSingleNotificationModules, reinterpret_cast(&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)