diff options
Diffstat (limited to 'toolkit/xre/dllservices/tests')
32 files changed, 3302 insertions, 0 deletions
diff --git a/toolkit/xre/dllservices/tests/AssemblyPayloads.h b/toolkit/xre/dllservices/tests/AssemblyPayloads.h new file mode 100644 index 0000000000..811bf88412 --- /dev/null +++ b/toolkit/xre/dllservices/tests/AssemblyPayloads.h @@ -0,0 +1,241 @@ +/* -*- 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 https://mozilla.org/MPL/2.0/. */ + +/* These assembly functions represent patterns that were already hooked by + * another application before our detour. + */ + +#ifndef mozilla_AssemblyPayloads_h +#define mozilla_AssemblyPayloads_h + +#include <cstdint> + +#define PADDING_256_NOP \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" + +extern "C" { + +#if defined(__clang__) +# if defined(_M_X64) +constexpr uintptr_t JumpDestination = 0x7fff00000000; + +__declspec(dllexport) __attribute__((naked)) void MovPushRet() { + asm volatile( + "mov %0, %%rax;" + "push %%rax;" + "ret;" + : + : "i"(JumpDestination)); +} + +__declspec(dllexport) __attribute__((naked)) void MovRaxJump() { + asm volatile( + "mov %0, %%rax;" + "jmpq *%%rax;" + : + : "i"(JumpDestination)); +} + +__declspec(dllexport) __attribute__((naked)) void DoubleJump() { + asm volatile( + "jmp label1;" + + "label2:" + "mov %0, %%rax;" + "jmpq *%%rax;" + + // 0x100 bytes padding to generate jmp rel32 instead of jmp rel8 + PADDING_256_NOP + + "label1:" + "jmp label2;" + : + : "i"(JumpDestination)); +} + +__declspec(dllexport) __attribute__((naked)) void NearJump() { + asm volatile( + "jae label3;" + "je label3;" + "jne label3;" + + "label4:" + "mov %0, %%rax;" + "jmpq *%%rax;" + + // 0x100 bytes padding to generate jae rel32 instead of jae rel8 + PADDING_256_NOP + + "label3:" + "jmp label4;" + : + : "i"(JumpDestination)); +} + +__declspec(dllexport) __attribute__((naked)) void OpcodeFF() { + // Skip PUSH (FF /6) because clang prefers Opcode 50+rd + // to translate PUSH r64 rather than Opcode FF. + asm volatile( + "incl %eax;" + "decl %ebx;" + "call *%rcx;" + "jmp *(%rip);" // Indirect jump to 0xcccccccc`cccccccc + "int $3;int $3;int $3;int $3;" + "int $3;int $3;int $3;int $3;"); +} + +__declspec(dllexport) __attribute__((naked)) void IndirectCall() { + asm volatile( + "call *(%rip);" // Indirect call to 0x90909090`90909090 + "nop;nop;nop;nop;nop;nop;nop;nop;" + "ret;"); +} + +__declspec(dllexport) __attribute__((naked)) void MovImm64() { + asm volatile( + "mov $0x1234567812345678, %r10;" + "nop;nop;nop"); +} + +# if !defined(MOZ_CODE_COVERAGE) +// This code reproduces bug 1798787: it uses the same prologue, the same unwind +// info, and it has a call instruction that starts within the 13 first bytes. +__attribute__((naked)) void DetouredCallCode(uintptr_t aCallee) { + asm volatile( + "subq $0x28, %rsp;" + "testq %rcx, %rcx;" + "jz exit;" + "callq *%rcx;" + "exit:" + "addq $0x28, %rsp;" + "retq;"); +} +constexpr uint8_t gDetouredCallCodeSize = 16; // size of function in bytes +alignas(uint32_t) uint8_t gDetouredCallUnwindInfo[] = { + 0x01, // Version (1), Flags (0) + 0x04, // SizeOfProlog (4) + 0x01, // CountOfUnwindCodes (1) + 0x00, // FrameRegister (0), FrameOffset (0) + // UnwindCodes[0] + 0x04, // .OffsetInProlog (4) + 0x42, // .UnwindOpCode(UWOP_ALLOC_SMALL=2), .UnwindInfo (4) +}; + +// This points to the same code as DetouredCallCode, but dynamically generated +// so that it can have custom unwinding info. See TestDllInterceptor.cpp. +extern decltype(&DetouredCallCode) gDetouredCall; + +// This is just a jumper: our hooking code will thus detour the jump target +// DetouredCall -- it will not detour DetouredCallJumper. We need to do this to +// point our hooking code to the dynamic code, because our hooking API needs an +// exported function name. +// +// guard(nocf) ensures that our generated code is a recognized jumper: +// jmp qword ptr [rip+offset DetouredCall] +// rather than: +// mov rax, qword ptr [rip+offset DetouredCall] +// mov rdx, qword ptr [rip+offset __guard_dispatch_icall_fptr] +// jmp rdx +__declspec(dllexport noinline guard(nocf)) void DetouredCallJumper( + uintptr_t aCallee) { + gDetouredCall(aCallee); +} +# endif // !defined(MOZ_CODE_COVERAGE) + +# elif defined(_M_IX86) +constexpr uintptr_t JumpDestination = 0x7fff0000; + +__declspec(dllexport) __attribute__((naked)) void PushRet() { + asm volatile( + "push %0;" + "ret;" + : + : "i"(JumpDestination)); +} + +__declspec(dllexport) __attribute__((naked)) void MovEaxJump() { + asm volatile( + "mov %0, %%eax;" + "jmp *%%eax;" + : + : "i"(JumpDestination)); +} + +__declspec(dllexport) __attribute__((naked)) void Opcode83() { + asm volatile( + "xor $0x42, %eax;" + "cmpl $1, 0xc(%ebp);"); +} + +__declspec(dllexport) __attribute__((naked)) void LockPrefix() { + // Test an instruction with a LOCK prefix (0xf0) at a non-zero offset + asm volatile( + "push $0x7c;" + "lock push $0x7c;"); +} + +__declspec(dllexport) __attribute__((naked)) void LooksLikeLockPrefix() { + // This is for a regression scenario of bug 1625452, where we double-counted + // the offset in CountPrefixBytes. When we count prefix bytes in front of + // the 2nd PUSH located at offset 2, we mistakenly started counting from + // the byte 0xf0 at offset 4, which is considered as LOCK, thus we try to + // detour the next byte 0xcc and it fails. + // + // 0: 6a7c push 7Ch + // 2: 68ccf00000 push 0F0CCh + // + asm volatile( + "push $0x7c;" + "push $0x0000f0cc;"); +} + +__declspec(dllexport) __attribute__((naked)) void DoubleJump() { + asm volatile( + "jmp label1;" + + "label2:" + "mov %0, %%eax;" + "jmp *%%eax;" + + // 0x100 bytes padding to generate jmp rel32 instead of jmp rel8 + PADDING_256_NOP + + "label1:" + "jmp label2;" + : + : "i"(JumpDestination)); +} +# endif + +# if !defined(_M_ARM64) +__declspec(dllexport) __attribute__((naked)) void UnsupportedOp() { + asm volatile( + "ud2;" + "nop;nop;nop;nop;nop;nop;nop;nop;" + "nop;nop;nop;nop;nop;nop;nop;nop;"); +} +# endif // !defined(_M_ARM64) + +#endif // defined(__clang__) + +} // extern "C" + +#endif // mozilla_AssemblyPayloads_h diff --git a/toolkit/xre/dllservices/tests/TestDllInterceptor.cpp b/toolkit/xre/dllservices/tests/TestDllInterceptor.cpp new file mode 100644 index 0000000000..3c58132a34 --- /dev/null +++ b/toolkit/xre/dllservices/tests/TestDllInterceptor.cpp @@ -0,0 +1,1416 @@ +/* -*- 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 <shlobj.h> +#include <stdio.h> +#include <commdlg.h> +#define SECURITY_WIN32 +#include <security.h> +#include <wininet.h> +#include <schnlsp.h> +#include <winternl.h> +#include <processthreadsapi.h> + +#include "AssemblyPayloads.h" +#include "mozilla/DynamicallyLinkedFunctionPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WindowsVersion.h" +#include "nsWindowsDllInterceptor.h" +#include "nsWindowsHelpers.h" + +#include "mozilla/MozProcessMitigationDynamicCodePolicy.h" + +NTSTATUS NTAPI NtFlushBuffersFile(HANDLE, PIO_STATUS_BLOCK); +NTSTATUS NTAPI NtReadFile(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, + PIO_STATUS_BLOCK, PVOID, ULONG, PLARGE_INTEGER, + PULONG); +NTSTATUS NTAPI NtReadFileScatter(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, + PIO_STATUS_BLOCK, PFILE_SEGMENT_ELEMENT, ULONG, + PLARGE_INTEGER, PULONG); +NTSTATUS NTAPI NtWriteFile(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, + PIO_STATUS_BLOCK, PVOID, ULONG, PLARGE_INTEGER, + PULONG); +NTSTATUS NTAPI NtWriteFileGather(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, + PIO_STATUS_BLOCK, PFILE_SEGMENT_ELEMENT, ULONG, + PLARGE_INTEGER, PULONG); +NTSTATUS NTAPI NtQueryFullAttributesFile(POBJECT_ATTRIBUTES, PVOID); +NTSTATUS NTAPI LdrLoadDll(PWCHAR filePath, PULONG flags, + PUNICODE_STRING moduleFileName, PHANDLE handle); +NTSTATUS NTAPI LdrUnloadDll(HMODULE); + +NTSTATUS NTAPI NtMapViewOfSection( + HANDLE aSection, HANDLE aProcess, PVOID* aBaseAddress, ULONG_PTR aZeroBits, + SIZE_T aCommitSize, PLARGE_INTEGER aSectionOffset, PSIZE_T aViewSize, + SECTION_INHERIT aInheritDisposition, ULONG aAllocationType, + ULONG aProtectionFlags); + +// These pointers are disguised as PVOID to avoid pulling in obscure headers +PVOID NTAPI LdrResolveDelayLoadedAPI(PVOID, PVOID, PVOID, PVOID, PVOID, ULONG); +void CALLBACK ProcessCaretEvents(HWINEVENTHOOK, DWORD, HWND, LONG, LONG, DWORD, + DWORD); +void __fastcall BaseThreadInitThunk(BOOL aIsInitialThread, void* aStartAddress, + void* aThreadParam); + +BOOL WINAPI ApiSetQueryApiSetPresence(PCUNICODE_STRING, PBOOLEAN); + +#if (_WIN32_WINNT < 0x0602) +BOOL WINAPI +SetProcessMitigationPolicy(PROCESS_MITIGATION_POLICY aMitigationPolicy, + PVOID aBuffer, SIZE_T aBufferLen); +#endif // (_WIN32_WINNT < 0x0602) + +using namespace mozilla; + +struct payload { + UINT64 a; + UINT64 b; + UINT64 c; + + bool operator==(const payload& other) const { + return (a == other.a && b == other.b && c == other.c); + } +}; + +extern "C" __declspec(dllexport) __declspec(noinline) payload + rotatePayload(payload p) { + UINT64 tmp = p.a; + p.a = p.b; + p.b = p.c; + p.c = tmp; + return p; +} + +// payloadNotHooked is a target function for a test to expect a negative result. +// We cannot use rotatePayload for that purpose because our detour cannot hook +// a function detoured already. Please keep this function always unhooked. +extern "C" __declspec(dllexport) __declspec(noinline) payload + payloadNotHooked(payload p) { + // Do something different from rotatePayload to avoid ICF. + p.a ^= p.b; + p.b ^= p.c; + p.c ^= p.a; + return p; +} + +// Declared as volatile to prevent optimizers from incorrectly eliding accesses +// to it. (See bug 1769001 for a motivating example.) +static volatile bool patched_func_called = false; + +static WindowsDllInterceptor::FuncHookType<decltype(&rotatePayload)> + orig_rotatePayload; + +static WindowsDllInterceptor::FuncHookType<decltype(&payloadNotHooked)> + orig_payloadNotHooked; + +static payload patched_rotatePayload(payload p) { + patched_func_called = true; + return orig_rotatePayload(p); +} + +// Invoke aFunc by taking aArg's contents and using them as aFunc's arguments +template <typename OrigFuncT, typename... Args, + typename ArgTuple = Tuple<Args...>, size_t... Indices> +decltype(auto) Apply(OrigFuncT& aFunc, ArgTuple&& aArgs, + std::index_sequence<Indices...>) { + return aFunc(Get<Indices>(std::forward<ArgTuple>(aArgs))...); +} + +#define DEFINE_TEST_FUNCTION(calling_convention) \ + template <typename R, typename... Args, typename... TestArgs> \ + bool TestFunction(R(calling_convention* aFunc)(Args...), bool (*aPred)(R), \ + TestArgs&&... aArgs) { \ + using ArgTuple = Tuple<Args...>; \ + using Indices = std::index_sequence_for<Args...>; \ + ArgTuple fakeArgs{std::forward<TestArgs>(aArgs)...}; \ + patched_func_called = false; \ + return aPred(Apply(aFunc, std::forward<ArgTuple>(fakeArgs), Indices())) && \ + patched_func_called; \ + } \ + \ + /* Specialization for functions returning void */ \ + template <typename PredT, typename... Args, typename... TestArgs> \ + bool TestFunction(void(calling_convention * aFunc)(Args...), PredT, \ + TestArgs&&... aArgs) { \ + using ArgTuple = Tuple<Args...>; \ + using Indices = std::index_sequence_for<Args...>; \ + ArgTuple fakeArgs{std::forward<TestArgs>(aArgs)...}; \ + patched_func_called = false; \ + Apply(aFunc, std::forward<ArgTuple>(fakeArgs), Indices()); \ + return patched_func_called; \ + } + +// C++11 allows empty arguments to macros. clang works just fine. MSVC does the +// right thing, but it also throws up warning C4003. +#if defined(_MSC_VER) && !defined(__clang__) +DEFINE_TEST_FUNCTION(__cdecl) +#else +DEFINE_TEST_FUNCTION() +#endif + +#ifdef _M_IX86 +DEFINE_TEST_FUNCTION(__stdcall) +DEFINE_TEST_FUNCTION(__fastcall) +#endif // _M_IX86 + +// Test the hooked function against the supplied predicate +template <typename OrigFuncT, typename PredicateT, typename... Args> +bool CheckHook(OrigFuncT& aOrigFunc, const char* aDllName, + const char* aFuncName, PredicateT&& aPred, Args&&... aArgs) { + if (TestFunction(aOrigFunc, std::forward<PredicateT>(aPred), + std::forward<Args>(aArgs)...)) { + printf( + "TEST-PASS | WindowsDllInterceptor | " + "Executed hooked function %s from %s\n", + aFuncName, aDllName); + fflush(stdout); + return true; + } + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Failed to execute hooked function %s from %s\n", + aFuncName, aDllName); + return false; +} + +struct InterceptorFunction { + static const size_t EXEC_MEMBLOCK_SIZE = 64 * 1024; // 64K + + static InterceptorFunction& Create() { + // Make sure the executable memory is allocated + if (!sBlock) { + Init(); + } + MOZ_ASSERT(sBlock); + + // Make sure we aren't making more functions than we allocated room for + MOZ_RELEASE_ASSERT((sNumInstances + 1) * sizeof(InterceptorFunction) <= + EXEC_MEMBLOCK_SIZE); + + // Grab the next InterceptorFunction from executable memory + InterceptorFunction& ret = *reinterpret_cast<InterceptorFunction*>( + sBlock + (sNumInstances++ * sizeof(InterceptorFunction))); + + // Set the InterceptorFunction to the code template. + auto funcCode = &ret[0]; + memcpy(funcCode, sInterceptorTemplate, TemplateLength); + + // Fill in the patched_func_called pointer in the template. + auto pfPtr = + reinterpret_cast<volatile bool**>(&ret[PatchedFuncCalledIndex]); + *pfPtr = &patched_func_called; + return ret; + } + + uint8_t& operator[](size_t i) { return mFuncCode[i]; } + + uint8_t* GetFunction() { return mFuncCode; } + + void SetStub(uintptr_t aStub) { + auto pfPtr = reinterpret_cast<uintptr_t*>(&mFuncCode[StubFuncIndex]); + *pfPtr = aStub; + } + + private: + // We intercept functions with short machine-code functions that set a boolean + // and run the stub that launches the original function. Each entry in the + // array is the code for one of those interceptor functions. We cannot + // free this memory until the test shuts down. + // The templates have spots for the address of patched_func_called + // and for the address of the stub function. Their indices in the byte + // array are given as constants below and they appear as blocks of + // 0xff bytes in the templates. +#if defined(_M_X64) + // 0: 48 b8 ff ff ff ff ff ff ff ff movabs rax, &patched_func_called + // a: c6 00 01 mov BYTE PTR [rax],0x1 + // d: 48 b8 ff ff ff ff ff ff ff ff movabs rax, &stub_func_ptr + // 17: ff e0 jmp rax + static constexpr uint8_t sInterceptorTemplate[] = { + 0x48, 0xB8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xC6, 0x00, 0x01, 0x48, 0xB8, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0}; + static const size_t PatchedFuncCalledIndex = 0x2; + static const size_t StubFuncIndex = 0xf; +#elif defined(_M_IX86) + // 0: c6 05 ff ff ff ff 01 mov BYTE PTR &patched_func_called, 0x1 + // 7: 68 ff ff ff ff push &stub_func_ptr + // c: c3 ret + static constexpr uint8_t sInterceptorTemplate[] = { + 0xC6, 0x05, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, + 0x68, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3}; + static const size_t PatchedFuncCalledIndex = 0x2; + static const size_t StubFuncIndex = 0x8; +#elif defined(_M_ARM64) + // 0: 31 00 80 52 movz w17, #0x1 + // 4: 90 00 00 58 ldr x16, #16 + // 8: 11 02 00 39 strb w17, [x16] + // c: 90 00 00 58 ldr x16, #16 + // 10: 00 02 1F D6 br x16 + // 14: &patched_func_called + // 1c: &stub_func_ptr + static constexpr uint8_t sInterceptorTemplate[] = { + 0x31, 0x00, 0x80, 0x52, 0x90, 0x00, 0x00, 0x58, 0x11, 0x02, 0x00, 0x39, + 0x90, 0x00, 0x00, 0x58, 0x00, 0x02, 0x1F, 0xD6, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + static const size_t PatchedFuncCalledIndex = 0x14; + static const size_t StubFuncIndex = 0x1c; +#else +# error "Missing template for architecture" +#endif + + static const size_t TemplateLength = sizeof(sInterceptorTemplate); + uint8_t mFuncCode[TemplateLength]; + + InterceptorFunction() = delete; + InterceptorFunction(const InterceptorFunction&) = delete; + InterceptorFunction& operator=(const InterceptorFunction&) = delete; + + static void Init() { + MOZ_ASSERT(!sBlock); + sBlock = reinterpret_cast<uint8_t*>( + ::VirtualAlloc(nullptr, EXEC_MEMBLOCK_SIZE, MEM_RESERVE | MEM_COMMIT, + PAGE_EXECUTE_READWRITE)); + } + + static uint8_t* sBlock; + static size_t sNumInstances; +}; + +uint8_t* InterceptorFunction::sBlock = nullptr; +size_t InterceptorFunction::sNumInstances = 0; + +constexpr uint8_t InterceptorFunction::sInterceptorTemplate[]; + +// Hook the function and optionally attempt calling it +template <typename OrigFuncT, size_t N, typename PredicateT, typename... Args> +bool TestHook(const char (&dll)[N], const char* func, PredicateT&& aPred, + Args&&... aArgs) { + auto orig_func( + mozilla::MakeUnique<WindowsDllInterceptor::FuncHookType<OrigFuncT>>()); + wchar_t dllW[N]; + std::copy(std::begin(dll), std::end(dll), std::begin(dllW)); + + bool successful = false; + WindowsDllInterceptor TestIntercept; + TestIntercept.Init(dll); + + InterceptorFunction& interceptorFunc = InterceptorFunction::Create(); + successful = orig_func->Set( + TestIntercept, func, + reinterpret_cast<OrigFuncT>(interceptorFunc.GetFunction())); + + if (successful) { + auto stub = reinterpret_cast<uintptr_t>(orig_func->GetStub()); + interceptorFunc.SetStub(stub); + printf("TEST-PASS | WindowsDllInterceptor | Could hook %s from %s\n", func, + dll); + fflush(stdout); + + // Test the DLL function we just hooked. + HMODULE module = ::LoadLibraryW(dllW); + FARPROC funcAddr = ::GetProcAddress(module, func); + if (!funcAddr) { + return false; + } + +// Check that unwind information has been added if and only if it was present +// for the original function. +#ifdef _M_X64 + + auto funcBytes = reinterpret_cast<uint8_t*>(funcAddr); + + // If the function we are hooking is a jumper, we need to lookup the + // destination of the jump to find the unwind information. + auto realFuncAddr = reinterpret_cast<uintptr_t>(funcAddr); + // jmp qword ptr[rip+offset] + if (funcBytes[0] == 0xff && funcBytes[1] == 0x25) { + realFuncAddr = *reinterpret_cast<uintptr_t*>( + realFuncAddr + 6 + *reinterpret_cast<int32_t*>(realFuncAddr + 2)); + } + // rex.jmp qword ptr[rip+offset] + else if (funcBytes[0] == 0x48 && funcBytes[1] == 0xff && + funcBytes[2] == 0x25) { + realFuncAddr = *reinterpret_cast<uintptr_t*>( + realFuncAddr + 7 + *reinterpret_cast<int32_t*>(realFuncAddr + 3)); + } + + uintptr_t funcImageBase = 0; + auto funcEntry = + RtlLookupFunctionEntry(realFuncAddr, &funcImageBase, nullptr); + bool funcHasUnwindInfo = bool(funcEntry); + + uintptr_t stubImageBase = 0; + auto stubEntry = RtlLookupFunctionEntry(stub, &stubImageBase, nullptr); + bool stubHasUnwindInfo = bool(stubEntry); + + if (funcHasUnwindInfo == stubHasUnwindInfo) { + printf( + "TEST-PASS | WindowsDllInterceptor | The hook for %s from %s and " + "the original function are coherent with respect to unwind info: " + "funcHasUnwindInfo (%d) == stubHasUnwindInfo (%d).\n", + func, dll, funcHasUnwindInfo, stubHasUnwindInfo); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Hook for %s from %s " + "and the original function are not coherent with respect to unwind " + "info: " + "funcHasUnwindInfo (%d) != stubHasUnwindInfo (%d).\n", + func, dll, funcHasUnwindInfo, stubHasUnwindInfo); + fflush(stdout); + return false; + } + + if (stubHasUnwindInfo) { + if (stub == (stubImageBase + stubEntry->BeginAddress)) { + printf( + "TEST-PASS | WindowsDllInterceptor | The hook for %s from %s has " + "coherent unwind info.\n", + func, dll); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | The hook for %s " + " from %s has incoherent unwind info.\n", + func, dll); + fflush(stdout); + return false; + } + } + +#endif // _M_X64 + + if (!aPred) { + printf( + "TEST-SKIPPED | WindowsDllInterceptor | " + "Will not attempt to execute patched %s.\n", + func); + fflush(stdout); + return true; + } + + return CheckHook(reinterpret_cast<OrigFuncT&>(funcAddr), dll, func, + std::forward<PredicateT>(aPred), + std::forward<Args>(aArgs)...); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Failed to hook %s from " + "%s\n", + func, dll); + fflush(stdout); + + // Print out the function's bytes so that we can easily analyze the error. + nsModuleHandle mod(::LoadLibraryW(dllW)); + FARPROC funcAddr = ::GetProcAddress(mod, func); + if (funcAddr) { + const uint32_t kNumBytesToDump = + WindowsDllInterceptor::GetWorstCaseRequiredBytesToPatch(); + + printf("\tFirst %u bytes of function:\n\t", kNumBytesToDump); + + auto code = reinterpret_cast<const uint8_t*>(funcAddr); + for (uint32_t i = 0; i < kNumBytesToDump; ++i) { + char suffix = (i < (kNumBytesToDump - 1)) ? ' ' : '\n'; + printf("%02hhX%c", code[i], suffix); + } + + fflush(stdout); + } + return false; + } +} + +// Detour the function and optionally attempt calling it +template <typename OrigFuncT, size_t N, typename PredicateT> +bool TestDetour(const char (&dll)[N], const char* func, PredicateT&& aPred) { + auto orig_func( + mozilla::MakeUnique<WindowsDllInterceptor::FuncHookType<OrigFuncT>>()); + wchar_t dllW[N]; + std::copy(std::begin(dll), std::end(dll), std::begin(dllW)); + + bool successful = false; + WindowsDllInterceptor TestIntercept; + TestIntercept.Init(dll); + + InterceptorFunction& interceptorFunc = InterceptorFunction::Create(); + successful = orig_func->Set( + TestIntercept, func, + reinterpret_cast<OrigFuncT>(interceptorFunc.GetFunction())); + + if (successful) { + interceptorFunc.SetStub(reinterpret_cast<uintptr_t>(orig_func->GetStub())); + printf("TEST-PASS | WindowsDllInterceptor | Could detour %s from %s\n", + func, dll); + fflush(stdout); + if (!aPred) { + printf( + "TEST-SKIPPED | WindowsDllInterceptor | " + "Will not attempt to execute patched %s.\n", + func); + fflush(stdout); + return true; + } + + // Test the DLL function we just hooked. + HMODULE module = ::LoadLibraryW(dllW); + FARPROC funcAddr = ::GetProcAddress(module, func); + if (!funcAddr) { + return false; + } + + return CheckHook(reinterpret_cast<OrigFuncT&>(funcAddr), dll, func, + std::forward<PredicateT>(aPred)); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Failed to detour %s " + "from %s\n", + func, dll); + fflush(stdout); + return false; + } +} + +// If a function pointer's type returns void*, this template converts that type +// to return uintptr_t instead, for the purposes of predicates. +template <typename FuncT> +struct SubstituteForVoidPtr { + using Type = FuncT; +}; + +template <typename... Args> +struct SubstituteForVoidPtr<void* (*)(Args...)> { + using Type = uintptr_t (*)(Args...); +}; + +#ifdef _M_IX86 +template <typename... Args> +struct SubstituteForVoidPtr<void*(__stdcall*)(Args...)> { + using Type = uintptr_t(__stdcall*)(Args...); +}; + +template <typename... Args> +struct SubstituteForVoidPtr<void*(__fastcall*)(Args...)> { + using Type = uintptr_t(__fastcall*)(Args...); +}; +#endif // _M_IX86 + +// Determines the function's return type +template <typename FuncT> +struct ReturnType; + +template <typename R, typename... Args> +struct ReturnType<R (*)(Args...)> { + using Type = R; +}; + +#ifdef _M_IX86 +template <typename R, typename... Args> +struct ReturnType<R(__stdcall*)(Args...)> { + using Type = R; +}; + +template <typename R, typename... Args> +struct ReturnType<R(__fastcall*)(Args...)> { + using Type = R; +}; +#endif // _M_IX86 + +// Predicates that may be supplied during tests +template <typename FuncT> +struct Predicates { + using ArgType = typename ReturnType<FuncT>::Type; + + template <ArgType CompVal> + static bool Equals(ArgType aValue) { + return CompVal == aValue; + } + + template <ArgType CompVal> + static bool NotEquals(ArgType aValue) { + return CompVal != aValue; + } + + template <ArgType CompVal> + static bool Ignore(ArgType aValue) { + return true; + } +}; + +// Functions that return void should be ignored, so we specialize the +// Ignore predicate for that case. Use nullptr as the value to compare against. +template <typename... Args> +struct Predicates<void (*)(Args...)> { + template <nullptr_t DummyVal> + static bool Ignore() { + return true; + } +}; + +#ifdef _M_IX86 +template <typename... Args> +struct Predicates<void(__stdcall*)(Args...)> { + template <nullptr_t DummyVal> + static bool Ignore() { + return true; + } +}; + +template <typename... Args> +struct Predicates<void(__fastcall*)(Args...)> { + template <nullptr_t DummyVal> + static bool Ignore() { + return true; + } +}; +#endif // _M_IX86 + +// The standard test. Hook |func|, and then try executing it with all zero +// arguments, using |pred| and |comp| to determine whether the call successfully +// executed. In general, you want set pred and comp such that they return true +// when the function is returning whatever value is expected with all-zero +// arguments. +// +// Note: When |func| returns void, you must supply |Ignore| and |nullptr| as the +// |pred| and |comp| arguments, respectively. +#define TEST_HOOK(dll, func, pred, comp) \ + TestHook<decltype(&func)>(dll, #func, \ + &Predicates<decltype(&func)>::pred<comp>) + +// We need to special-case functions that return INVALID_HANDLE_VALUE +// (ie, CreateFile). Our template machinery for comparing values doesn't work +// with integer constants passed as pointers (well, it works on MSVC, but not +// clang, because that is not standard-compliant). +#define TEST_HOOK_FOR_INVALID_HANDLE_VALUE(dll, func) \ + TestHook<SubstituteForVoidPtr<decltype(&func)>::Type>( \ + dll, #func, \ + &Predicates<SubstituteForVoidPtr<decltype(&func)>::Type>::Equals< \ + uintptr_t(-1)>) + +// This variant allows you to explicitly supply arguments to the hooked function +// during testing. You want to provide arguments that produce the conditions +// that induce the function to return a value that is accepted by your +// predicate. +#define TEST_HOOK_PARAMS(dll, func, pred, comp, ...) \ + TestHook<decltype(&func)>( \ + dll, #func, &Predicates<decltype(&func)>::pred<comp>, __VA_ARGS__) + +// This is for cases when we want to hook |func|, but it is unsafe to attempt +// to execute the function in the context of a test. +#define TEST_HOOK_SKIP_EXEC(dll, func) \ + TestHook<decltype(&func)>( \ + dll, #func, \ + reinterpret_cast<bool (*)(typename ReturnType<decltype(&func)>::Type)>( \ + NULL)) + +// The following three variants are identical to the previous macros, +// however the forcibly use a Detour on 32-bit Windows. On 64-bit Windows, +// these macros are identical to their TEST_HOOK variants. +#define TEST_DETOUR(dll, func, pred, comp) \ + TestDetour<decltype(&func)>(dll, #func, \ + &Predicates<decltype(&func)>::pred<comp>) + +#define TEST_DETOUR_PARAMS(dll, func, pred, comp, ...) \ + TestDetour<decltype(&func)>( \ + dll, #func, &Predicates<decltype(&func)>::pred<comp>, __VA_ARGS__) + +#define TEST_DETOUR_SKIP_EXEC(dll, func) \ + TestDetour<decltype(&func)>( \ + dll, #func, \ + reinterpret_cast<bool (*)(typename ReturnType<decltype(&func)>::Type)>( \ + NULL)) + +template <typename OrigFuncT, size_t N, typename PredicateT, typename... Args> +bool MaybeTestHook(const bool cond, const char (&dll)[N], const char* func, + PredicateT&& aPred, Args&&... aArgs) { + if (!cond) { + printf( + "TEST-SKIPPED | WindowsDllInterceptor | Skipped hook test for %s from " + "%s\n", + func, dll); + fflush(stdout); + return true; + } + + return TestHook<OrigFuncT>(dll, func, std::forward<PredicateT>(aPred), + std::forward<Args>(aArgs)...); +} + +// Like TEST_HOOK, but the test is only executed when cond is true. +#define MAYBE_TEST_HOOK(cond, dll, func, pred, comp) \ + MaybeTestHook<decltype(&func)>(cond, dll, #func, \ + &Predicates<decltype(&func)>::pred<comp>) + +#define MAYBE_TEST_HOOK_PARAMS(cond, dll, func, pred, comp, ...) \ + MaybeTestHook<decltype(&func)>( \ + cond, dll, #func, &Predicates<decltype(&func)>::pred<comp>, __VA_ARGS__) + +#define MAYBE_TEST_HOOK_SKIP_EXEC(cond, dll, func) \ + MaybeTestHook<decltype(&func)>( \ + cond, dll, #func, \ + reinterpret_cast<bool (*)(typename ReturnType<decltype(&func)>::Type)>( \ + NULL)) + +bool ShouldTestTipTsf() { + if (!IsWin8OrLater()) { + return false; + } + + mozilla::DynamicallyLinkedFunctionPtr<decltype(&SHGetKnownFolderPath)> + pSHGetKnownFolderPath(L"shell32.dll", "SHGetKnownFolderPath"); + if (!pSHGetKnownFolderPath) { + return false; + } + + PWSTR commonFilesPath = nullptr; + if (FAILED(pSHGetKnownFolderPath(FOLDERID_ProgramFilesCommon, 0, nullptr, + &commonFilesPath))) { + return false; + } + + wchar_t fullPath[MAX_PATH + 1] = {}; + wcscpy(fullPath, commonFilesPath); + wcscat(fullPath, L"\\Microsoft Shared\\Ink\\tiptsf.dll"); + CoTaskMemFree(commonFilesPath); + + if (!LoadLibraryW(fullPath)) { + return false; + } + + // Leak the module so that it's loaded for the interceptor test + return true; +} + +static const wchar_t gEmptyUnicodeStringLiteral[] = L""; +static UNICODE_STRING gEmptyUnicodeString; +static BOOLEAN gIsPresent; + +bool HasApiSetQueryApiSetPresence() { + mozilla::DynamicallyLinkedFunctionPtr<decltype(&ApiSetQueryApiSetPresence)> + func(L"Api-ms-win-core-apiquery-l1-1-0.dll", "ApiSetQueryApiSetPresence"); + if (!func) { + return false; + } + + // Prepare gEmptyUnicodeString for the test + ::RtlInitUnicodeString(&gEmptyUnicodeString, gEmptyUnicodeStringLiteral); + + return true; +} + +// Set this to true to test function unhooking (currently broken). +const bool ShouldTestUnhookFunction = false; + +#if defined(_M_X64) || defined(_M_ARM64) + +// Use VMSharingPolicyUnique for the ShortInterceptor, as it needs to +// reserve its trampoline memory in a special location. +using ShortInterceptor = mozilla::interceptor::WindowsDllInterceptor< + mozilla::interceptor::VMSharingPolicyUnique< + mozilla::interceptor::MMPolicyInProcess>>; + +static ShortInterceptor::FuncHookType<decltype(&::NtMapViewOfSection)> + orig_NtMapViewOfSection; + +#endif // defined(_M_X64) || defined(_M_ARM64) + +bool TestShortDetour() { +#if defined(_M_X64) || defined(_M_ARM64) + auto pNtMapViewOfSection = reinterpret_cast<decltype(&::NtMapViewOfSection)>( + ::GetProcAddress(::GetModuleHandleW(L"ntdll.dll"), "NtMapViewOfSection")); + if (!pNtMapViewOfSection) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Failed to resolve ntdll!NtMapViewOfSection\n"); + fflush(stdout); + return false; + } + + { // Scope for shortInterceptor + ShortInterceptor shortInterceptor; + shortInterceptor.TestOnlyDetourInit( + L"ntdll.dll", + mozilla::interceptor::DetourFlags::eTestOnlyForceShortPatch); + + InterceptorFunction& interceptorFunc = InterceptorFunction::Create(); + if (!orig_NtMapViewOfSection.SetDetour( + shortInterceptor, "NtMapViewOfSection", + reinterpret_cast<decltype(&::NtMapViewOfSection)>( + interceptorFunc.GetFunction()))) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Failed to hook ntdll!NtMapViewOfSection via 10-byte patch\n"); + fflush(stdout); + return false; + } + + interceptorFunc.SetStub( + reinterpret_cast<uintptr_t>(orig_NtMapViewOfSection.GetStub())); + + auto pred = + &Predicates<decltype(&::NtMapViewOfSection)>::Ignore<((NTSTATUS)0)>; + + if (!CheckHook(pNtMapViewOfSection, "ntdll.dll", "NtMapViewOfSection", + pred)) { + // CheckHook has already printed the error message for us + return false; + } + } + + // Now ensure that our hook cleanup worked + if (ShouldTestUnhookFunction) { + NTSTATUS status = + pNtMapViewOfSection(nullptr, nullptr, nullptr, 0, 0, nullptr, nullptr, + ((SECTION_INHERIT)0), 0, 0); + if (NT_SUCCESS(status)) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Unexpected successful call to ntdll!NtMapViewOfSection after " + "removing short-patched hook\n"); + fflush(stdout); + return false; + } + + printf( + "TEST-PASS | WindowsDllInterceptor | " + "Successfully unhooked ntdll!NtMapViewOfSection via short patch\n"); + fflush(stdout); + } + + return true; +#else + return true; +#endif +} + +constexpr uintptr_t NoStubAddressCheck = 0; +constexpr uintptr_t ExpectedFail = 1; +struct TestCase { + const char* mFunctionName; + uintptr_t mExpectedStub; + bool mPatchedOnce; + explicit TestCase(const char* aFunctionName, uintptr_t aExpectedStub) + : mFunctionName(aFunctionName), + mExpectedStub(aExpectedStub), + mPatchedOnce(false) {} +} g_AssemblyTestCases[] = { +#if defined(__clang__) +// We disable these testcases because the code coverage instrumentation injects +// code in a way that WindowsDllInterceptor doesn't understand. +# ifndef MOZ_CODE_COVERAGE +# if defined(_M_X64) + // Since we have PatchIfTargetIsRecognizedTrampoline for x64, we expect the + // original jump destination is returned as a stub. + TestCase("MovPushRet", JumpDestination), + TestCase("MovRaxJump", JumpDestination), + TestCase("DoubleJump", JumpDestination), + + // Passing NoStubAddressCheck as the following testcases return + // a trampoline address instead of the original destination. + TestCase("NearJump", NoStubAddressCheck), + TestCase("OpcodeFF", NoStubAddressCheck), + TestCase("IndirectCall", NoStubAddressCheck), + TestCase("MovImm64", NoStubAddressCheck), +# elif defined(_M_IX86) + // Skip the stub address check as we always generate a trampoline for x86. + TestCase("PushRet", NoStubAddressCheck), + TestCase("MovEaxJump", NoStubAddressCheck), + TestCase("DoubleJump", NoStubAddressCheck), + TestCase("Opcode83", NoStubAddressCheck), + TestCase("LockPrefix", NoStubAddressCheck), + TestCase("LooksLikeLockPrefix", NoStubAddressCheck), +# endif +# if !defined(DEBUG) + // Skip on Debug build because it hits MOZ_ASSERT_UNREACHABLE. + TestCase("UnsupportedOp", ExpectedFail), +# endif // !defined(DEBUG) +# endif // MOZ_CODE_COVERAGE +#endif // defined(__clang__) +}; + +template <typename InterceptorType> +bool TestAssemblyFunctions() { + static const auto patchedFunction = []() { patched_func_called = true; }; + + InterceptorType interceptor; + interceptor.Init("TestDllInterceptor.exe"); + + for (auto& testCase : g_AssemblyTestCases) { + if (testCase.mExpectedStub == NoStubAddressCheck && testCase.mPatchedOnce) { + // For the testcases with NoStubAddressCheck, we revert a hook by + // jumping into the original stub, which is not detourable again. + continue; + } + + typename InterceptorType::template FuncHookType<void (*)()> hook; + bool result = + hook.Set(interceptor, testCase.mFunctionName, patchedFunction); + if (testCase.mExpectedStub == ExpectedFail) { + if (result) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Unexpectedly succeeded to detour %s.\n", + testCase.mFunctionName); + return false; + } +#if defined(NIGHTLY_BUILD) + const Maybe<DetourError>& maybeError = interceptor.GetLastDetourError(); + if (maybeError.isNothing()) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "DetourError was not set on detour error.\n"); + return false; + } + if (maybeError.ref().mErrorCode != + DetourResultCode::DETOUR_PATCHER_CREATE_TRAMPOLINE_ERROR) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "A wrong detour errorcode was set on detour error.\n"); + return false; + } +#endif // defined(NIGHTLY_BUILD) + printf("TEST-PASS | WindowsDllInterceptor | %s\n", + testCase.mFunctionName); + continue; + } + + if (!result) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Failed to detour %s.\n", + testCase.mFunctionName); + return false; + } + + testCase.mPatchedOnce = true; + + const auto actualStub = reinterpret_cast<uintptr_t>(hook.GetStub()); + if (testCase.mExpectedStub != NoStubAddressCheck && + actualStub != testCase.mExpectedStub) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Wrong stub was backed up for %s: %zx\n", + testCase.mFunctionName, actualStub); + return false; + } + + patched_func_called = false; + + auto originalFunction = reinterpret_cast<void (*)()>( + GetProcAddress(GetModuleHandleW(nullptr), testCase.mFunctionName)); + originalFunction(); + + if (!patched_func_called) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Hook from %s was not called\n", + testCase.mFunctionName); + return false; + } + + printf("TEST-PASS | WindowsDllInterceptor | %s\n", testCase.mFunctionName); + } + + return true; +} + +#if defined(_M_X64) && !defined(MOZ_CODE_COVERAGE) +// We want to test hooking and unhooking with unwind information, so we need: +// - a VMSharingPolicy such that ShouldUnhookUponDestruction() is true and +// Items() is implemented; +// - a MMPolicy such that ShouldUnhookUponDestruction() is true and +// kSupportsUnwindInfo is true. +using DetouredCallInterceptor = mozilla::interceptor::WindowsDllInterceptor< + mozilla::interceptor::VMSharingPolicyUnique< + mozilla::interceptor::MMPolicyInProcess>>; + +struct DetouredCallChunk { + alignas(uint32_t) RUNTIME_FUNCTION functionTable[1]; + alignas(uint32_t) uint8_t unwindInfo[sizeof(gDetouredCallUnwindInfo)]; + uint8_t code[gDetouredCallCodeSize]; +}; + +// Unfortunately using RtlAddFunctionTable for static code that lives within +// a module doesn't seem to work. Presumably it conflicts with the static +// function tables. So we recreate gDetouredCall as dynamic code to be able to +// associate it with unwind information. +decltype(&DetouredCallCode) gDetouredCall = + []() -> decltype(&DetouredCallCode) { + auto detouredCallChunk = reinterpret_cast<DetouredCallChunk*>( + VirtualAlloc(nullptr, sizeof(DetouredCallChunk), MEM_RESERVE | MEM_COMMIT, + PAGE_READWRITE)); + if (!detouredCallChunk) { + return nullptr; + } + + detouredCallChunk->functionTable[0].BeginAddress = + offsetof(DetouredCallChunk, code); + detouredCallChunk->functionTable[0].EndAddress = + offsetof(DetouredCallChunk, code) + gDetouredCallCodeSize; + detouredCallChunk->functionTable[0].UnwindData = + offsetof(DetouredCallChunk, unwindInfo); + memcpy(reinterpret_cast<void*>(&detouredCallChunk->unwindInfo), + reinterpret_cast<void*>(gDetouredCallUnwindInfo), + sizeof(detouredCallChunk->unwindInfo)); + memcpy(reinterpret_cast<void*>(&detouredCallChunk->code[0]), + reinterpret_cast<void*>(DetouredCallCode), + sizeof(detouredCallChunk->code)); + + DWORD oldProtect = 0; + if (!VirtualProtect(reinterpret_cast<void*>(detouredCallChunk), + sizeof(DetouredCallChunk), PAGE_EXECUTE_READ, + &oldProtect)) { + VirtualFree(detouredCallChunk, 0, MEM_RELEASE); + return nullptr; + } + + if (!RtlAddFunctionTable(detouredCallChunk->functionTable, 1, + reinterpret_cast<uintptr_t>(detouredCallChunk))) { + VirtualFree(detouredCallChunk, 0, MEM_RELEASE); + return nullptr; + } + + return reinterpret_cast<decltype(&DetouredCallCode)>(detouredCallChunk->code); +}(); + +// We use our own variable instead of patched_func_called because the callee of +// gDetouredCall could end up calling other, already hooked functions could +// change patched_func_called. +static volatile bool sCalledPatchedDetouredCall = false; +static volatile bool sCalledDetouredCallCallee = false; +static volatile bool sCouldUnwindFromDetouredCallCallee = false; +void DetouredCallCallee() { + sCalledDetouredCallCallee = true; + + // Check that we can fully unwind the stack + CONTEXT contextRecord{}; + RtlCaptureContext(&contextRecord); + if (!contextRecord.Rip) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "DetouredCallCallee was unable to get an initial context to work " + "with\n"); + fflush(stdout); + return; + } + while (contextRecord.Rip) { + DWORD64 imageBase = 0; + auto FunctionEntry = + RtlLookupFunctionEntry(contextRecord.Rip, &imageBase, nullptr); + if (!FunctionEntry) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "DetouredCallCallee was unable to get unwind info for ControlPc=%p\n", + reinterpret_cast<void*>(contextRecord.Rip)); + fflush(stdout); + return; + } + printf( + "TEST-PASS | WindowsDllInterceptor | " + "DetouredCallCallee was able to get unwind info for ControlPc=%p\n", + reinterpret_cast<void*>(contextRecord.Rip)); + fflush(stdout); + void* handlerData = nullptr; + DWORD64 establisherFrame = 0; + RtlVirtualUnwind(UNW_FLAG_NHANDLER, imageBase, contextRecord.Rip, + FunctionEntry, &contextRecord, &handlerData, + &establisherFrame, nullptr); + } + sCouldUnwindFromDetouredCallCallee = true; +} + +static DetouredCallInterceptor::FuncHookType<decltype(&DetouredCallCode)> + orig_DetouredCall; + +static void patched_DetouredCall(uintptr_t aCallee) { + sCalledPatchedDetouredCall = true; + return orig_DetouredCall(aCallee); +} + +bool TestCallingDetouredCall(const char* aTestDescription, + bool aExpectCalledPatchedDetouredCall) { + sCalledPatchedDetouredCall = false; + sCalledDetouredCallCallee = false; + sCouldUnwindFromDetouredCallCallee = false; + DetouredCallJumper(reinterpret_cast<uintptr_t>(DetouredCallCallee)); + + if (aExpectCalledPatchedDetouredCall != sCalledPatchedDetouredCall) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "%s: expectCalledPatchedDetouredCall (%d) differs from " + "sCalledPatchedDetouredCall (%d)\n", + aTestDescription, aExpectCalledPatchedDetouredCall, + sCalledPatchedDetouredCall); + fflush(stdout); + return false; + } + + printf( + "TEST-PASS | WindowsDllInterceptor | " + "%s: expectCalledPatchedDetouredCall (%d) matches with " + "sCalledPatchedDetouredCall (%d)\n", + aTestDescription, aExpectCalledPatchedDetouredCall, + sCalledPatchedDetouredCall); + fflush(stdout); + + if (!sCalledDetouredCallCallee) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "%s: gDetouredCall failed to call its callee\n", + aTestDescription); + fflush(stdout); + return false; + } + + printf( + "TEST-PASS | WindowsDllInterceptor | " + "%s: gDetouredCall successfully called its callee\n", + aTestDescription); + fflush(stdout); + + if (!sCouldUnwindFromDetouredCallCallee) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "%s: the callee of gDetouredCall failed to unwind\n", + aTestDescription); + fflush(stdout); + return false; + } + + printf( + "TEST-PASS | WindowsDllInterceptor | " + "%s: the callee of gDetouredCall successfully unwinded\n", + aTestDescription); + fflush(stdout); + return true; +} + +// Test that detouring a call preserves unwind information (bug 1798787). +bool TestDetouredCallUnwindInfo() { + if (!gDetouredCall) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "Failed to generate dynamic gDetouredCall code\n"); + fflush(stdout); + return false; + } + + uintptr_t imageBase = 0; + if (!RtlLookupFunctionEntry(reinterpret_cast<uintptr_t>(gDetouredCall), + &imageBase, nullptr)) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "Failed to find unwind information for dynamic gDetouredCall code\n"); + fflush(stdout); + return false; + } + + // We first double check that we manage to unwind when we *do not* detour + if (!TestCallingDetouredCall("Before hooking", false)) { + return false; + } + + uintptr_t StubAddress = 0; + + // The real test starts here: let's detour and check if we can still unwind + { + DetouredCallInterceptor ExeIntercept; + ExeIntercept.Init("TestDllInterceptor.exe"); + if (!orig_DetouredCall.Set(ExeIntercept, "DetouredCallJumper", + &patched_DetouredCall)) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "Failed to hook the detoured call jumper.\n"); + fflush(stdout); + return false; + } + + printf( + "TEST-PASS | WindowsDllInterceptor | " + "Successfully hooked the detoured call jumper.\n"); + fflush(stdout); + + StubAddress = reinterpret_cast<uintptr_t>(orig_DetouredCall.GetStub()); + if (!RtlLookupFunctionEntry(StubAddress, &imageBase, nullptr)) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "Failed to find unwind information for detoured code of " + "gDetouredCall\n"); + fflush(stdout); + return false; + } + + TestCallingDetouredCall("After hooking", true); + } + + // Check that we can still unwind after clearing the hook. + return TestCallingDetouredCall("After unhooking", false); +} +#endif // defined(_M_X64) && !defined(MOZ_CODE_COVERAGE) + +bool TestDynamicCodePolicy() { + if (!IsWin8Point1OrLater()) { + // Skip if a platform does not support this policy. + return true; + } + + MOZ_PROCESS_MITIGATION_DYNAMIC_CODE_POLICY policy = {}; + policy.ProhibitDynamicCode = true; + + mozilla::DynamicallyLinkedFunctionPtr<decltype(&SetProcessMitigationPolicy)> + pSetProcessMitigationPolicy(L"kernel32.dll", + "SetProcessMitigationPolicy"); + if (!pSetProcessMitigationPolicy) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "SetProcessMitigationPolicy does not exist.\n"); + fflush(stdout); + return false; + } + + if (!pSetProcessMitigationPolicy(ProcessDynamicCodePolicy, &policy, + sizeof(policy))) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "Fail to enable ProcessDynamicCodePolicy.\n"); + fflush(stdout); + return false; + } + + WindowsDllInterceptor ExeIntercept; + ExeIntercept.Init("TestDllInterceptor.exe"); + + // Make sure we fail to hook a function if ProcessDynamicCodePolicy is on + // because we cannot create an executable trampoline region. + if (orig_payloadNotHooked.Set(ExeIntercept, "payloadNotHooked", + &patched_rotatePayload)) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "ProcessDynamicCodePolicy is not working.\n"); + fflush(stdout); + return false; + } + + printf( + "TEST-PASS | WindowsDllInterceptor | " + "Successfully passed TestDynamicCodePolicy.\n"); + fflush(stdout); + return true; +} + +extern "C" int wmain(int argc, wchar_t* argv[]) { + LARGE_INTEGER start; + QueryPerformanceCounter(&start); + + // We disable this part of the test because the code coverage instrumentation + // injects code in rotatePayload in a way that WindowsDllInterceptor doesn't + // understand. +#ifndef MOZ_CODE_COVERAGE + payload initial = {0x12345678, 0xfc4e9d31, 0x87654321}; + payload p0, p1; + ZeroMemory(&p0, sizeof(p0)); + ZeroMemory(&p1, sizeof(p1)); + + p0 = rotatePayload(initial); + + { + WindowsDllInterceptor ExeIntercept; + ExeIntercept.Init("TestDllInterceptor.exe"); + if (orig_rotatePayload.Set(ExeIntercept, "rotatePayload", + &patched_rotatePayload)) { + printf("TEST-PASS | WindowsDllInterceptor | Hook added\n"); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Failed to add " + "hook\n"); + fflush(stdout); + return 1; + } + + p1 = rotatePayload(initial); + + if (patched_func_called) { + printf("TEST-PASS | WindowsDllInterceptor | Hook called\n"); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Hook was not " + "called\n"); + fflush(stdout); + return 1; + } + + if (p0 == p1) { + printf("TEST-PASS | WindowsDllInterceptor | Hook works properly\n"); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Hook didn't return " + "the right information\n"); + fflush(stdout); + return 1; + } + } + + patched_func_called = false; + ZeroMemory(&p1, sizeof(p1)); + + p1 = rotatePayload(initial); + + if (ShouldTestUnhookFunction != patched_func_called) { + printf( + "TEST-PASS | WindowsDllInterceptor | Hook was %scalled after " + "unregistration\n", + ShouldTestUnhookFunction ? "not " : ""); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Hook was %scalled " + "after unregistration\n", + ShouldTestUnhookFunction ? "" : "not "); + fflush(stdout); + return 1; + } + + if (p0 == p1) { + printf( + "TEST-PASS | WindowsDllInterceptor | Original function worked " + "properly\n"); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Original function " + "didn't return the right information\n"); + fflush(stdout); + return 1; + } +#endif + + CredHandle credHandle; + memset(&credHandle, 0, sizeof(CredHandle)); + OBJECT_ATTRIBUTES attributes = {}; + + // NB: These tests should be ordered such that lower-level APIs are tested + // before higher-level APIs. + if (TestShortDetour() && + // Run <ShortInterceptor> first because <WindowsDllInterceptor> + // does not clean up hooks. +#if defined(_M_X64) + TestAssemblyFunctions<ShortInterceptor>() && +#endif + TestAssemblyFunctions<WindowsDllInterceptor>() && +#ifdef _M_IX86 + // We keep this test to hook complex code on x86. (Bug 850957) + TEST_HOOK("ntdll.dll", NtFlushBuffersFile, NotEquals, 0) && +#endif + TEST_HOOK("ntdll.dll", NtCreateFile, NotEquals, 0) && + TEST_HOOK("ntdll.dll", NtReadFile, NotEquals, 0) && + TEST_HOOK("ntdll.dll", NtReadFileScatter, NotEquals, 0) && + TEST_HOOK("ntdll.dll", NtWriteFile, NotEquals, 0) && + TEST_HOOK("ntdll.dll", NtWriteFileGather, NotEquals, 0) && + TEST_HOOK_PARAMS("ntdll.dll", NtQueryFullAttributesFile, NotEquals, 0, + &attributes, nullptr) && + TEST_DETOUR_SKIP_EXEC("ntdll.dll", LdrLoadDll) && + TEST_HOOK("ntdll.dll", LdrUnloadDll, NotEquals, 0) && + MAYBE_TEST_HOOK_SKIP_EXEC(IsWin8OrLater(), "ntdll.dll", + LdrResolveDelayLoadedAPI) && + MAYBE_TEST_HOOK_PARAMS(HasApiSetQueryApiSetPresence(), + "Api-ms-win-core-apiquery-l1-1-0.dll", + ApiSetQueryApiSetPresence, Equals, FALSE, + &gEmptyUnicodeString, &gIsPresent) && + TEST_HOOK("kernelbase.dll", QueryDosDeviceW, Equals, 0) && + TEST_HOOK("kernel32.dll", GetFileAttributesW, Equals, + INVALID_FILE_ATTRIBUTES) && +#if !defined(_M_ARM64) +# ifndef MOZ_ASAN + // Bug 733892: toolkit/crashreporter/nsExceptionHandler.cpp + // This fails on ASan because the ASan runtime already hooked this + // function + TEST_HOOK("kernel32.dll", SetUnhandledExceptionFilter, Ignore, nullptr) && +# endif +#endif // !defined(_M_ARM64) +#ifdef _M_IX86 + TEST_HOOK_FOR_INVALID_HANDLE_VALUE("kernel32.dll", CreateFileW) && +#endif +#if !defined(_M_ARM64) + TEST_HOOK_FOR_INVALID_HANDLE_VALUE("kernel32.dll", CreateFileA) && +#endif // !defined(_M_ARM64) +#if !defined(_M_ARM64) + TEST_HOOK("kernel32.dll", TlsAlloc, NotEquals, TLS_OUT_OF_INDEXES) && + TEST_HOOK_PARAMS("kernel32.dll", TlsFree, Equals, FALSE, + TLS_OUT_OF_INDEXES) && + TEST_HOOK("kernel32.dll", CloseHandle, Equals, FALSE) && + TEST_HOOK("kernel32.dll", DuplicateHandle, Equals, FALSE) && +#endif // !defined(_M_ARM64) + TEST_DETOUR_SKIP_EXEC("kernel32.dll", BaseThreadInitThunk) && +#if defined(_M_X64) || defined(_M_ARM64) + MAYBE_TEST_HOOK(!IsWin8OrLater(), "kernel32.dll", + RtlInstallFunctionTableCallback, Equals, FALSE) && + TEST_HOOK("user32.dll", GetKeyState, Ignore, 0) && // see Bug 1316415 +#endif + TEST_HOOK("user32.dll", GetWindowInfo, Equals, FALSE) && + TEST_HOOK("user32.dll", TrackPopupMenu, Equals, FALSE) && + TEST_DETOUR("user32.dll", CreateWindowExW, Equals, nullptr) && + TEST_HOOK("user32.dll", InSendMessageEx, Equals, ISMEX_NOSEND) && + TEST_HOOK("user32.dll", SendMessageTimeoutW, Equals, 0) && + TEST_HOOK("user32.dll", SetCursorPos, NotEquals, FALSE) && +#if !defined(_M_ARM64) + TEST_HOOK("imm32.dll", ImmGetContext, Equals, nullptr) && +#endif // !defined(_M_ARM64) + TEST_HOOK("imm32.dll", ImmGetCompositionStringW, Ignore, 0) && + TEST_HOOK_SKIP_EXEC("imm32.dll", ImmSetCandidateWindow) && + TEST_HOOK("imm32.dll", ImmNotifyIME, Equals, 0) && + TEST_HOOK("comdlg32.dll", GetSaveFileNameW, Ignore, FALSE) && + TEST_HOOK("comdlg32.dll", GetOpenFileNameW, Ignore, FALSE) && +#if defined(_M_X64) + TEST_HOOK("comdlg32.dll", PrintDlgW, Ignore, 0) && +#endif + MAYBE_TEST_HOOK(ShouldTestTipTsf(), "tiptsf.dll", ProcessCaretEvents, + Ignore, nullptr) && + TEST_HOOK("wininet.dll", InternetOpenA, NotEquals, nullptr) && + TEST_HOOK("wininet.dll", InternetCloseHandle, Equals, FALSE) && + TEST_HOOK("wininet.dll", InternetConnectA, Equals, nullptr) && + TEST_HOOK("wininet.dll", InternetQueryDataAvailable, Equals, FALSE) && + TEST_HOOK("wininet.dll", InternetReadFile, Equals, FALSE) && + TEST_HOOK("wininet.dll", InternetWriteFile, Equals, FALSE) && + TEST_HOOK("wininet.dll", InternetSetOptionA, Equals, FALSE) && + TEST_HOOK("wininet.dll", HttpAddRequestHeadersA, Equals, FALSE) && + TEST_HOOK("wininet.dll", HttpOpenRequestA, Equals, nullptr) && + TEST_HOOK("wininet.dll", HttpQueryInfoA, Equals, FALSE) && + TEST_HOOK("wininet.dll", HttpSendRequestA, Equals, FALSE) && + TEST_HOOK("wininet.dll", HttpSendRequestExA, Equals, FALSE) && + TEST_HOOK("wininet.dll", HttpEndRequestA, Equals, FALSE) && + TEST_HOOK("wininet.dll", InternetQueryOptionA, Equals, FALSE) && + TEST_HOOK("sspicli.dll", AcquireCredentialsHandleA, NotEquals, + SEC_E_OK) && + TEST_HOOK_PARAMS("sspicli.dll", QueryCredentialsAttributesA, Equals, + SEC_E_INVALID_HANDLE, &credHandle, 0, nullptr) && + TEST_HOOK_PARAMS("sspicli.dll", FreeCredentialsHandle, Equals, + SEC_E_INVALID_HANDLE, &credHandle) && +#if defined(_M_X64) && !defined(MOZ_CODE_COVERAGE) + TestDetouredCallUnwindInfo() && +#endif // defined(_M_X64) && !defined(MOZ_CODE_COVERAGE) + // Run TestDynamicCodePolicy() at the end because the policy is + // irreversible. + TestDynamicCodePolicy()) { + printf("TEST-PASS | WindowsDllInterceptor | all checks passed\n"); + + LARGE_INTEGER end, freq; + QueryPerformanceCounter(&end); + + QueryPerformanceFrequency(&freq); + + LARGE_INTEGER result; + result.QuadPart = end.QuadPart - start.QuadPart; + result.QuadPart *= 1000000; + result.QuadPart /= freq.QuadPart; + + printf("Elapsed time: %lld microseconds\n", result.QuadPart); + + return 0; + } + + return 1; +} diff --git a/toolkit/xre/dllservices/tests/TestDllInterceptor.exe.manifest b/toolkit/xre/dllservices/tests/TestDllInterceptor.exe.manifest new file mode 100644 index 0000000000..11287012c5 --- /dev/null +++ b/toolkit/xre/dllservices/tests/TestDllInterceptor.exe.manifest @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" + manifestVersion="1.0" + xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"> + <assemblyIdentity type="win32" + name="TestDllInterceptor" + version="1.0.0.0" /> + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <!-- Need this to use functions in WindowsVersion.h --> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> <!-- Win10 --> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> <!-- Win8.1 --> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> <!-- Win8 --> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> <!-- Win7 --> + </application> + </compatibility> +</assembly> diff --git a/toolkit/xre/dllservices/tests/TestDllInterceptorCrossProcess.cpp b/toolkit/xre/dllservices/tests/TestDllInterceptorCrossProcess.cpp new file mode 100644 index 0000000000..32cf90bac1 --- /dev/null +++ b/toolkit/xre/dllservices/tests/TestDllInterceptorCrossProcess.cpp @@ -0,0 +1,159 @@ +/* -*- 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 https://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Attributes.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "nsWindowsDllInterceptor.h" +#include "nsWindowsHelpers.h" + +#include <string> + +using std::wstring; + +extern "C" __declspec(dllexport) int ReturnResult() { return 2; } + +static mozilla::CrossProcessDllInterceptor::FuncHookType< + decltype(&ReturnResult)> + gOrigReturnResult; + +static int ReturnResultHook() { + if (gOrigReturnResult() != 2) { + return 3; + } + + return 0; +} + +int ParentMain(int argc, wchar_t* argv[]) { + mozilla::SetArgv0ToFullBinaryPath(argv); + + // We'll add the child process to a job so that, in the event of a failure in + // this parent process, the child process will be automatically terminated. + nsAutoHandle job(::CreateJobObjectW(nullptr, nullptr)); + if (!job) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Job creation " + "failed\n"); + return 1; + } + + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = {}; + jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + if (!::SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation, + &jobInfo, sizeof(jobInfo))) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Job config " + "failed\n"); + return 1; + } + + wchar_t childArgv_1[] = L"-child"; + + wchar_t* childArgv[] = {argv[0], childArgv_1}; + + mozilla::UniquePtr<wchar_t[]> cmdLine( + mozilla::MakeCommandLine(mozilla::ArrayLength(childArgv), childArgv)); + + STARTUPINFOW si = {sizeof(si)}; + PROCESS_INFORMATION pi; + if (!::CreateProcessW(argv[0], cmdLine.get(), nullptr, nullptr, FALSE, + CREATE_SUSPENDED, nullptr, nullptr, &si, &pi)) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Failed to spawn " + "child process\n"); + return 1; + } + + nsAutoHandle childProcess(pi.hProcess); + nsAutoHandle childMainThread(pi.hThread); + + if (!::AssignProcessToJobObject(job.get(), childProcess.get())) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Failed to assign " + "child process to job\n"); + ::TerminateProcess(childProcess.get(), 1); + return 1; + } + + mozilla::nt::CrossExecTransferManager transferMgr(childProcess); + if (!transferMgr) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | " + "CrossExecTransferManager instantiation failed.\n"); + return 1; + } + + mozilla::CrossProcessDllInterceptor intcpt(childProcess.get()); + intcpt.Init("TestDllInterceptorCrossProcess.exe"); + + if (!gOrigReturnResult.Set(transferMgr, intcpt, "ReturnResult", + &ReturnResultHook)) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Failed to add " + "hook\n"); + return 1; + } + + printf("TEST-PASS | DllInterceptorCrossProcess | Hook added\n"); + + if (::ResumeThread(childMainThread.get()) == static_cast<DWORD>(-1)) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Failed to resume " + "child thread\n"); + return 1; + } + + BOOL remoteDebugging; + bool debugging = + ::IsDebuggerPresent() || + (::CheckRemoteDebuggerPresent(childProcess.get(), &remoteDebugging) && + remoteDebugging); + + DWORD waitResult = + ::WaitForSingleObject(childProcess.get(), debugging ? INFINITE : 60000); + if (waitResult != WAIT_OBJECT_0) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Child process " + "failed to finish\n"); + return 1; + } + + DWORD childExitCode; + if (!::GetExitCodeProcess(childProcess.get(), &childExitCode)) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Failed to obtain " + "child process exit code\n"); + return 1; + } + + if (childExitCode) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Child process " + "exit code is %lu instead of 0\n", + childExitCode); + return 1; + } + + printf( + "TEST-PASS | DllInterceptorCrossProcess | Child process exit code is " + "zero\n"); + return 0; +} + +extern "C" int wmain(int argc, wchar_t* argv[]) { + if (argc > 1) { + // clang keeps inlining this call despite every attempt to force it to do + // otherwise. We'll use GetProcAddress and call its function pointer + // instead. + auto pReturnResult = reinterpret_cast<decltype(&ReturnResult)>( + ::GetProcAddress(::GetModuleHandleW(nullptr), "ReturnResult")); + return pReturnResult(); + } + + return ParentMain(argc, argv); +} diff --git a/toolkit/xre/dllservices/tests/TestIATPatcher.cpp b/toolkit/xre/dllservices/tests/TestIATPatcher.cpp new file mode 100644 index 0000000000..2f84c0541a --- /dev/null +++ b/toolkit/xre/dllservices/tests/TestIATPatcher.cpp @@ -0,0 +1,121 @@ +/* -*- 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 https://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Assertions.h" +#include "mozilla/DynamicallyLinkedFunctionPtr.h" +#include "nsWindowsDllInterceptor.h" +#include "nsWindowsHelpers.h" + +#include <shlwapi.h> + +static int NormalImport() { return ::GetSystemMetrics(SM_CYCAPTION); } + +static bool DelayLoadImport() { + return !!::UrlIsW(L"http://example.com/", URLIS_FILEURL); +} + +static mozilla::WindowsIATPatcher::FuncHookType<decltype(&::GetSystemMetrics)> + gGetSystemMetricsHook; + +static mozilla::WindowsIATPatcher::FuncHookType<decltype(&::MessageBoxA)> + gMessageBoxAHook; + +static mozilla::WindowsIATPatcher::FuncHookType<decltype(&::UrlIsW)> gUrlIsHook; + +static bool gGetSystemMetricsHookCalled = false; + +static int WINAPI GetSystemMetricsHook(int aIndex) { + MOZ_DIAGNOSTIC_ASSERT(aIndex == SM_CYCAPTION); + gGetSystemMetricsHookCalled = true; + return 0; +} + +static bool gUrlIsHookCalled = false; + +static BOOL WINAPI UrlIsWHook(PCWSTR aUrl, URLIS aFlags) { + gUrlIsHookCalled = true; + return TRUE; +} + +static HMODULE GetStrongReferenceToExeModule() { + HMODULE result; + if (!::GetModuleHandleExW(0, nullptr, &result)) { + return nullptr; + } + + return result; +} + +#define PRINT_FAIL(msg) printf("TEST-UNEXPECTED-FAIL | IATPatcher | " msg "\n") + +extern "C" int wmain(int argc, wchar_t* argv[]) { + nsModuleHandle ourModule1(GetStrongReferenceToExeModule()); + if (!ourModule1) { + PRINT_FAIL("Failed obtaining HMODULE for executable"); + return 1; + } + + if (!gGetSystemMetricsHook.Set(ourModule1, "user32.dll", "GetSystemMetrics", + &GetSystemMetricsHook)) { + PRINT_FAIL("Failed setting GetSystemMetrics hook"); + return 1; + } + + if (NormalImport() || !gGetSystemMetricsHookCalled) { + PRINT_FAIL("GetSystemMetrics hook was not called"); + return 1; + } + + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::GetSystemMetrics)> + pRealGetSystemMetrics(L"user32.dll", "GetSystemMetrics"); + if (!pRealGetSystemMetrics) { + PRINT_FAIL("Failed resolving real GetSystemMetrics pointer"); + return 1; + } + + if (gGetSystemMetricsHook.GetStub() != pRealGetSystemMetrics) { + PRINT_FAIL( + "GetSystemMetrics hook stub pointer does not match real " + "GetSystemMetrics pointer"); + return 1; + } + + nsModuleHandle ourModule2(GetStrongReferenceToExeModule()); + if (!ourModule2) { + PRINT_FAIL("Failed obtaining HMODULE for executable"); + return 1; + } + + // This should fail becuase the test never calls, and thus never imports, + // MessageBoxA + if (gMessageBoxAHook.Set(ourModule2, "user32.dll", "MessageBoxA", nullptr)) { + PRINT_FAIL("Setting MessageBoxA hook succeeded when it should have failed"); + return 1; + } + + nsModuleHandle ourModule3(GetStrongReferenceToExeModule()); + if (!ourModule3) { + PRINT_FAIL("Failed obtaining HMODULE for executable"); + return 1; + } + + // These tests involve a delay-loaded import, which are not supported; we + // expect these tests to FAIL. + + if (gUrlIsHook.Set(ourModule3, "shlwapi.dll", "UrlIsW", &UrlIsWHook)) { + PRINT_FAIL("gUrlIsHook.Set should have failed"); + return 1; + } + + if (DelayLoadImport() || gUrlIsHookCalled) { + PRINT_FAIL("gUrlIsHook should not have been called"); + return 1; + } + + printf("TEST-PASS | IATPatcher | All tests passed.\n"); + return 0; +} diff --git a/toolkit/xre/dllservices/tests/TestMMPolicy.cpp b/toolkit/xre/dllservices/tests/TestMMPolicy.cpp new file mode 100644 index 0000000000..1ae93a4ed1 --- /dev/null +++ b/toolkit/xre/dllservices/tests/TestMMPolicy.cpp @@ -0,0 +1,204 @@ +/* -*- 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 "nsWindowsDllInterceptor.h" + +#include <functional> + +mozilla::interceptor::MMPolicyInProcess gPolicy; + +void DepleteVirtualAddress( + uint8_t* aStart, size_t aSize, + const std::function<void(void*)>& aPostAllocCallback) { + const DWORD granularity = gPolicy.GetAllocGranularity(); + if (aStart == 0 || aSize < granularity) { + return; + } + + uint8_t* alignedStart = reinterpret_cast<uint8_t*>( + (((reinterpret_cast<uintptr_t>(aStart) - 1) / granularity) + 1) * + granularity); + aSize -= (alignedStart - aStart); + if (auto p = VirtualAlloc(alignedStart, aSize, MEM_RESERVE, PAGE_NOACCESS)) { + aPostAllocCallback(p); + return; + } + + uintptr_t mask = ~(static_cast<uintptr_t>(granularity) - 1); + size_t halfSize = (aSize >> 1) & mask; + if (halfSize == 0) { + return; + } + + DepleteVirtualAddress(aStart, halfSize, aPostAllocCallback); + DepleteVirtualAddress(aStart + halfSize, aSize - halfSize, + aPostAllocCallback); +} + +bool ValidateFreeRegion(LPVOID aRegion, size_t aDesiredLen) { + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery(aRegion, &mbi, sizeof(mbi)) != sizeof(mbi)) { + printf( + "TEST-FAILED | TestMMPolicy | " + "VirtualQuery(%p) failed - %08lx\n", + aRegion, GetLastError()); + return false; + } + + if (mbi.State != MEM_FREE) { + printf( + "TEST-FAILED | TestMMPolicy | " + "%p is not within a free region\n", + aRegion); + return false; + } + + if (aRegion != mbi.BaseAddress || + reinterpret_cast<uintptr_t>(mbi.BaseAddress) % + gPolicy.GetAllocGranularity()) { + printf( + "TEST-FAILED | TestMMPolicy | " + "%p is not a region's start address\n", + aRegion); + return false; + } + + LPVOID allocated = VirtualAlloc(aRegion, aDesiredLen, + MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + if (!allocated) { + printf( + "TEST-FAILED | TestMMPolicy | " + "VirtualAlloc(%p) failed - %08lx\n", + aRegion, GetLastError()); + return false; + } + + if (!VirtualFree(allocated, 0, MEM_RELEASE)) { + printf( + "TEST-FAILED | TestMMPolicy | " + "VirtualFree(%p) failed - %08lx\n", + allocated, GetLastError()); + return false; + } + + return true; +} + +bool TestFindRegion() { + // Skip the near-null addresses + uint8_t* minAddr = reinterpret_cast<uint8_t*>( + std::max(gPolicy.GetAllocGranularity(), 0x1000000ul)); + // 64bit address space is too large to deplete. 32bit space is enough. + uint8_t* maxAddr = reinterpret_cast<uint8_t*>(std::min( + gPolicy.GetMaxUserModeAddress(), static_cast<uintptr_t>(0xffffffff))); + + // Keep one of the regions we allocate so that we can release it later. + void* lastResort = nullptr; + + // Reserve all free regions in the range [minAddr, maxAddr] + for (uint8_t* address = minAddr; address <= maxAddr;) { + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery(address, &mbi, sizeof(mbi)) != sizeof(mbi)) { + printf( + "TEST-FAILED | TestMMPolicy | " + "VirtualQuery(%p) failed - %08lx\n", + address, GetLastError()); + break; + } + + address = reinterpret_cast<uint8_t*>(mbi.BaseAddress); + if (mbi.State == MEM_FREE) { + DepleteVirtualAddress(address, mbi.RegionSize, + [&lastResort](void* aAllocated) { + // Pick the first address we allocate to make sure + // FindRegion scans the full range. + if (!lastResort) { + lastResort = aAllocated; + } + }); + } + + address += mbi.RegionSize; + } + + if (!lastResort) { + printf( + "TEST-SKIPPED | TestMMPolicy | " + "No free region in [%p - %p]. Skipping the testcase.\n", + minAddr, maxAddr); + return true; + } + + // Make sure there are no free regions + PVOID freeRegion = + gPolicy.FindRegion(GetCurrentProcess(), 1, minAddr, maxAddr); + if (freeRegion) { + if (reinterpret_cast<uintptr_t>(freeRegion) % + gPolicy.GetAllocGranularity()) { + printf( + "TEST-FAILED | TestMMPolicy | " + "MMPolicyBase::FindRegion returned an unaligned address %p.\n", + freeRegion); + return false; + } + + printf( + "TEST-SKIPPED | TestMMPolicy | " + "%p was freed after depletion. Skipping the testcase.\n", + freeRegion); + return true; + } + + // Free one region, and thus we can expect FindRegion finds this region + if (!VirtualFree(lastResort, 0, MEM_RELEASE)) { + printf( + "TEST-FAILED | TestMMPolicy | " + "VirtualFree(%p) failed - %08lx\n", + lastResort, GetLastError()); + return false; + } + printf("The region starting from %p has been freed.\n", lastResort); + + // Run the function several times because it uses a randon number inside + // and its result is nondeterministic. + for (int i = 0; i < 50; ++i) { + // Because one region was freed, a desire up to one region + // should be fulfilled. + const size_t desiredLengths[] = {1, gPolicy.GetAllocGranularity()}; + + for (auto desiredLen : desiredLengths) { + freeRegion = + gPolicy.FindRegion(GetCurrentProcess(), desiredLen, minAddr, maxAddr); + if (!freeRegion) { + printf( + "TEST-FAILED | TestMMPolicy | " + "Failed to find a free region.\n"); + return false; + } + + if (!ValidateFreeRegion(freeRegion, desiredLen)) { + return false; + } + } + } + + return true; +} + +extern "C" int wmain(int argc, wchar_t* argv[]) { + // Preload delayload modules (e.g. advapi32.dll, bcryptPrimitives.dll, etc.) + // by calling rand_s(), which is used in MMPolicy::FindRegion, before + // depleting the process memory during the test. + unsigned int rnd = 0; + rand_s(&rnd); + + if (!TestFindRegion()) { + return 1; + } + + printf("TEST-PASS | TestMMPolicy | All tests passed.\n"); + return 0; +} diff --git a/toolkit/xre/dllservices/tests/gtest/TestDLLBlocklist.cpp b/toolkit/xre/dllservices/tests/gtest/TestDLLBlocklist.cpp new file mode 100644 index 0000000000..5f141b22ae --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDLLBlocklist.cpp @@ -0,0 +1,223 @@ +/* -*- 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 "gtest/gtest.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Char16.h" +#include "mozilla/gtest/MozAssertions.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsString.h" +#include "nsTArray.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; +} + +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, 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())); +} + +// RedirectToNoOpEntryPoint needs the launcher process. +#if defined(MOZ_LAUNCHER_PROCESS) +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 +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) +{ + nsTArray<DLL_BLOCKLIST_STRING_TYPE> dupes; + 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'); + } + + // Check for duplicate entries + for (auto&& dupe : dupes) { + EXPECT_NE(stricmp(dupe, pEntry->mName), 0); + } + + dupes.AppendElement(pEntry->mName); + } +} + +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) +} diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.cpp @@ -0,0 +1,7 @@ +/* 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> + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) { return TRUE; } diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.rc b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.rc new file mode 100644 index 0000000000..f56aa099ff --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.rc @@ -0,0 +1,42 @@ +/* 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 <winver.h> + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 5,5,5,6 + PRODUCTVERSION 5,5,5,1 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "mozilla.org" + VALUE "FileDescription", L"Test DLL" + VALUE "FileVersion", "1.0" + VALUE "InternalName", "Test DLL" + VALUE "OriginalFilename", "TestDllBlocklist_AllowByVersion.dll" + VALUE "ProductName", "Test DLL" + VALUE "ProductVersion", "1.0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1252 + END +END diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_AllowByVersion/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_AllowByVersion/moz.build new file mode 100644 index 0000000000..0987cdde1a --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_AllowByVersion/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# 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/. + +DIST_INSTALL = False + +SharedLibrary("TestDllBlocklist_AllowByVersion") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_AllowByVersion.cpp", +] + +RCFILE = "TestDllBlocklist_AllowByVersion.rc" + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_AllowByVersion.dll"] diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByName/TestDllBlocklist_MatchByName.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByName/TestDllBlocklist_MatchByName.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByName/TestDllBlocklist_MatchByName.cpp @@ -0,0 +1,7 @@ +/* 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> + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) { return TRUE; } diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByName/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByName/moz.build new file mode 100644 index 0000000000..f34931898a --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByName/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# 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/. + +DIST_INSTALL = False + +SharedLibrary("TestDllBlocklist_MatchByName") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_MatchByName.cpp", +] + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_MatchByName.dll"] diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.cpp @@ -0,0 +1,7 @@ +/* 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> + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) { return TRUE; } diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.rc b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.rc new file mode 100644 index 0000000000..7390c1cb34 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.rc @@ -0,0 +1,42 @@ +/* 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 <winver.h> + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 5,5,5,5 + PRODUCTVERSION 5,5,5,1 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "mozilla.org" + VALUE "FileDescription", L"Test DLL" + VALUE "FileVersion", "1.0" + VALUE "InternalName", "Test DLL" + VALUE "OriginalFilename", "TestDllBlocklist_MatchByVersion.dll" + VALUE "ProductName", "Test DLL" + VALUE "ProductVersion", "1.0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1252 + END +END diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByVersion/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByVersion/moz.build new file mode 100644 index 0000000000..38e10524c7 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByVersion/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# 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/. + +DIST_INSTALL = False + +SharedLibrary("TestDllBlocklist_MatchByVersion") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_MatchByVersion.cpp", +] + +RCFILE = "TestDllBlocklist_MatchByVersion.rc" + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_MatchByVersion.dll"] diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.cpp new file mode 100644 index 0000000000..2505b8b700 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.cpp @@ -0,0 +1,12 @@ +/* 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 "mozilla/Assertions.h" + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) { + MOZ_RELEASE_ASSERT(0); + return TRUE; +} diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.rc b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.rc new file mode 100644 index 0000000000..7c79dac373 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.rc @@ -0,0 +1,42 @@ +/* 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 <winver.h> + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 5,5,5,5 + PRODUCTVERSION 5,5,5,1 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "mozilla.org" + VALUE "FileDescription", L"Test DLL" + VALUE "FileVersion", "1.0" + VALUE "InternalName", "Test DLL" + VALUE "OriginalFilename", "TestDllBlocklist_NoOpEntryPoint.dll" + VALUE "ProductName", "Test DLL" + VALUE "ProductVersion", "1.0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1252 + END +END diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_NoOpEntryPoint/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_NoOpEntryPoint/moz.build new file mode 100644 index 0000000000..e9a10a150a --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_NoOpEntryPoint/moz.build @@ -0,0 +1,21 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# 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/. + +DIST_INSTALL = False + +SharedLibrary("TestDllBlocklist_NoOpEntryPoint") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_NoOpEntryPoint.cpp", +] + +RCFILE = "TestDllBlocklist_NoOpEntryPoint.rc" + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_NoOpEntryPoint.dll"] + +OS_LIBS += [ + "uuid", +] diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SocketProcessOnly/TestDllBlocklist_SocketProcessOnly.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SocketProcessOnly/TestDllBlocklist_SocketProcessOnly.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SocketProcessOnly/TestDllBlocklist_SocketProcessOnly.cpp @@ -0,0 +1,7 @@ +/* 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> + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) { return TRUE; } diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SocketProcessOnly/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SocketProcessOnly/moz.build new file mode 100644 index 0000000000..dc93544e1b --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SocketProcessOnly/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# 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/. + +DIST_INSTALL = False + +SharedLibrary("TestDllBlocklist_SocketProcessOnly") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_SocketProcessOnly.cpp", +] + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_SocketProcessOnly.dll"] diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UserBlocked/TestDllBlocklist_UserBlocked.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UserBlocked/TestDllBlocklist_UserBlocked.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UserBlocked/TestDllBlocklist_UserBlocked.cpp @@ -0,0 +1,7 @@ +/* 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> + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) { return TRUE; } diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UserBlocked/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UserBlocked/moz.build new file mode 100644 index 0000000000..31996c5cb2 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UserBlocked/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# 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/. + +DIST_INSTALL = False + +SharedLibrary("TestDllBlocklist_UserBlocked") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_UserBlocked.cpp", +] + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_UserBlocked.dll"] diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UtilityProcessOnly/TestDllBlocklist_UtilityProcessOnly.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UtilityProcessOnly/TestDllBlocklist_UtilityProcessOnly.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UtilityProcessOnly/TestDllBlocklist_UtilityProcessOnly.cpp @@ -0,0 +1,7 @@ +/* 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> + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) { return TRUE; } diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UtilityProcessOnly/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UtilityProcessOnly/moz.build new file mode 100644 index 0000000000..913d0f155c --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UtilityProcessOnly/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# 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/. + +DIST_INSTALL = False + +SharedLibrary("TestDllBlocklist_UtilityProcessOnly") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_UtilityProcessOnly.cpp", +] + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_UtilityProcessOnly.dll"] diff --git a/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules.cpp b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules.cpp new file mode 100644 index 0000000000..f7865a2ed0 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules.cpp @@ -0,0 +1,461 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +#include "gtest/gtest.h" + +#include "js/RegExp.h" +#include "mozilla/BinarySearch.h" +#include "mozilla/gtest/MozAssertions.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/UntrustedModulesProcessor.h" +#include "mozilla/WinDllServices.h" +#include "nsContentUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "TelemetryFixture.h" +#include "UntrustedModulesBackupService.h" +#include "UntrustedModulesDataSerializer.h" + +using namespace mozilla; + +class ModuleLoadCounter final { + nsTHashMap<nsStringCaseInsensitiveHashKey, int> mCounters; + + public: + template <size_t N> + ModuleLoadCounter(const nsString (&aNames)[N], const int (&aCounts)[N]) + : mCounters(N) { + for (size_t i = 0; i < N; ++i) { + mCounters.InsertOrUpdate(aNames[i], aCounts[i]); + } + } + + template <size_t N> + bool Remains(const nsString (&aNames)[N], const int (&aCounts)[N]) { + EXPECT_EQ(mCounters.Count(), N); + if (mCounters.Count() != N) { + return false; + } + + bool result = true; + for (size_t i = 0; i < N; ++i) { + auto entry = mCounters.Lookup(aNames[i]); + if (!entry) { + wprintf(L"%s is not registered.\n", + static_cast<const wchar_t*>(aNames[i].get())); + result = false; + } else if (*entry != aCounts[i]) { + // We can return false, but let's print out all unmet modules + // which may be helpful to investigate test failures. + wprintf(L"%s:%4d\n", static_cast<const wchar_t*>(aNames[i].get()), + *entry); + result = false; + } + } + return result; + } + + bool IsDone() const { + bool allZero = true; + for (const auto& data : mCounters.Values()) { + if (data < 0) { + // If any counter is negative, we know the test fails. + // No need to continue. + return true; + } + if (data > 0) { + allZero = false; + } + } + // If all counters are zero, the test finished nicely. Otherwise, those + // counters are expected to be decremented later. Let's continue. + return allZero; + } + + void Decrement(const nsString& aName) { + if (auto entry = mCounters.Lookup(aName)) { + --(*entry); + } + } +}; + +class UntrustedModulesCollector { + static constexpr int kMaximumPendingQueries = 500; + Vector<UntrustedModulesData> mData; + + public: + Vector<UntrustedModulesData>& Data() { return mData; } + + nsresult Collect(ModuleLoadCounter& aChecker) { + nsresult rv = NS_OK; + + mData.clear(); + int pendingQueries = 0; + + EXPECT_TRUE(SpinEventLoopUntil( + "xre:UntrustedModulesCollector"_ns, + [this, &pendingQueries, &aChecker, &rv]() { + // Some of expected loaded modules are still missing + // after kMaximumPendingQueries queries were submitted. + // Giving up here to avoid an infinite loop. + if (pendingQueries >= kMaximumPendingQueries) { + rv = NS_ERROR_ABORT; + return true; + } + + ++pendingQueries; + + RefPtr<DllServices> dllSvc(DllServices::Get()); + dllSvc->GetUntrustedModulesData()->Then( + GetMainThreadSerialEventTarget(), __func__, + [this, &pendingQueries, + &aChecker](Maybe<UntrustedModulesData>&& aResult) { + EXPECT_GT(pendingQueries, 0); + --pendingQueries; + + if (aResult.isSome()) { + wprintf(L"Received data. (pendingQueries=%d)\n", + pendingQueries); + for (auto item : aResult.ref().mEvents) { + aChecker.Decrement(item->mEvent.mRequestedDllName); + } + EXPECT_TRUE(mData.emplaceBack(std::move(aResult.ref()))); + } + }, + [&pendingQueries, &rv](nsresult aReason) { + EXPECT_GT(pendingQueries, 0); + --pendingQueries; + + wprintf(L"GetUntrustedModulesData() failed - %08x\n", aReason); + EXPECT_TRUE(false); + rv = aReason; + }); + + // Keep calling GetUntrustedModulesData() until we meet the condition. + return aChecker.IsDone(); + })); + + EXPECT_TRUE(SpinEventLoopUntil( + "xre:UntrustedModulesCollector(pendingQueries)"_ns, + [&pendingQueries]() { return pendingQueries <= 0; })); + + return rv; + } +}; + +class UntrustedModulesFixture : public TelemetryTestFixture { + static constexpr int kLoadCountBeforeDllServices = 5; + static constexpr int kLoadCountAfterDllServices = 5; + static constexpr uint32_t kMaxModulesArrayLen = 10; + + // One of the important test scenarios is to load modules before DllServices + // is initialized and to make sure those loading events are forwarded when + // DllServices is initialized. + // However, GTest instantiates a Fixture class every testcase and there is + // no way to re-enable DllServices and UntrustedModulesProcessor once it's + // disabled, which means no matter how many testcases we have, only the + // first testcase exercises that scenario. That's why we implement that + // test scenario in InitialModuleLoadOnce as a static member and runs it + // in the first testcase to be executed. + static INIT_ONCE sInitLoadOnce; + static UntrustedModulesCollector sInitLoadDataCollector; + + static nsString PrependWorkingDir(const nsAString& aLeaf) { + nsCOMPtr<nsIFile> file; + EXPECT_TRUE(NS_SUCCEEDED(NS_GetSpecialDirectory(NS_OS_CURRENT_WORKING_DIR, + getter_AddRefs(file)))); + EXPECT_NS_SUCCEEDED(file->Append(aLeaf)); + bool exists; + EXPECT_TRUE(NS_SUCCEEDED(file->Exists(&exists)) && exists); + nsString fullPath; + EXPECT_NS_SUCCEEDED(file->GetPath(fullPath)); + return fullPath; + } + + static BOOL CALLBACK InitialModuleLoadOnce(PINIT_ONCE, void*, void**); + + protected: + static constexpr int kInitLoadCount = + kLoadCountBeforeDllServices + kLoadCountAfterDllServices; + static const nsString kTestModules[]; + + static void ValidateUntrustedModules(const UntrustedModulesData& aData, + bool aIsTruncatedData = false); + + static void LoadAndFree(const nsAString& aLeaf) { + nsModuleHandle dll(::LoadLibraryW(PrependWorkingDir(aLeaf).get())); + EXPECT_TRUE(!!dll); + } + + virtual void SetUp() override { + TelemetryTestFixture::SetUp(); + ::InitOnceExecuteOnce(&sInitLoadOnce, InitialModuleLoadOnce, nullptr, + nullptr); + } + + static const Vector<UntrustedModulesData>& GetInitLoadData() { + return sInitLoadDataCollector.Data(); + } + + // This method is useful if we want a new instance of UntrustedModulesData + // which is not copyable. + static UntrustedModulesData CollectSingleData() { + // If we call LoadAndFree more than once, those loading events are + // likely to be merged into an instance of UntrustedModulesData, + // meaning the length of the collector's vector is at least one but + // the exact number is unknown. + LoadAndFree(kTestModules[0]); + + UntrustedModulesCollector collector; + ModuleLoadCounter waitForOne({kTestModules[0]}, {1}); + EXPECT_NS_SUCCEEDED(collector.Collect(waitForOne)); + EXPECT_TRUE(waitForOne.Remains({kTestModules[0]}, {0})); + EXPECT_EQ(collector.Data().length(), 1U); + + // Cannot "return collector.Data()[0]" as copy ctor is deleted. + return UntrustedModulesData(std::move(collector.Data()[0])); + } + + template <typename DataFetcherT> + void ValidateJSValue(const char16_t* aPattern, size_t aPatternLength, + DataFetcherT&& aDataFetcher) { + AutoJSContextWithGlobal cx(mCleanGlobal); + mozilla::Telemetry::UntrustedModulesDataSerializer serializer( + cx.GetJSContext(), kMaxModulesArrayLen); + EXPECT_TRUE(!!serializer); + aDataFetcher(serializer); + + JS::Rooted<JS::Value> jsval(cx.GetJSContext()); + serializer.GetObject(&jsval); + + nsAutoString json; + EXPECT_TRUE(nsContentUtils::StringifyJSON(cx.GetJSContext(), &jsval, json)); + + JS::Rooted<JSObject*> re( + cx.GetJSContext(), + JS::NewUCRegExpObject(cx.GetJSContext(), aPattern, aPatternLength, + JS::RegExpFlag::Global)); + EXPECT_TRUE(!!re); + + JS::Rooted<JS::Value> matchResult(cx.GetJSContext(), JS::NullValue()); + size_t idx = 0; + EXPECT_TRUE(JS::ExecuteRegExpNoStatics(cx.GetJSContext(), re, json.get(), + json.Length(), &idx, true, + &matchResult)); + // On match, with aOnlyMatch = true, ExecuteRegExpNoStatics returns boolean + // true. If no match, ExecuteRegExpNoStatics returns Null. + EXPECT_TRUE(matchResult.isBoolean() && matchResult.toBoolean()); + if (!matchResult.isBoolean() || !matchResult.toBoolean()) { + // If match failed, print out the actual JSON kindly. + wprintf(L"JSON: %s\n", static_cast<const wchar_t*>(json.get())); + wprintf(L"RE: %s\n", aPattern); + } + } +}; + +const nsString UntrustedModulesFixture::kTestModules[] = { + // Sorted for binary-search + u"TestUntrustedModules_Dll1.dll"_ns, + u"TestUntrustedModules_Dll2.dll"_ns, +}; + +INIT_ONCE UntrustedModulesFixture::sInitLoadOnce = INIT_ONCE_STATIC_INIT; +UntrustedModulesCollector UntrustedModulesFixture::sInitLoadDataCollector; + +void UntrustedModulesFixture::ValidateUntrustedModules( + const UntrustedModulesData& aData, bool aIsTruncatedData) { + // This defines a list of modules which are listed on our blocklist and + // thus its loading status is not expected to be Status::Loaded. + // Although the UntrustedModulesFixture test does not touch any of them, + // the current process might have run a test like TestDllBlocklist where + // we try to load and block them. + const struct { + const wchar_t* mName; + ModuleLoadInfo::Status mStatus; + } kKnownModules[] = { + // Sorted by mName for binary-search + {L"TestDllBlocklist_MatchByName.dll", ModuleLoadInfo::Status::Blocked}, + {L"TestDllBlocklist_MatchByVersion.dll", ModuleLoadInfo::Status::Blocked}, + {L"TestDllBlocklist_NoOpEntryPoint.dll", + ModuleLoadInfo::Status::Redirected}, +#if !defined(MOZ_ASAN) + // With ASAN, the test uses mozglue's blocklist where + // the user blocklist is not used. So only check for this + // DLL in the non-ASAN case. + {L"TestDllBlocklist_UserBlocked.dll", ModuleLoadInfo::Status::Blocked}, +#endif // !defined(MOZ_ASAN) + }; + + EXPECT_EQ(aData.mProcessType, GeckoProcessType_Default); + EXPECT_EQ(aData.mPid, ::GetCurrentProcessId()); + + nsTHashtable<nsPtrHashKey<void>> moduleSet; + for (const RefPtr<ModuleRecord>& module : aData.mModules.Values()) { + moduleSet.PutEntry(module); + } + + size_t numBlockedEvents = 0; + for (auto item : aData.mEvents) { + const auto& evt = item->mEvent; + const nsDependentSubstring leafName = + nt::GetLeafName(evt.mModule->mResolvedNtName); + const nsAutoString leafNameStr(leafName.Data(), leafName.Length()); + const ModuleLoadInfo::Status loadStatus = + static_cast<ModuleLoadInfo::Status>(evt.mLoadStatus); + if (loadStatus == ModuleLoadInfo::Status::Blocked) { + ++numBlockedEvents; + } + + size_t match; + if (BinarySearchIf( + kKnownModules, 0, ArrayLength(kKnownModules), + [&leafNameStr](const auto& aVal) { + return _wcsicmp(leafNameStr.get(), aVal.mName); + }, + &match)) { + EXPECT_EQ(loadStatus, kKnownModules[match].mStatus); + } else { + EXPECT_EQ(evt.mLoadStatus, 0U); + } + + if (BinarySearchIf( + kTestModules, 0, ArrayLength(kTestModules), + [&leafNameStr](const auto& aVal) { + return _wcsicmp(leafNameStr.get(), aVal.get()); + }, + &match)) { + // We know the test modules are loaded in the main thread, + // but we don't know about other modules. + EXPECT_EQ(evt.mThreadId, ::GetCurrentThreadId()); + } + + // Make sure mModule is pointing to an entry of mModules. + EXPECT_TRUE(moduleSet.Contains(evt.mModule)); + EXPECT_FALSE(evt.mIsDependent); + } + + // No check for the mXULLoadDurationMS field because the field has a value + // in CCov build GTest, but it is empty in non-CCov build (bug 1681936). + EXPECT_EQ(aData.mNumEvents, aData.mEvents.length()); + EXPECT_GT(aData.mNumEvents, 0U); + if (aIsTruncatedData) { + EXPECT_EQ(aData.mStacks.GetModuleCount(), 0U); + EXPECT_LE(aData.mNumEvents, UntrustedModulesData::kMaxEvents); + } else if (numBlockedEvents == aData.mNumEvents) { + // If all loading events were blocked or aData is truncated, + // the stacks are empty. + EXPECT_EQ(aData.mStacks.GetModuleCount(), 0U); + } else { + EXPECT_GT(aData.mStacks.GetModuleCount(), 0U); + } + EXPECT_EQ(aData.mSanitizationFailures, 0U); + EXPECT_EQ(aData.mTrustTestFailures, 0U); +} + +BOOL CALLBACK UntrustedModulesFixture::InitialModuleLoadOnce(PINIT_ONCE, void*, + void**) { + for (int i = 0; i < kLoadCountBeforeDllServices; ++i) { + for (const auto& mod : kTestModules) { + LoadAndFree(mod); + } + } + + RefPtr<DllServices> dllSvc(DllServices::Get()); + dllSvc->StartUntrustedModulesProcessor(true); + + for (int i = 0; i < kLoadCountAfterDllServices; ++i) { + for (const auto& mod : kTestModules) { + LoadAndFree(mod); + } + } + + ModuleLoadCounter waitForTwo(kTestModules, {kInitLoadCount, kInitLoadCount}); + EXPECT_EQ(sInitLoadDataCollector.Collect(waitForTwo), NS_OK); + EXPECT_TRUE(waitForTwo.Remains(kTestModules, {0, 0})); + + for (const auto& event : GetInitLoadData()) { + ValidateUntrustedModules(event); + } + + // Data was removed when retrieved. No data is retrieved again. + UntrustedModulesCollector collector; + ModuleLoadCounter waitOnceForEach(kTestModules, {1, 1}); + EXPECT_EQ(collector.Collect(waitOnceForEach), NS_ERROR_ABORT); + EXPECT_TRUE(waitOnceForEach.Remains(kTestModules, {1, 1})); + + return TRUE; +} + +#define PROCESS_OBJ(TYPE, PID) \ + u"\"" TYPE u"\\." PID u"\":{" \ + u"\"processType\":\"" TYPE u"\",\"elapsed\":\\d+\\.\\d+," \ + u"\"sanitizationFailures\":0,\"trustTestFailures\":0," \ + u"\"events\":\\[{" \ + u"\"processUptimeMS\":\\d+,\"loadDurationMS\":\\d+\\.\\d+," \ + u"\"threadID\":\\d+,\"threadName\":\"Main Thread\"," \ + u"\"baseAddress\":\"0x[0-9a-f]+\",\"moduleIndex\":0," \ + u"\"isDependent\":false,\"loadStatus\":0}\\]," \ + u"\"combinedStacks\":{" \ + u"\"memoryMap\":\\[\\[\"\\w+\\.\\w+\",\"[0-9A-Z]+\"\\]" \ + u"(,\\[\"\\w+\\.\\w+\",\"[0-9A-Z]+\\\"\\])*\\]," \ + u"\"stacks\":\\[\\[\\[(-1|\\d+),\\d+\\]" \ + u"(,\\[(-1|\\d+),\\d+\\])*\\]\\]}}" + +TEST_F(UntrustedModulesFixture, Serialize) { + // clang-format off + const char16_t kPattern[] = u"{\"structVersion\":1," + u"\"modules\":\\[{" + u"\"resolvedDllName\":\"TestUntrustedModules_Dll1\\.dll\"," + u"\"fileVersion\":\"1\\.2\\.3\\.4\"," + u"\"companyName\":\"Mozilla Corporation\",\"trustFlags\":0}\\]," + u"\"blockedModules\":\\[.*?\\]," // allow for the case where there are some blocked modules + u"\"processes\":{" + PROCESS_OBJ(u"browser", u"0xabc") u"," + PROCESS_OBJ(u"browser", u"0x4") u"," + PROCESS_OBJ(u"rdd", u"0x4") + u"}}"; + // clang-format on + + UntrustedModulesBackupData backup1, backup2; + { + UntrustedModulesData data1 = CollectSingleData(); + UntrustedModulesData data2 = CollectSingleData(); + UntrustedModulesData data3 = CollectSingleData(); + + data1.mPid = 0xabc; + data2.mPid = 0x4; + data2.mProcessType = GeckoProcessType_RDD; + data3.mPid = 0x4; + + backup1.Add(std::move(data1)); + backup2.Add(std::move(data2)); + backup1.Add(std::move(data3)); + } + + ValidateJSValue(kPattern, ArrayLength(kPattern) - 1, + [&backup1, &backup2]( + Telemetry::UntrustedModulesDataSerializer& aSerializer) { + EXPECT_NS_SUCCEEDED(aSerializer.Add(backup1)); + EXPECT_NS_SUCCEEDED(aSerializer.Add(backup2)); + }); +} + +TEST_F(UntrustedModulesFixture, Backup) { + RefPtr<UntrustedModulesBackupService> backupSvc( + UntrustedModulesBackupService::Get()); + for (int i = 0; i < 100; ++i) { + backupSvc->Backup(CollectSingleData()); + } + + backupSvc->SettleAllStagingData(); + EXPECT_TRUE(backupSvc->Staging().IsEmpty()); + + for (const auto& entry : backupSvc->Settled()) { + const RefPtr<UntrustedModulesDataContainer>& container = entry.GetData(); + EXPECT_TRUE(!!container); + const UntrustedModulesData& data = container->mData; + EXPECT_EQ(entry.GetKey(), ProcessHashKey(data.mProcessType, data.mPid)); + ValidateUntrustedModules(data, /*aIsTruncatedData*/ true); + } +} diff --git a/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll1/TestUntrustedModules_Dll1.cpp b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll1/TestUntrustedModules_Dll1.cpp new file mode 100644 index 0000000000..4f6ce877eb --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll1/TestUntrustedModules_Dll1.cpp @@ -0,0 +1,7 @@ +/* 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> + +BOOL WINAPI DllMain(HINSTANCE, DWORD, LPVOID) { return TRUE; } diff --git a/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll1/TestUntrustedModules_Dll1.rc b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll1/TestUntrustedModules_Dll1.rc new file mode 100644 index 0000000000..2358b88b93 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll1/TestUntrustedModules_Dll1.rc @@ -0,0 +1,38 @@ +/* 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 <winver.h>
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION 1,2,3,4 // This field will be collected
+ PRODUCTVERSION 5,6,7,8
+ FILEFLAGSMASK 0x3fL
+#ifdef _DEBUG
+ FILEFLAGS 0x1L
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS__WINDOWS32
+ FILETYPE VFT_DLL
+ FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904e4"
+ BEGIN
+ VALUE "CompanyName", "Mozilla Corporation"
+ VALUE "OriginalFilename", "TestUntrustedModules_Dll1.dll"
+ VALUE "ProductName", "Test DLL"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x0409, 1252
+ END
+END
diff --git a/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll1/moz.build b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll1/moz.build new file mode 100644 index 0000000000..57fc59ca8a --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll1/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# 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/. + +DIST_INSTALL = False + +SharedLibrary("TestUntrustedModules_Dll1") + +UNIFIED_SOURCES = [ + "TestUntrustedModules_Dll1.cpp", +] + +RCFILE = "TestUntrustedModules_Dll1.rc" + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestUntrustedModules_Dll1.dll"] diff --git a/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll2/TestUntrustedModules_Dll2.cpp b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll2/TestUntrustedModules_Dll2.cpp new file mode 100644 index 0000000000..4f6ce877eb --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll2/TestUntrustedModules_Dll2.cpp @@ -0,0 +1,7 @@ +/* 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> + +BOOL WINAPI DllMain(HINSTANCE, DWORD, LPVOID) { return TRUE; } diff --git a/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll2/moz.build b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll2/moz.build new file mode 100644 index 0000000000..fcefe41329 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll2/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# 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/. + +DIST_INSTALL = False + +SharedLibrary("TestUntrustedModules_Dll2") + +UNIFIED_SOURCES = [ + "TestUntrustedModules_Dll2.cpp", +] + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestUntrustedModules_Dll2.dll"] diff --git a/toolkit/xre/dllservices/tests/gtest/moz.build b/toolkit/xre/dllservices/tests/gtest/moz.build new file mode 100644 index 0000000000..525eba0c38 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/moz.build @@ -0,0 +1,35 @@ +# 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("dllservicestest") + +UNIFIED_SOURCES += [ + "TestDLLBlocklist.cpp", +] + +if CONFIG["CPU_ARCH"] != "x86": + UNIFIED_SOURCES += [ + "TestUntrustedModules.cpp", + ] + +LOCAL_INCLUDES += [ + "/toolkit/components/telemetry/other", + "/toolkit/components/telemetry/tests/gtest", +] + +TEST_DIRS += [ + "TestDllBlocklist_AllowByVersion", + "TestDllBlocklist_MatchByName", + "TestDllBlocklist_MatchByVersion", + "TestDllBlocklist_NoOpEntryPoint", + "TestDllBlocklist_SocketProcessOnly", + "TestDllBlocklist_UserBlocked", + "TestDllBlocklist_UtilityProcessOnly", + "TestUntrustedModules_Dll1", + "TestUntrustedModules_Dll2", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" diff --git a/toolkit/xre/dllservices/tests/moz.build b/toolkit/xre/dllservices/tests/moz.build new file mode 100644 index 0000000000..eee1b22ae3 --- /dev/null +++ b/toolkit/xre/dllservices/tests/moz.build @@ -0,0 +1,46 @@ +# -*- 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/. + +GeckoCppUnitTests( + [ + "TestDllInterceptor", + "TestIATPatcher", + "TestMMPolicy", + ], + linkage=None, +) + +if CONFIG["CPU_ARCH"] in ("x86", "x86_64"): + # Cross-process interceptors not yet supported on aarch64 + GeckoCppUnitTests( + [ + "TestDllInterceptorCrossProcess", + ], + linkage=None, + ) + +OS_LIBS += [ + "advapi32", + "ntdll", + "ole32", + "shlwapi", + "user32", + "uuid", +] + +DELAYLOAD_DLLS += [ + "shlwapi.dll", +] + +if CONFIG["CC_TYPE"] in ("gcc", "clang"): + # This allows us to use wmain as the entry point on mingw + LDFLAGS += [ + "-municode", + ] + +TEST_DIRS += [ + "gtest", +] |