summaryrefslogtreecommitdiffstats
path: root/browser/app/winlauncher/test
diff options
context:
space:
mode:
Diffstat (limited to 'browser/app/winlauncher/test')
-rw-r--r--browser/app/winlauncher/test/TestCrossProcessWin.cpp703
-rw-r--r--browser/app/winlauncher/test/TestSafeThreadLocal.cpp84
-rw-r--r--browser/app/winlauncher/test/TestSameBinary.cpp255
-rw-r--r--browser/app/winlauncher/test/moz.build30
4 files changed, 1072 insertions, 0 deletions
diff --git a/browser/app/winlauncher/test/TestCrossProcessWin.cpp b/browser/app/winlauncher/test/TestCrossProcessWin.cpp
new file mode 100644
index 0000000000..fa212b30c2
--- /dev/null
+++ b/browser/app/winlauncher/test/TestCrossProcessWin.cpp
@@ -0,0 +1,703 @@
+/* -*- 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 <thread>
+#include <winternl.h>
+
+#define MOZ_USE_LAUNCHER_ERROR
+
+#include <atomic>
+#include <thread>
+#include "freestanding/SharedSection.cpp"
+#include "mozilla/CmdLineAndEnvUtils.h"
+#include "mozilla/DynamicBlocklist.h"
+#include "mozilla/NativeNt.h"
+#include "mozilla/Vector.h"
+
+#define DLL_BLOCKLIST_ENTRY(name, ...) \
+ {MOZ_LITERAL_UNICODE_STRING(L##name), __VA_ARGS__},
+#define DLL_BLOCKLIST_STRING_TYPE UNICODE_STRING
+
+#include "mozilla/WindowsDllBlocklistLauncherDefs.h"
+
+const wchar_t kChildArg[] = L"--child";
+const char* kTestDependentModulePaths[] = {
+ "\\Device\\HarddiskVolume4\\Windows\\system32\\A B C",
+ "\\Device\\HarddiskVolume4\\Windows\\system32\\a b c.dll",
+ "\\Device\\HarddiskVolume4\\Windows\\system32\\A B C.exe",
+ "\\Device\\HarddiskVolume4\\Windows\\system32\\X Y Z.dll",
+ "\\Device\\HarddiskVolume1\\a b C",
+ "\\Device\\HarddiskVolume2\\A b c.DLL",
+ "\\Device\\HarddiskVolume3\\A B c.exe",
+ "\\Device\\HarddiskVolume4\\X y Z.dll",
+};
+const wchar_t kExpectedDependentModules[] =
+ L"A B C\0"
+ L"a b c.dll\0"
+ L"A B C.exe\0"
+ L"X Y Z.dll\0";
+
+const UNICODE_STRING kStringNotInBlocklist =
+ MOZ_LITERAL_UNICODE_STRING(L"Test_NotInBlocklist.dll");
+const UNICODE_STRING kTestDependentModuleString =
+ MOZ_LITERAL_UNICODE_STRING(L"Test_DependentModule.dll");
+
+using namespace mozilla;
+using namespace mozilla::freestanding;
+
+// clang-format off
+const DllBlockInfo kDllBlocklistShort[] = {
+ // The entries do not have to be sorted.
+ DLL_BLOCKLIST_ENTRY("X Y Z_Test", MAKE_VERSION(1, 2, 65535, 65535),
+ DllBlockInfoFlags::BLOCK_WIN8_AND_OLDER)
+ DLL_BLOCKLIST_ENTRY("\u30E9\u30FC\u30E1\u30F3_Test")
+ DLL_BLOCKLIST_ENTRY("Avmvirtualsource_Test.ax", MAKE_VERSION(1, 0, 0, 3),
+ DllBlockInfoFlags::BROWSER_PROCESS_ONLY)
+ DLL_BLOCKLIST_ENTRY("1ccelerator_Test.dll", MAKE_VERSION(3, 2, 1, 6))
+ DLL_BLOCKLIST_ENTRY("atkdx11disp_Test.dll", DllBlockInfo::ALL_VERSIONS)
+ {},
+};
+// clang-format on
+
+namespace mozilla::freestanding {
+class SharedSectionTestHelper {
+ public:
+ static constexpr size_t GetModulePathArraySize() {
+ return SharedSection::kSharedViewSize -
+ (offsetof(SharedSection::Layout, mFirstBlockEntry) +
+ sizeof(DllBlockInfo));
+ }
+};
+} // namespace mozilla::freestanding
+
+class TempFile final {
+ wchar_t mFullPath[MAX_PATH + 1];
+
+ public:
+ TempFile() : mFullPath{0} {
+ wchar_t tempDir[MAX_PATH + 1];
+ DWORD len = ::GetTempPathW(ArrayLength(tempDir), tempDir);
+ if (!len) {
+ return;
+ }
+
+ len = ::GetTempFileNameW(tempDir, L"blocklist", 0, mFullPath);
+ if (!len) {
+ return;
+ }
+ }
+
+ operator const wchar_t*() const { return mFullPath[0] ? mFullPath : nullptr; }
+};
+
+template <typename T, int N>
+void PrintLauncherError(const LauncherResult<T>& aResult,
+ const char (&aMsg)[N]) {
+ const LauncherError& err = aResult.inspectErr();
+ printf("TEST-FAILED | TestCrossProcessWin | %s - %lx at %s:%d\n", aMsg,
+ err.mError.AsHResult(), err.mFile, err.mLine);
+}
+
+#define VERIFY_FUNCTION_RESOLVED(mod, exports, name) \
+ do { \
+ if (reinterpret_cast<FARPROC>(exports->m##name) != \
+ ::GetProcAddress(mod, #name)) { \
+ printf( \
+ "TEST-FAILED | TestCrossProcessWin | " \
+ "Kernel32ExportsSolver::" #name " did not match.\n"); \
+ return false; \
+ } \
+ } while (0)
+
+static bool VerifySharedSection(SharedSection& aSharedSection) {
+ Kernel32ExportsSolver* k32Exports = aSharedSection.GetKernel32Exports();
+ if (!k32Exports) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | Failed to map a shared section\n");
+ return false;
+ }
+
+ HMODULE k32mod = ::GetModuleHandleW(L"kernel32.dll");
+ VERIFY_FUNCTION_RESOLVED(k32mod, k32Exports, FlushInstructionCache);
+ VERIFY_FUNCTION_RESOLVED(k32mod, k32Exports, GetModuleHandleW);
+ VERIFY_FUNCTION_RESOLVED(k32mod, k32Exports, GetSystemInfo);
+ VERIFY_FUNCTION_RESOLVED(k32mod, k32Exports, VirtualProtect);
+
+ Span<const wchar_t> modulesArray = aSharedSection.GetDependentModules();
+ bool matched = memcmp(modulesArray.data(), kExpectedDependentModules,
+ sizeof(kExpectedDependentModules)) == 0;
+ if (!matched) {
+ // Print actual strings on error
+ for (const wchar_t* p = modulesArray.data(); *p;) {
+ printf("%p: %S\n", p, p);
+ while (*p) {
+ ++p;
+ }
+ ++p;
+ }
+ return false;
+ }
+
+ for (const DllBlockInfo* info = kDllBlocklistShort; info->mName.Buffer;
+ ++info) {
+ const DllBlockInfo* matched = aSharedSection.SearchBlocklist(info->mName);
+ if (!matched) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | No blocklist entry match for "
+ "entry in blocklist.\n");
+ return false;
+ }
+ }
+
+ if (aSharedSection.SearchBlocklist(kStringNotInBlocklist)) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | Found blocklist entry match for "
+ "something not in the blocklist.\n");
+ }
+
+ if (aSharedSection.IsDisabled()) {
+ printf("TEST-FAILED | TestCrossProcessWin | Wrong disabled value.\n");
+ }
+
+ return true;
+}
+
+static bool TestAddString() {
+ wchar_t testBuffer[3] = {0};
+ UNICODE_STRING ustr;
+
+ // This makes |testBuffer| full.
+ ::RtlInitUnicodeString(&ustr, L"a");
+ if (!AddString(testBuffer, ustr)) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | "
+ "AddString failed.\n");
+ return false;
+ }
+
+ // Adding a string to a full buffer should fail.
+ ::RtlInitUnicodeString(&ustr, L"b");
+ if (AddString(testBuffer, ustr)) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | "
+ "AddString caused OOB memory access.\n");
+ return false;
+ }
+
+ bool matched = memcmp(testBuffer, L"a\0", sizeof(testBuffer)) == 0;
+ if (!matched) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | "
+ "AddString wrote wrong values.\n");
+ return false;
+ }
+
+ return true;
+}
+
+// Convert |aBlockEntries|, which is an array ending with an empty instance
+// of DllBlockInfo, to DynamicBlockList by storing it to a temp file, loading
+// as DynamicBlockList, and deleting the temp file.
+static DynamicBlockList ConvertStaticBlocklistToDynamic(
+ const DllBlockInfo aBlockEntries[]) {
+ size_t originalLength = 0;
+ CheckedUint32 totalStringLen = 0;
+ for (const DllBlockInfo* entry = aBlockEntries; entry->mName.Length;
+ ++entry) {
+ totalStringLen += entry->mName.Length;
+ MOZ_RELEASE_ASSERT(totalStringLen.isValid());
+ ++originalLength;
+ }
+
+ // Pack all strings in this buffer without null characters
+ UniquePtr<uint8_t[]> stringBuffer =
+ MakeUnique<uint8_t[]>(totalStringLen.value());
+
+ // The string buffer is placed immediately after the array of DllBlockInfo
+ const size_t stringBufferOffset = (originalLength + 1) * sizeof(DllBlockInfo);
+
+ // Entries in the dynamic blocklist do have to be sorted,
+ // unlike in the static blocklist.
+ UniquePtr<DllBlockInfo[]> sortedBlockEntries =
+ MakeUnique<DllBlockInfo[]>(originalLength);
+ memcpy(sortedBlockEntries.get(), aBlockEntries,
+ sizeof(DllBlockInfo) * originalLength);
+ std::sort(sortedBlockEntries.get(), sortedBlockEntries.get() + originalLength,
+ [](const DllBlockInfo& a, const DllBlockInfo& b) {
+ return ::RtlCompareUnicodeString(&a.mName, &b.mName, TRUE) < 0;
+ });
+
+ Vector<DllBlockInfo> copied;
+ Unused << copied.resize(originalLength + 1); // aBlockEntries + sentinel
+
+ size_t currentStringOffset = 0;
+ for (size_t i = 0; i < originalLength; ++i) {
+ copied[i].mMaxVersion = sortedBlockEntries[i].mMaxVersion;
+ copied[i].mFlags = sortedBlockEntries[i].mFlags;
+
+ // Copy the module's name to the string buffer and store its offset
+ // in mName.Buffer
+ memcpy(stringBuffer.get() + currentStringOffset,
+ sortedBlockEntries[i].mName.Buffer,
+ sortedBlockEntries[i].mName.Length);
+ copied[i].mName.Buffer =
+ reinterpret_cast<wchar_t*>(stringBufferOffset + currentStringOffset);
+ // Only keep mName.Length and leave mName.MaximumLength to be zero
+ copied[i].mName.Length = sortedBlockEntries[i].mName.Length;
+
+ currentStringOffset += sortedBlockEntries[i].mName.Length;
+ }
+
+ TempFile blocklistFile;
+ nsAutoHandle file(::CreateFileW(blocklistFile, GENERIC_WRITE, FILE_SHARE_READ,
+ nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,
+ nullptr));
+ MOZ_RELEASE_ASSERT(file);
+
+ DynamicBlockListBase::FileHeader header;
+ header.mSignature = DynamicBlockListBase::kSignature;
+ header.mFileVersion = DynamicBlockListBase::kCurrentVersion;
+ header.mPayloadSize =
+ sizeof(DllBlockInfo) * copied.length() + totalStringLen.value();
+
+ DWORD written = 0;
+ MOZ_RELEASE_ASSERT(
+ ::WriteFile(file.get(), &header, sizeof(header), &written, nullptr));
+ MOZ_RELEASE_ASSERT(::WriteFile(file.get(), copied.begin(),
+ sizeof(DllBlockInfo) * copied.length(),
+ &written, nullptr));
+ MOZ_RELEASE_ASSERT(::WriteFile(file.get(), stringBuffer.get(),
+ totalStringLen.value(), &written, nullptr));
+
+ DynamicBlockList blockList(blocklistFile);
+ ::DeleteFileW(blocklistFile);
+ return blockList;
+}
+
+const DynamicBlockList gFullList =
+ ConvertStaticBlocklistToDynamic(gWindowsDllBlocklist);
+const DynamicBlockList gShortList =
+ ConvertStaticBlocklistToDynamic(kDllBlocklistShort);
+
+static bool TestDependentModules() {
+ LauncherVoidResult result = gSharedSection.Init();
+ if (result.isErr()) {
+ PrintLauncherError(result, "SharedSection::Init failed");
+ return false;
+ }
+
+ constexpr size_t sizeInBytes =
+ SharedSectionTestHelper::GetModulePathArraySize();
+ UniquePtr<uint8_t[]> bufferData = MakeUnique<uint8_t[]>(sizeInBytes);
+ Span<uint8_t> buffer(bufferData, sizeInBytes);
+ memset(buffer.data(), 0x88, buffer.size());
+
+ // Try to add a long string that does not fit in the section,
+ // since there's no room for the NULL character to indicate the final string.
+ UNICODE_STRING ustr;
+ ustr.Buffer = reinterpret_cast<wchar_t*>(buffer.data());
+ ustr.Length = ustr.MaximumLength = buffer.size();
+
+ result = gSharedSection.AddDependentModule(&ustr);
+ if (result.isOk() || result.inspectErr() != WindowsError::FromWin32Error(
+ ERROR_INSUFFICIENT_BUFFER)) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | "
+ "Adding a too long string should fail.\n");
+ return false;
+ }
+
+ result = gSharedSection.Init();
+ if (result.isErr()) {
+ PrintLauncherError(result, "SharedSection::Init failed");
+ return false;
+ }
+
+ // Keep adding a single-char string until it fails and
+ // make sure no crash.
+ // We want to make sure no strings match any earlier strings so
+ // we can get the expected count. This is a little tricky since
+ // it includes case-insensitivity, so start at the "CJK Unified Ideographs
+ // Extension A" block of Unicode, which has no two characters that compare
+ // equal under a case insensitive comparison.
+ *(reinterpret_cast<wchar_t*>(buffer.data())) = 0x3400;
+ ustr.Length = ustr.MaximumLength = sizeof(wchar_t);
+ wchar_t numberOfStringsAdded = 0;
+ while (gSharedSection.AddDependentModule(&ustr).isOk()) {
+ ++numberOfStringsAdded;
+ // Make sure the string doesn't match any earlier strings
+ wchar_t oldValue = *(reinterpret_cast<wchar_t*>(buffer.data()));
+ *(reinterpret_cast<wchar_t*>(buffer.data())) = oldValue + 1;
+ }
+
+ int numberOfCharactersInBuffer =
+ SharedSectionTestHelper::GetModulePathArraySize() / sizeof(wchar_t);
+ // Each string is two characters long (one "real" character and a null), but
+ // the whole buffer needs an additional null at the end.
+ int expectedNumberOfStringsAdded = (numberOfCharactersInBuffer - 1) / 2;
+ if (numberOfStringsAdded != expectedNumberOfStringsAdded) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | "
+ "Added %d dependent strings before failing (expected %d).\n",
+ static_cast<int>(numberOfStringsAdded), expectedNumberOfStringsAdded);
+ return false;
+ }
+
+ // SetBlocklist is not allowed after AddDependentModule
+ result = gSharedSection.SetBlocklist(gShortList, false);
+ if (result.isOk() || result.inspectErr() !=
+ WindowsError::FromWin32Error(ERROR_INVALID_STATE)) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | "
+ "SetBlocklist is not allowed after AddDependentModule\n");
+ return false;
+ }
+
+ gSharedSection.Reset();
+ return true;
+}
+
+static bool TestDynamicBlocklist() {
+ if (!gFullList.GetPayloadSize() || !gShortList.GetPayloadSize()) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | DynamicBlockList::LoadFile "
+ "failed\n");
+ return false;
+ }
+
+ LauncherVoidResult result = gSharedSection.Init();
+ if (result.isErr()) {
+ PrintLauncherError(result, "SharedSection::Init failed");
+ return false;
+ }
+
+ // Set gShortList, and gShortList
+ // 1. Setting gShortList succeeds
+ // 2. Next try to set gShortList fails
+ result = gSharedSection.SetBlocklist(gShortList, false);
+ if (result.isErr()) {
+ PrintLauncherError(result, "SetBlocklist(gShortList) failed");
+ return false;
+ }
+ result = gSharedSection.SetBlocklist(gShortList, false);
+ if (result.isOk() || result.inspectErr() !=
+ WindowsError::FromWin32Error(ERROR_INVALID_STATE)) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | "
+ "SetBlocklist is allowed only once\n");
+ return false;
+ }
+
+ result = gSharedSection.Init();
+ if (result.isErr()) {
+ PrintLauncherError(result, "SharedSection::Init failed");
+ return false;
+ }
+
+ // Add gFullList and gShortList
+ // 1. Adding gFullList always fails because it doesn't fit the section
+ // 2. Adding gShortList succeeds because no entry is added yet
+ MOZ_RELEASE_ASSERT(
+ gFullList.GetPayloadSize() >
+ SharedSectionTestHelper::GetModulePathArraySize(),
+ "Test assumes gFullList is too big to fit in shared section");
+ result = gSharedSection.SetBlocklist(gFullList, false);
+ if (result.isOk() || result.inspectErr() != WindowsError::FromWin32Error(
+ ERROR_INSUFFICIENT_BUFFER)) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | "
+ "SetBlocklist(gFullList) should fail\n");
+ return false;
+ }
+ result = gSharedSection.SetBlocklist(gShortList, false);
+ if (result.isErr()) {
+ PrintLauncherError(result, "SetBlocklist(gShortList) failed");
+ return false;
+ }
+
+ // AddDependentModule is allowed after SetBlocklist
+ result = gSharedSection.AddDependentModule(&kTestDependentModuleString);
+ if (result.isErr()) {
+ PrintLauncherError(result, "SharedSection::AddDependentModule failed");
+ return false;
+ }
+
+ gSharedSection.Reset();
+ return true;
+}
+
+class ChildProcess final {
+ nsAutoHandle mChildProcess;
+ nsAutoHandle mChildMainThread;
+ DWORD mProcessId;
+
+ public:
+ // The following variables are updated from the parent process via
+ // WriteProcessMemory while the process is suspended as a part of
+ // TestWithChildProcess().
+ //
+ // Having both a non-const and a const is important because a constant
+ // is separately placed in the .rdata section which is read-only, so
+ // the region's attribute needs to be changed before modifying data via
+ // WriteProcessMemory.
+ // The keyword "volatile" is needed for a constant, otherwise the compiler
+ // evaluates a constant as a literal without fetching data from memory.
+ static HMODULE sExecutableImageBase;
+ static volatile const DWORD sReadOnlyProcessId;
+
+ static int Main() {
+ SRWLOCK lock = SRWLOCK_INIT;
+ ::AcquireSRWLockExclusive(&lock);
+
+ Vector<std::thread> threads;
+ std::atomic<bool> success = true;
+ for (int i = 0; i < 10; ++i) {
+ Unused << threads.emplaceBack(
+ [&success](SRWLOCK* aLock) {
+ // All threads call GetKernel32Exports(), but only the first thread
+ // maps a write-copy section and populates it.
+ ::AcquireSRWLockShared(aLock);
+ if (gSharedSection.GetKernel32Exports() == nullptr) {
+ success = false;
+ }
+ ::ReleaseSRWLockShared(aLock);
+ },
+ &lock);
+ }
+
+ // Wait a msec for all threads to be ready and release the lock
+ ::Sleep(1);
+ ::ReleaseSRWLockExclusive(&lock);
+
+ for (auto& thread : threads) {
+ thread.join();
+ }
+
+ if (!success) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | "
+ "GetKernel32Exports() returned null.\n");
+ return 1;
+ }
+
+ if (sExecutableImageBase != ::GetModuleHandle(nullptr)) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | "
+ "sExecutableImageBase is expected to be %p, but actually was %p.\n",
+ ::GetModuleHandle(nullptr), sExecutableImageBase);
+ return 1;
+ }
+
+ if (sReadOnlyProcessId != ::GetCurrentProcessId()) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | "
+ "sReadOnlyProcessId is expected to be %lx, but actually was %lx.\n",
+ ::GetCurrentProcessId(), sReadOnlyProcessId);
+ return 1;
+ }
+
+ if (!VerifySharedSection(gSharedSection)) {
+ return 1;
+ }
+
+ // Test a scenario to transfer a transferred section as a readonly handle
+ gSharedSection.ConvertToReadOnly();
+
+ // AddDependentModule fails as the handle is readonly.
+ LauncherVoidResult result =
+ gSharedSection.AddDependentModule(&kTestDependentModuleString);
+ if (result.inspectErr() !=
+ WindowsError::FromWin32Error(ERROR_ACCESS_DENIED)) {
+ PrintLauncherError(result, "The readonly section was writable");
+ return 1;
+ }
+
+ if (!VerifySharedSection(gSharedSection)) {
+ return 1;
+ }
+
+ return 0;
+ }
+
+ ChildProcess(const wchar_t* aExecutable, const wchar_t* aOption)
+ : mProcessId(0) {
+ const wchar_t* childArgv[] = {aExecutable, aOption};
+ auto cmdLine(
+ mozilla::MakeCommandLine(mozilla::ArrayLength(childArgv), childArgv));
+
+ STARTUPINFOW si = {sizeof(si)};
+ PROCESS_INFORMATION pi;
+ BOOL ok =
+ ::CreateProcessW(aExecutable, cmdLine.get(), nullptr, nullptr, FALSE,
+ CREATE_SUSPENDED, nullptr, nullptr, &si, &pi);
+ if (!ok) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | "
+ "CreateProcessW falied - %08lx.\n",
+ GetLastError());
+ return;
+ }
+
+ mProcessId = pi.dwProcessId;
+
+ mChildProcess.own(pi.hProcess);
+ mChildMainThread.own(pi.hThread);
+ }
+
+ ~ChildProcess() { ::TerminateProcess(mChildProcess, 0); }
+
+ operator HANDLE() const { return mChildProcess; }
+ DWORD GetProcessId() const { return mProcessId; }
+
+ bool ResumeAndWaitUntilExit() {
+ if (::ResumeThread(mChildMainThread) == 0xffffffff) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | "
+ "ResumeThread failed - %08lx\n",
+ GetLastError());
+ return false;
+ }
+
+ if (::WaitForSingleObject(mChildProcess, 60000) != WAIT_OBJECT_0) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | "
+ "Unexpected result from WaitForSingleObject\n");
+ return false;
+ }
+
+ DWORD exitCode;
+ if (!::GetExitCodeProcess(mChildProcess, &exitCode)) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | "
+ "GetExitCodeProcess failed - %08lx\n",
+ GetLastError());
+ return false;
+ }
+
+ return exitCode == 0;
+ }
+};
+
+HMODULE ChildProcess::sExecutableImageBase = 0;
+volatile const DWORD ChildProcess::sReadOnlyProcessId = 0;
+
+int wmain(int argc, wchar_t* argv[]) {
+ printf("Process: %-8lx Base: %p\n", ::GetCurrentProcessId(),
+ ::GetModuleHandle(nullptr));
+
+ if (argc == 2 && wcscmp(argv[1], kChildArg) == 0) {
+ return ChildProcess::Main();
+ }
+
+ ChildProcess childProcess(argv[0], kChildArg);
+ if (!childProcess) {
+ return 1;
+ }
+
+ if (!TestAddString()) {
+ return 1;
+ }
+
+ if (!TestDependentModules()) {
+ return 1;
+ }
+
+ if (!TestDynamicBlocklist()) {
+ return 1;
+ }
+
+ LauncherResult<HMODULE> remoteImageBase =
+ nt::GetProcessExeModule(childProcess);
+ if (remoteImageBase.isErr()) {
+ PrintLauncherError(remoteImageBase, "nt::GetProcessExeModule failed");
+ return 1;
+ }
+
+ nt::CrossExecTransferManager transferMgr(childProcess);
+ if (!transferMgr) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | "
+ "CrossExecTransferManager instantiation failed.\n");
+ return 1;
+ }
+
+ LauncherVoidResult result =
+ transferMgr.Transfer(&ChildProcess::sExecutableImageBase,
+ &remoteImageBase.inspect(), sizeof(HMODULE));
+ if (result.isErr()) {
+ PrintLauncherError(result, "ChildProcess::WriteData(Imagebase) failed");
+ return 1;
+ }
+
+ DWORD childPid = childProcess.GetProcessId();
+
+ DWORD* readOnlyData = const_cast<DWORD*>(&ChildProcess::sReadOnlyProcessId);
+ result = transferMgr.Transfer(readOnlyData, &childPid, sizeof(DWORD));
+ if (result.isOk()) {
+ printf(
+ "TEST-UNEXPECTED | TestCrossProcessWin | "
+ "A constant was located in a writable section.");
+ return 1;
+ }
+
+ AutoVirtualProtect prot =
+ transferMgr.Protect(readOnlyData, sizeof(uint32_t), PAGE_READWRITE);
+ if (!prot) {
+ printf(
+ "TEST-FAILED | TestCrossProcessWin | "
+ "VirtualProtect failed - %08lx\n",
+ prot.GetError().AsHResult());
+ return 1;
+ }
+
+ result = transferMgr.Transfer(readOnlyData, &childPid, sizeof(DWORD));
+ if (result.isErr()) {
+ PrintLauncherError(result, "ChildProcess::WriteData(PID) failed");
+ return 1;
+ }
+
+ result = gSharedSection.Init();
+ if (result.isErr()) {
+ PrintLauncherError(result, "SharedSection::Init failed");
+ return 1;
+ }
+
+ result = gSharedSection.SetBlocklist(gShortList, false);
+ if (result.isErr()) {
+ PrintLauncherError(result, "SetBlocklist(gShortList) failed");
+ return false;
+ }
+
+ for (const char* testString : kTestDependentModulePaths) {
+ // Test AllocatedUnicodeString(const char*) that is used
+ // in IsDependentModule()
+ nt::AllocatedUnicodeString depModule(testString);
+ UNICODE_STRING depModuleLeafName;
+ nt::GetLeafName(&depModuleLeafName, depModule);
+ result = gSharedSection.AddDependentModule(&depModuleLeafName);
+ if (result.isErr()) {
+ PrintLauncherError(result, "SharedSection::AddDependentModule failed");
+ return 1;
+ }
+ }
+
+ result =
+ gSharedSection.TransferHandle(transferMgr, GENERIC_READ | GENERIC_WRITE);
+ if (result.isErr()) {
+ PrintLauncherError(result, "SharedSection::TransferHandle failed");
+ return 1;
+ }
+
+ // Close the section in the parent process before resuming the child process
+ gSharedSection.Reset(nullptr);
+
+ if (!childProcess.ResumeAndWaitUntilExit()) {
+ return 1;
+ }
+
+ printf("TEST-PASS | TestCrossProcessWin | All checks passed\n");
+ return 0;
+}
diff --git a/browser/app/winlauncher/test/TestSafeThreadLocal.cpp b/browser/app/winlauncher/test/TestSafeThreadLocal.cpp
new file mode 100644
index 0000000000..31af93c375
--- /dev/null
+++ b/browser/app/winlauncher/test/TestSafeThreadLocal.cpp
@@ -0,0 +1,84 @@
+/* -*- 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/. */
+
+#define MOZ_USE_LAUNCHER_ERROR
+
+#include "freestanding/SafeThreadLocal.h"
+
+#include "mozilla/NativeNt.h"
+#include "nsWindowsHelpers.h"
+
+#include <process.h>
+#include <stdio.h>
+
+// Need a non-inline function to bypass compiler optimization that the thread
+// local storage pointer is cached in a register before accessing a thread-local
+// variable.
+MOZ_NEVER_INLINE PVOID SwapThreadLocalStoragePointer(PVOID aNewValue) {
+ auto oldValue = mozilla::nt::RtlGetThreadLocalStoragePointer();
+ mozilla::nt::RtlSetThreadLocalStoragePointerForTestingOnly(aNewValue);
+ return oldValue;
+}
+
+static mozilla::freestanding::SafeThreadLocal<int*> gTheStorage;
+
+// Need non-inline functions to bypass compiler optimization that the thread
+// local storage pointer is cached in a register before accessing a thread-local
+// variable. See bug 1803322 for a motivating example.
+MOZ_NEVER_INLINE int* getTheStorage() { return gTheStorage.get(); }
+MOZ_NEVER_INLINE void setTheStorage(int* p) { gTheStorage.set(p); }
+
+static unsigned int __stdcall TestNonMainThread(void* aArg) {
+ for (int i = 0; i < 100; ++i) {
+ setTheStorage(&i);
+ if (getTheStorage() != &i) {
+ printf(
+ "TEST-FAILED | TestSafeThreadLocal | "
+ "A value is not correctly stored in the thread-local storage.\n");
+ return 1;
+ }
+ }
+ return 0;
+}
+
+extern "C" int wmain(int argc, wchar_t* argv[]) {
+ int dummy = 0x1234;
+
+ auto origHead = SwapThreadLocalStoragePointer(nullptr);
+ // Setting gTheStorage when TLS is null.
+ setTheStorage(&dummy);
+ SwapThreadLocalStoragePointer(origHead);
+
+ nsAutoHandle handles[8];
+ for (auto& handle : handles) {
+ handle.own(reinterpret_cast<HANDLE>(
+ ::_beginthreadex(nullptr, 0, TestNonMainThread, nullptr, 0, nullptr)));
+ }
+
+ for (int i = 0; i < 100; ++i) {
+ if (getTheStorage() != &dummy) {
+ printf(
+ "TEST-FAILED | TestSafeThreadLocal | "
+ "A value is not correctly stored in the global scope.\n");
+ return 1;
+ }
+ }
+
+ for (auto& handle : handles) {
+ ::WaitForSingleObject(handle, INFINITE);
+
+#if !defined(MOZ_ASAN)
+ // ASAN builds under Windows 11 can have unexpected thread exit codes.
+ // See bug 1798796
+ DWORD exitCode;
+ if (!::GetExitCodeThread(handle, &exitCode) || exitCode) {
+ return 1;
+ }
+#endif // !defined(MOZ_ASAN)
+ }
+
+ return 0;
+}
diff --git a/browser/app/winlauncher/test/TestSameBinary.cpp b/browser/app/winlauncher/test/TestSameBinary.cpp
new file mode 100644
index 0000000000..2cb45f546f
--- /dev/null
+++ b/browser/app/winlauncher/test/TestSameBinary.cpp
@@ -0,0 +1,255 @@
+/* -*- 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/. */
+
+#define MOZ_USE_LAUNCHER_ERROR
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <utility>
+
+#include "SameBinary.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/CmdLineAndEnvUtils.h"
+#include "mozilla/NativeNt.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Vector.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+#include "nsWindowsHelpers.h"
+
+#define EXPECT_SAMEBINARY_IS(expected, option, message) \
+ do { \
+ mozilla::LauncherResult<bool> isSame = \
+ mozilla::IsSameBinaryAsParentProcess(option); \
+ if (isSame.isErr()) { \
+ PrintLauncherError(isSame, \
+ "IsSameBinaryAsParentProcess returned error " \
+ "when we were expecting success."); \
+ return 1; \
+ } \
+ if (isSame.unwrap() != expected) { \
+ PrintErrorMsg(message); \
+ return 1; \
+ } \
+ } while (0)
+
+/**
+ * This test involves three processes:
+ * 1. The "Monitor" process, which is executed by |MonitorMain|. This process
+ * is responsible for integrating with the test harness, so it spawns the
+ * "Parent" process (2), and then waits for the other two processes to
+ * finish.
+ * 2. The "Parent" process, which is executed by |ParentMain|. This process
+ * creates the "Child" process (3) and then waits indefinitely.
+ * 3. The "Child" process, which is executed by |ChildMain| and carries out
+ * the actual test. It terminates the Parent process during its execution,
+ * using the Child PID as the Parent process's exit code. This serves as a
+ * hacky yet effective way to signal to the Monitor process which PID it
+ * should wait on to ensure that the Child process has exited.
+ */
+
+static const char kMsgStart[] = "TEST-FAILED | SameBinary | ";
+
+inline void PrintErrorMsg(const char* aMsg) {
+ printf("%s%s\n", kMsgStart, aMsg);
+}
+
+inline void PrintWinError(const char* aMsg) {
+ mozilla::WindowsError err(mozilla::WindowsError::FromLastError());
+ printf("%s%s: %S\n", kMsgStart, aMsg, err.AsString().get());
+}
+
+template <typename T>
+inline void PrintLauncherError(const mozilla::LauncherResult<T>& aResult,
+ const char* aMsg = nullptr) {
+ const char* const kSep = aMsg ? ": " : "";
+ const char* msg = aMsg ? aMsg : "";
+ const mozilla::LauncherError& err = aResult.inspectErr();
+ printf("%s%s%s%S (%s:%d)\n", kMsgStart, msg, kSep,
+ err.mError.AsString().get(), err.mFile, err.mLine);
+}
+
+static int ChildMain(DWORD aExpectedParentPid) {
+ mozilla::LauncherResult<DWORD> parentPid = mozilla::nt::GetParentProcessId();
+ if (parentPid.isErr()) {
+ PrintLauncherError(parentPid);
+ return 1;
+ }
+
+ if (parentPid.inspect() != aExpectedParentPid) {
+ PrintErrorMsg("Unexpected mismatch of parent PIDs");
+ return 1;
+ }
+
+ const DWORD kAccess = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE;
+ nsAutoHandle parentProcess(
+ ::OpenProcess(kAccess, FALSE, parentPid.inspect()));
+ if (!parentProcess) {
+ PrintWinError("Unexpectedly failed to call OpenProcess on parent");
+ return 1;
+ }
+
+ EXPECT_SAMEBINARY_IS(
+ true, mozilla::ImageFileCompareOption::Default,
+ "IsSameBinaryAsParentProcess returned incorrect result for identical "
+ "binaries");
+ EXPECT_SAMEBINARY_IS(
+ true, mozilla::ImageFileCompareOption::CompareNtPathsOnly,
+ "IsSameBinaryAsParentProcess(CompareNtPathsOnly) returned incorrect "
+ "result for identical binaries");
+
+ // Total hack, but who cares? We'll set the parent's exit code as our PID
+ // so that the monitor process knows who to wait for!
+ if (!::TerminateProcess(parentProcess.get(), ::GetCurrentProcessId())) {
+ PrintWinError("Unexpected failure in TerminateProcess");
+ return 1;
+ }
+
+ // Close our handle to the parent process so that no references are held.
+ ::CloseHandle(parentProcess.disown());
+
+ // Querying a pid on a terminated process may still succeed some time after
+ // that process has been terminated. For the purposes of this test, we'll poll
+ // the OS until we cannot succesfully open the parentPid anymore.
+ const uint32_t kMaxAttempts = 100;
+ uint32_t curAttempt = 0;
+ while (HANDLE p = ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE,
+ parentPid.inspect())) {
+ ::CloseHandle(p);
+ ::Sleep(100);
+ ++curAttempt;
+ if (curAttempt >= kMaxAttempts) {
+ PrintErrorMsg(
+ "Exhausted retry attempts waiting for parent pid to become invalid");
+ return 1;
+ }
+ }
+
+ EXPECT_SAMEBINARY_IS(
+ false, mozilla::ImageFileCompareOption::Default,
+ "IsSameBinaryAsParentProcess returned incorrect result for dead parent "
+ "process");
+ EXPECT_SAMEBINARY_IS(
+ false, mozilla::ImageFileCompareOption::CompareNtPathsOnly,
+ "IsSameBinaryAsParentProcess(CompareNtPathsOnly) returned incorrect "
+ "result for dead parent process");
+
+ return 0;
+}
+
+static nsReturnRef<HANDLE> CreateSelfProcess(int argc, wchar_t* argv[]) {
+ nsAutoHandle empty;
+
+ DWORD myPid = ::GetCurrentProcessId();
+
+ wchar_t strPid[11] = {};
+#if defined(__MINGW32__)
+ _ultow(myPid, strPid, 16);
+#else
+ if (_ultow_s(myPid, strPid, 16)) {
+ PrintErrorMsg("_ultow_s failed");
+ return empty.out();
+ }
+#endif // defined(__MINGW32__)
+
+ wchar_t* extraArgs[] = {strPid};
+
+ auto cmdLine = mozilla::MakeCommandLine(
+ argc, argv, mozilla::ArrayLength(extraArgs), extraArgs);
+ if (!cmdLine) {
+ PrintErrorMsg("MakeCommandLine failed");
+ return empty.out();
+ }
+
+ STARTUPINFOW si = {sizeof(si)};
+ PROCESS_INFORMATION pi;
+ BOOL ok =
+ ::CreateProcessW(argv[0], cmdLine.get(), nullptr, nullptr, FALSE,
+ CREATE_UNICODE_ENVIRONMENT, nullptr, nullptr, &si, &pi);
+ if (!ok) {
+ PrintWinError("CreateProcess failed");
+ return empty.out();
+ }
+
+ nsAutoHandle proc(pi.hProcess);
+ nsAutoHandle thd(pi.hThread);
+
+ return proc.out();
+}
+
+static int ParentMain(int argc, wchar_t* argv[]) {
+ nsAutoHandle childProc(CreateSelfProcess(argc, argv));
+ if (!childProc) {
+ return 1;
+ }
+
+ if (::WaitForSingleObject(childProc.get(), INFINITE) != WAIT_OBJECT_0) {
+ PrintWinError(
+ "Unexpected result from WaitForSingleObject on child process");
+ return 1;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("This process should be terminated by now");
+ return 0;
+}
+
+static int MonitorMain(int argc, wchar_t* argv[]) {
+ // In this process, "parent" means the process that will be running
+ // ParentMain, which is our child process (confusing, I know...)
+ nsAutoHandle parentProc(CreateSelfProcess(argc, argv));
+ if (!parentProc) {
+ return 1;
+ }
+
+ if (::WaitForSingleObject(parentProc.get(), 60000) != WAIT_OBJECT_0) {
+ PrintWinError("Unexpected result from WaitForSingleObject on parent");
+ return 1;
+ }
+
+ DWORD childPid;
+ if (!::GetExitCodeProcess(parentProc.get(), &childPid)) {
+ PrintWinError("GetExitCodeProcess failed");
+ return 1;
+ }
+
+ nsAutoHandle childProc(::OpenProcess(SYNCHRONIZE, FALSE, childPid));
+ if (!childProc) {
+ // Nothing to wait on anymore, which is OK.
+ return 0;
+ }
+
+ // We want no more references to parentProc
+ ::CloseHandle(parentProc.disown());
+
+ if (::WaitForSingleObject(childProc.get(), 60000) != WAIT_OBJECT_0) {
+ PrintWinError("Unexpected result from WaitForSingleObject on child");
+ return 1;
+ }
+
+ return 0;
+}
+
+extern "C" int wmain(int argc, wchar_t* argv[]) {
+ if (argc == 3) {
+ return ChildMain(wcstoul(argv[2], nullptr, 16));
+ }
+
+ if (!mozilla::SetArgv0ToFullBinaryPath(argv)) {
+ return 1;
+ }
+
+ if (argc == 1) {
+ return MonitorMain(argc, argv);
+ }
+
+ if (argc == 2) {
+ return ParentMain(argc, argv);
+ }
+
+ PrintErrorMsg("Unexpected argc");
+ return 1;
+}
diff --git a/browser/app/winlauncher/test/moz.build b/browser/app/winlauncher/test/moz.build
new file mode 100644
index 0000000000..a8503ddf55
--- /dev/null
+++ b/browser/app/winlauncher/test/moz.build
@@ -0,0 +1,30 @@
+# -*- 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/.
+
+DisableStlWrapping()
+
+GeckoCppUnitTests(
+ [
+ "TestCrossProcessWin",
+ "TestSafeThreadLocal",
+ "TestSameBinary",
+ ],
+ linkage=None,
+)
+
+LOCAL_INCLUDES += [
+ "/browser/app/winlauncher",
+]
+
+OS_LIBS += [
+ "ntdll",
+]
+
+if CONFIG["CC_TYPE"] in ("gcc", "clang"):
+ # This allows us to use wmain as the entry point on mingw
+ LDFLAGS += [
+ "-municode",
+ ]