diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /toolkit/xre | |
parent | Initial commit. (diff) | |
download | firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/xre')
213 files changed, 43969 insertions, 0 deletions
diff --git a/toolkit/xre/AssembleCmdLine.h b/toolkit/xre/AssembleCmdLine.h new file mode 100644 index 0000000000..d969fedfed --- /dev/null +++ b/toolkit/xre/AssembleCmdLine.h @@ -0,0 +1,139 @@ +/* -*- 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/. */ + +#ifndef mozilla_AssembleCmdLine_h +#define mozilla_AssembleCmdLine_h + +#if defined(XP_WIN) + +# include "mozilla/UniquePtr.h" + +# include <stdlib.h> +# include <windows.h> + +# ifdef MOZILLA_INTERNAL_API +# include "nsString.h" +# endif // MOZILLA_INTERNAL_API + +namespace mozilla { + +// Out param `aWideCmdLine` must be free()d by the caller. +inline int assembleCmdLine(const char* const* aArgv, wchar_t** aWideCmdLine, + UINT aCodePage) { + const char* const* arg; + char* p; + const char* q; + char* cmdLine; + int cmdLineSize; + int numBackslashes; + int i; + int argNeedQuotes; + + /* + * Find out how large the command line buffer should be. + */ + cmdLineSize = 0; + for (arg = aArgv; *arg; ++arg) { + /* + * \ and " need to be escaped by a \. In the worst case, + * every character is a \ or ", so the string of length + * may double. If we quote an argument, that needs two ". + * Finally, we need a space between arguments, and + * a null byte at the end of command line. + */ + cmdLineSize += 2 * strlen(*arg) /* \ and " need to be escaped */ + + 2 /* we quote every argument */ + + 1; /* space in between, or final null */ + } + p = cmdLine = (char*)malloc(cmdLineSize * sizeof(char)); + if (!p) { + return -1; + } + + for (arg = aArgv; *arg; ++arg) { + /* Add a space to separates the arguments */ + if (arg != aArgv) { + *p++ = ' '; + } + q = *arg; + numBackslashes = 0; + argNeedQuotes = 0; + + /* + * If the argument is empty or contains white space, it needs to + * be quoted. + */ + if (**arg == '\0' || strpbrk(*arg, " \f\n\r\t\v")) { + argNeedQuotes = 1; + } + + if (argNeedQuotes) { + *p++ = '"'; + } + while (*q) { + if (*q == '\\') { + numBackslashes++; + q++; + } else if (*q == '"') { + if (numBackslashes) { + /* + * Double the backslashes since they are followed + * by a quote + */ + for (i = 0; i < 2 * numBackslashes; i++) { + *p++ = '\\'; + } + numBackslashes = 0; + } + /* To escape the quote */ + *p++ = '\\'; + *p++ = *q++; + } else { + if (numBackslashes) { + /* + * Backslashes are not followed by a quote, so + * don't need to double the backslashes. + */ + for (i = 0; i < numBackslashes; i++) { + *p++ = '\\'; + } + numBackslashes = 0; + } + *p++ = *q++; + } + } + + /* Now we are at the end of this argument */ + if (numBackslashes) { + /* + * Double the backslashes if we have a quote string + * delimiter at the end. + */ + if (argNeedQuotes) { + numBackslashes *= 2; + } + for (i = 0; i < numBackslashes; i++) { + *p++ = '\\'; + } + } + if (argNeedQuotes) { + *p++ = '"'; + } + } + + *p = '\0'; + int numChars = MultiByteToWideChar(aCodePage, 0, cmdLine, -1, nullptr, 0); + *aWideCmdLine = (wchar_t*)malloc(numChars * sizeof(wchar_t)); + MultiByteToWideChar(aCodePage, 0, cmdLine, -1, *aWideCmdLine, numChars); + free(cmdLine); + return 0; +} + +} // namespace mozilla + +#endif // defined(XP_WIN) + +#endif // mozilla_AssembleCmdLine_h diff --git a/toolkit/xre/AutoSQLiteLifetime.cpp b/toolkit/xre/AutoSQLiteLifetime.cpp new file mode 100644 index 0000000000..4500ec1bfe --- /dev/null +++ b/toolkit/xre/AutoSQLiteLifetime.cpp @@ -0,0 +1,156 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsDebug.h" +#include "AutoSQLiteLifetime.h" +#include "sqlite3.h" + +#ifdef MOZ_MEMORY +# include "mozmemory.h" +# ifdef MOZ_DMD +# include "nsIMemoryReporter.h" +# include "DMD.h" + +namespace mozilla { +namespace storage { +extern mozilla::Atomic<size_t> gSqliteMemoryUsed; +} +} // namespace mozilla + +using mozilla::storage::gSqliteMemoryUsed; + +# endif + +namespace { + +// By default, SQLite tracks the size of all its heap blocks by adding an extra +// 8 bytes at the start of the block to hold the size. Unfortunately, this +// causes a lot of 2^N-sized allocations to be rounded up by jemalloc +// allocator, wasting memory. For example, a request for 1024 bytes has 8 +// bytes added, becoming a request for 1032 bytes, and jemalloc rounds this up +// to 2048 bytes, wasting 1012 bytes. (See bug 676189 for more details.) +// +// So we register jemalloc as the malloc implementation, which avoids this +// 8-byte overhead, and thus a lot of waste. This requires us to provide a +// function, sqliteMemRoundup(), which computes the actual size that will be +// allocated for a given request. SQLite uses this function before all +// allocations, and may be able to use any excess bytes caused by the rounding. +// +// Note: the wrappers for malloc, realloc and moz_malloc_usable_size are +// necessary because the sqlite_mem_methods type signatures differ slightly +// from the standard ones -- they use int instead of size_t. But we don't need +// a wrapper for free. + +# ifdef MOZ_DMD + +// sqlite does its own memory accounting, and we use its numbers in our memory +// reporters. But we don't want sqlite's heap blocks to show up in DMD's +// output as unreported, so we mark them as reported when they're allocated and +// mark them as unreported when they are freed. +// +// In other words, we are marking all sqlite heap blocks as reported even +// though we're not reporting them ourselves. Instead we're trusting that +// sqlite is fully and correctly accounting for all of its heap blocks via its +// own memory accounting. Well, we don't have to trust it entirely, because +// it's easy to keep track (while doing this DMD-specific marking) of exactly +// how much memory SQLite is using. And we can compare that against what +// SQLite reports it is using. + +MOZ_DEFINE_MALLOC_SIZE_OF_ON_ALLOC(SqliteMallocSizeOfOnAlloc) +MOZ_DEFINE_MALLOC_SIZE_OF_ON_FREE(SqliteMallocSizeOfOnFree) + +# endif + +static void* sqliteMemMalloc(int n) { + void* p = ::malloc(n); +# ifdef MOZ_DMD + gSqliteMemoryUsed += SqliteMallocSizeOfOnAlloc(p); +# endif + return p; +} + +static void sqliteMemFree(void* p) { +# ifdef MOZ_DMD + gSqliteMemoryUsed -= SqliteMallocSizeOfOnFree(p); +# endif + ::free(p); +} + +static void* sqliteMemRealloc(void* p, int n) { +# ifdef MOZ_DMD + gSqliteMemoryUsed -= SqliteMallocSizeOfOnFree(p); + void* pnew = ::realloc(p, n); + if (pnew) { + gSqliteMemoryUsed += SqliteMallocSizeOfOnAlloc(pnew); + } else { + // realloc failed; undo the SqliteMallocSizeOfOnFree from above + gSqliteMemoryUsed += SqliteMallocSizeOfOnAlloc(p); + } + return pnew; +# else + return ::realloc(p, n); +# endif +} + +static int sqliteMemSize(void* p) { return ::moz_malloc_usable_size(p); } + +static int sqliteMemRoundup(int n) { + n = malloc_good_size(n); + + // jemalloc can return blocks of size 2 and 4, but SQLite requires that all + // allocations be 8-aligned. So we round up sub-8 requests to 8. This + // wastes a small amount of memory but is obviously safe. + return n <= 8 ? 8 : n; +} + +static int sqliteMemInit(void* p) { return 0; } + +static void sqliteMemShutdown(void* p) {} + +const sqlite3_mem_methods memMethods = { + &sqliteMemMalloc, &sqliteMemFree, &sqliteMemRealloc, &sqliteMemSize, + &sqliteMemRoundup, &sqliteMemInit, &sqliteMemShutdown, nullptr}; + +} // namespace + +#endif // MOZ_MEMORY + +namespace mozilla { + +AutoSQLiteLifetime::AutoSQLiteLifetime() { + if (++AutoSQLiteLifetime::sSingletonEnforcer != 1) { + MOZ_CRASH("multiple instances of AutoSQLiteLifetime constructed!"); + } + +#ifdef MOZ_MEMORY + sResult = ::sqlite3_config(SQLITE_CONFIG_MALLOC, &memMethods); +#else + sResult = SQLITE_OK; +#endif + + if (sResult == SQLITE_OK) { + // TODO (bug 1191405): do not preallocate the connections caches until we + // have figured the impact on our consumers and memory. + sqlite3_config(SQLITE_CONFIG_PAGECACHE, NULL, 0, 0); + + // Explicitly initialize sqlite3. Although this is implicitly called by + // various sqlite3 functions (and the sqlite3_open calls in our case), + // the documentation suggests calling this directly. So we do. + sResult = ::sqlite3_initialize(); + } +} + +AutoSQLiteLifetime::~AutoSQLiteLifetime() { + // Shutdown the sqlite3 API. Warn if shutdown did not turn out okay, but + // there is nothing actionable we can do in that case. + sResult = ::sqlite3_shutdown(); + NS_WARNING_ASSERTION(sResult == SQLITE_OK, + "sqlite3 did not shutdown cleanly."); +} + +int AutoSQLiteLifetime::sSingletonEnforcer = 0; +int AutoSQLiteLifetime::sResult = SQLITE_MISUSE; + +} // namespace mozilla diff --git a/toolkit/xre/AutoSQLiteLifetime.h b/toolkit/xre/AutoSQLiteLifetime.h new file mode 100644 index 0000000000..26f8d976b2 --- /dev/null +++ b/toolkit/xre/AutoSQLiteLifetime.h @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_AutoSQLiteLifetime_h +#define mozilla_AutoSQLiteLifetime_h + +namespace mozilla { + +class AutoSQLiteLifetime final { + private: + static int sSingletonEnforcer; + static int sResult; + + public: + AutoSQLiteLifetime(); + ~AutoSQLiteLifetime(); + static int getInitResult() { return AutoSQLiteLifetime::sResult; } +}; + +} // namespace mozilla + +#endif diff --git a/toolkit/xre/Bootstrap.cpp b/toolkit/xre/Bootstrap.cpp new file mode 100644 index 0000000000..b81fde839b --- /dev/null +++ b/toolkit/xre/Bootstrap.cpp @@ -0,0 +1,117 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "mozilla/Bootstrap.h" +#include "nsXPCOM.h" + +#include "AutoSQLiteLifetime.h" + +#ifdef MOZ_WIDGET_ANDROID +# ifdef MOZ_PROFILE_GENERATE +extern "C" int __llvm_profile_dump(void); +# endif +#endif + +namespace mozilla { + +class BootstrapImpl final : public Bootstrap { + protected: + AutoSQLiteLifetime mSQLLT; + + virtual void Dispose() override { delete this; } + + public: + BootstrapImpl() = default; + + ~BootstrapImpl() = default; + + virtual void NS_LogInit() override { ::NS_LogInit(); } + + virtual void NS_LogTerm() override { ::NS_LogTerm(); } + + virtual void XRE_TelemetryAccumulate(int aID, uint32_t aSample) override { + ::XRE_TelemetryAccumulate(aID, aSample); + } + + virtual void XRE_StartupTimelineRecord(int aEvent, + mozilla::TimeStamp aWhen) override { + ::XRE_StartupTimelineRecord(aEvent, aWhen); + } + + virtual int XRE_main(int argc, char* argv[], + const BootstrapConfig& aConfig) override { + return ::XRE_main(argc, argv, aConfig); + } + + virtual void XRE_StopLateWriteChecks() override { + ::XRE_StopLateWriteChecks(); + } + + virtual int XRE_XPCShellMain(int argc, char** argv, char** envp, + const XREShellData* aShellData) override { + return ::XRE_XPCShellMain(argc, argv, envp, aShellData); + } + + virtual GeckoProcessType XRE_GetProcessType() override { + return ::XRE_GetProcessType(); + } + + virtual void XRE_SetProcessType(const char* aProcessTypeString) override { + ::XRE_SetProcessType(aProcessTypeString); + } + + virtual nsresult XRE_InitChildProcess( + int argc, char* argv[], const XREChildData* aChildData) override { + return ::XRE_InitChildProcess(argc, argv, aChildData); + } + + virtual void XRE_EnableSameExecutableForContentProc() override { + ::XRE_EnableSameExecutableForContentProc(); + } + +#ifdef MOZ_WIDGET_ANDROID + virtual void GeckoStart(JNIEnv* aEnv, char** argv, int argc, + const StaticXREAppData& aAppData, bool xpcshell, + const char* outFilePath) override { + ::GeckoStart(aEnv, argv, argc, aAppData, xpcshell, outFilePath); + } + + virtual void XRE_SetAndroidChildFds( + JNIEnv* aEnv, const XRE_AndroidChildFds& aFds) override { + ::XRE_SetAndroidChildFds(aEnv, aFds); + } + +# ifdef MOZ_PROFILE_GENERATE + virtual void XRE_WriteLLVMProfData() override { + __android_log_print(ANDROID_LOG_INFO, "GeckoLibLoad", + "Calling __llvm_profile_dump()"); + __llvm_profile_dump(); + } +# endif +#endif + +#ifdef LIBFUZZER + virtual void XRE_LibFuzzerSetDriver(LibFuzzerDriver aDriver) override { + ::XRE_LibFuzzerSetDriver(aDriver); + } +#endif + +#ifdef MOZ_ENABLE_FORKSERVER + virtual int XRE_ForkServer(int* argc, char*** argv) override { + return ::XRE_ForkServer(argc, argv); + } +#endif +}; + +extern "C" NS_EXPORT void NS_FROZENCALL +XRE_GetBootstrap(Bootstrap::UniquePtr& b) { + static bool sBootstrapInitialized = false; + MOZ_RELEASE_ASSERT(!sBootstrapInitialized); + + sBootstrapInitialized = true; + b.reset(new BootstrapImpl()); +} + +} // namespace mozilla diff --git a/toolkit/xre/Bootstrap.h b/toolkit/xre/Bootstrap.h new file mode 100644 index 0000000000..9d46bec55b --- /dev/null +++ b/toolkit/xre/Bootstrap.h @@ -0,0 +1,176 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/** + * This file represents the only external interface exposed from libxul. It + * is used by the various stub binaries (nsBrowserApp, xpcshell, + * plugin-container) to initialize XPCOM and start their main loop. + */ + +#ifndef mozilla_Bootstrap_h +#define mozilla_Bootstrap_h + +#include "mozilla/Maybe.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/Variant.h" +#include "nscore.h" +#include "nsXULAppAPI.h" + +#ifdef MOZ_WIDGET_ANDROID +# include "jni.h" + +namespace mozilla { +struct StaticXREAppData; +} + +extern "C" NS_EXPORT void GeckoStart(JNIEnv* aEnv, char** argv, int argc, + const mozilla::StaticXREAppData& aAppData, + bool xpcshell, const char* outFilePath); +#endif + +#if defined(XP_WIN) && defined(MOZ_SANDBOX) +namespace sandbox { +class BrokerServices; +} +#endif + +namespace mozilla { + +struct StaticXREAppData; + +struct BootstrapConfig { +#if defined(XP_WIN) && defined(MOZ_SANDBOX) + /* Chromium sandbox BrokerServices. */ + sandbox::BrokerServices* sandboxBrokerServices; +#endif + /* Pointer to static XRE AppData from application.ini.h */ + const StaticXREAppData* appData; + /* When the pointer above is null, points to the (string) path of an + * application.ini file to open and parse. + * When the pointer above is non-null, may indicate the directory where + * application files are, relative to the XRE. */ + const char* appDataPath; +}; + +/** + * This class is virtual abstract so that using it does not require linking + * any symbols. The singleton instance of this class is obtained from the + * exported method XRE_GetBootstrap. + */ +class Bootstrap { + protected: + Bootstrap() {} + + // Because of allocator mismatches, code outside libxul shouldn't delete a + // Bootstrap instance. Use Dispose(). + virtual ~Bootstrap() {} + + /** + * Destroy and deallocate this Bootstrap instance. + */ + virtual void Dispose() = 0; + + /** + * Helper class to use with UniquePtr. + */ + class BootstrapDelete { + public: + constexpr BootstrapDelete() {} + void operator()(Bootstrap* aPtr) const { aPtr->Dispose(); } + }; + + public: + typedef mozilla::UniquePtr<Bootstrap, BootstrapDelete> UniquePtr; + + virtual void NS_LogInit() = 0; + + virtual void NS_LogTerm() = 0; + + virtual void XRE_TelemetryAccumulate(int aID, uint32_t aSample) = 0; + + virtual void XRE_StartupTimelineRecord(int aEvent, + mozilla::TimeStamp aWhen) = 0; + + virtual int XRE_main(int argc, char* argv[], + const BootstrapConfig& aConfig) = 0; + + virtual void XRE_StopLateWriteChecks() = 0; + + virtual int XRE_XPCShellMain(int argc, char** argv, char** envp, + const XREShellData* aShellData) = 0; + + virtual GeckoProcessType XRE_GetProcessType() = 0; + + virtual void XRE_SetProcessType(const char* aProcessTypeString) = 0; + + virtual nsresult XRE_InitChildProcess(int argc, char* argv[], + const XREChildData* aChildData) = 0; + + virtual void XRE_EnableSameExecutableForContentProc() = 0; + +#ifdef MOZ_WIDGET_ANDROID + virtual void GeckoStart(JNIEnv* aEnv, char** argv, int argc, + const StaticXREAppData& aAppData, bool xpcshell, + const char* outFilePath) = 0; + + virtual void XRE_SetAndroidChildFds(JNIEnv* aEnv, + const XRE_AndroidChildFds& fds) = 0; +# ifdef MOZ_PROFILE_GENERATE + virtual void XRE_WriteLLVMProfData() = 0; +# endif +#endif + +#ifdef LIBFUZZER + virtual void XRE_LibFuzzerSetDriver(LibFuzzerDriver aDriver) = 0; +#endif + +#ifdef MOZ_ENABLE_FORKSERVER + virtual int XRE_ForkServer(int* argc, char*** argv) = 0; +#endif +}; + +enum class LibLoadingStrategy { + NoReadAhead, + ReadAhead, +}; + +#if defined(XP_WIN) +using DLErrorType = unsigned long; // (DWORD) +#else +using DLErrorType = UniqueFreePtr<char>; +#endif + +using BootstrapError = Variant<nsresult, DLErrorType>; + +using BootstrapResult = ::mozilla::Result<Bootstrap::UniquePtr, BootstrapError>; + +/** + * Creates and returns the singleton instance of the bootstrap object. + * @param `b` is an outparam. We use a parameter and not a return value + * because MSVC doesn't let us return a c++ class from a function with + * "C" linkage. On failure this will be null. + * @note This function may only be called once and will crash if called again. + */ +#ifdef XPCOM_GLUE +typedef void (*GetBootstrapType)(Bootstrap::UniquePtr&); +BootstrapResult GetBootstrap( + const char* aXPCOMFile = nullptr, + LibLoadingStrategy aLibLoadingStrategy = LibLoadingStrategy::NoReadAhead); +#else +extern "C" NS_EXPORT void NS_FROZENCALL +XRE_GetBootstrap(Bootstrap::UniquePtr& b); + +inline BootstrapResult GetBootstrap(const char* aXPCOMFile = nullptr) { + Bootstrap::UniquePtr bootstrap; + XRE_GetBootstrap(bootstrap); + return bootstrap; +} +#endif + +} // namespace mozilla + +#endif // mozilla_Bootstrap_h diff --git a/toolkit/xre/CmdLineAndEnvUtils.cpp b/toolkit/xre/CmdLineAndEnvUtils.cpp new file mode 100644 index 0000000000..634071bc90 --- /dev/null +++ b/toolkit/xre/CmdLineAndEnvUtils.cpp @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsCOMPtr.h" +#include "nsIFile.h" +#include "nsDependentString.h" +#include "prenv.h" + +#ifdef XP_WIN +# include <windows.h> +#endif + +namespace mozilla { + +// Load the path of a file saved with SaveFileToEnv +already_AddRefed<nsIFile> GetFileFromEnv(const char* name) { + nsresult rv; + nsCOMPtr<nsIFile> file; + +#ifdef XP_WIN + WCHAR path[_MAX_PATH]; + if (!GetEnvironmentVariableW(NS_ConvertASCIItoUTF16(name).get(), path, + _MAX_PATH)) + return nullptr; + + rv = NS_NewLocalFile(nsDependentString(path), true, getter_AddRefs(file)); + if (NS_FAILED(rv)) return nullptr; + + return file.forget(); +#else + const char* arg = PR_GetEnv(name); + if (!arg || !*arg) { + return nullptr; + } + + rv = NS_NewNativeLocalFile(nsDependentCString(arg), true, + getter_AddRefs(file)); + if (NS_FAILED(rv)) { + return nullptr; + } + + return file.forget(); +#endif +} + +} // namespace mozilla diff --git a/toolkit/xre/CmdLineAndEnvUtils.h b/toolkit/xre/CmdLineAndEnvUtils.h new file mode 100644 index 0000000000..cad34d2503 --- /dev/null +++ b/toolkit/xre/CmdLineAndEnvUtils.h @@ -0,0 +1,732 @@ +/* -*- 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/. */ + +#ifndef mozilla_CmdLineAndEnvUtils_h +#define mozilla_CmdLineAndEnvUtils_h + +// NB: This code may be used outside of xul and thus must not depend on XPCOM + +#if defined(MOZILLA_INTERNAL_API) +# include "prenv.h" +# include "prprf.h" +# include <string.h> +#endif + +#if defined(XP_WIN) +# include "mozilla/UniquePtr.h" +# include "mozilla/Vector.h" +# include "mozilla/WinHeaderOnlyUtils.h" + +# include <wchar.h> +# include <windows.h> +#endif // defined(XP_WIN) + +#include "mozilla/Maybe.h" +#include "mozilla/MemoryChecking.h" +#include "mozilla/TypedEnumBits.h" + +#include <ctype.h> +#include <stdint.h> +#include <stdlib.h> + +#ifndef NS_NO_XPCOM +# include "nsIFile.h" +# include "mozilla/AlreadyAddRefed.h" +#endif + +// Undo X11/X.h's definition of None +#undef None + +namespace mozilla { + +enum ArgResult { + ARG_NONE = 0, + ARG_FOUND = 1, + ARG_BAD = 2 // you wanted a param, but there isn't one +}; + +template <typename CharT> +inline void RemoveArg(int& argc, CharT** argv) { + do { + *argv = *(argv + 1); + ++argv; + } while (*argv); + + --argc; +} + +namespace internal { + +#if 'a' == '\x61' +// Valid option characters must have the same representation in every locale +// (which is true for most of ASCII, barring \x5C and \x7E). +static inline constexpr bool isValidOptionCharacter(char c) { + // We specifically avoid the use of `islower` here; it's locale-dependent, and + // may return true for non-ASCII values in some locales. + return ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || c == '-'; +}; + +// Convert uppercase to lowercase, locale-insensitively. +static inline constexpr char toLowercase(char c) { + // We specifically avoid the use of `tolower` here; it's locale-dependent, and + // may output ASCII values for non-ASCII input (or vice versa) in some + // locales. + return ('A' <= c && c <= 'Z') ? char(c | ' ') : c; +}; + +// Convert a CharT to a char, ensuring that no CharT is mapped to any valid +// option character except the unique CharT naturally corresponding thereto. +template <typename CharT> +static inline constexpr char toNarrow(CharT c) { + // confirmed to compile down to nothing when `CharT` is `char` + return (c & static_cast<CharT>(0xff)) == c ? c : 0xff; +}; +#else +// The target system's character set isn't even ASCII-compatible. If you're +// porting Gecko to such a platform, you'll have to implement these yourself. +# error Character conversion functions not implemented for this platform. +#endif + +// Case-insensitively compare a string taken from the command-line (`mixedstr`) +// to the text of some known command-line option (`lowerstr`). +template <typename CharT> +static inline bool strimatch(const char* lowerstr, const CharT* mixedstr) { + while (*lowerstr) { + if (!*mixedstr) return false; // mixedstr is shorter + + // Non-ASCII strings may compare incorrectly depending on the user's locale. + // Some ASCII-safe characters are also dispermitted for semantic reasons + // and simplicity. + if (!isValidOptionCharacter(*lowerstr)) return false; + + if (toLowercase(toNarrow(*mixedstr)) != *lowerstr) { + return false; // no match + } + + ++lowerstr; + ++mixedstr; + } + + if (*mixedstr) return false; // lowerstr is shorter + + return true; +} + +// Given a command-line argument, return Nothing if it isn't structurally a +// command-line option, and Some(<the option text>) if it is. +template <typename CharT> +mozilla::Maybe<const CharT*> ReadAsOption(const CharT* str) { + if (!str) { + return Nothing(); + } + if (*str == '-') { + str++; + if (*str == '-') { + str++; + } + return Some(str); + } +#ifdef XP_WIN + if (*str == '/') { + return Some(str + 1); + } +#endif + return Nothing(); +} + +} // namespace internal + +using internal::strimatch; + +const wchar_t kCommandLineDelimiter[] = L" \t"; + +enum class CheckArgFlag : uint32_t { + None = 0, + // (1 << 0) Used to be CheckOSInt + RemoveArg = (1 << 1) // Remove the argument from the argv array. +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(CheckArgFlag) + +/** + * Check for a commandline flag. If the flag takes a parameter, the + * parameter is returned in aParam. Flags may be in the form -arg or + * --arg (or /arg on win32). + * + * @param aArgc The argc value. + * @param aArgv The original argv. + * @param aArg the parameter to check. Must be lowercase. + * @param aParam if non-null, the -arg <data> will be stored in this pointer. + * This is *not* allocated, but rather a pointer to the argv data. + * @param aFlags Flags @see CheckArgFlag + */ +template <typename CharT> +inline ArgResult CheckArg(int& aArgc, CharT** aArgv, const char* aArg, + const CharT** aParam = nullptr, + CheckArgFlag aFlags = CheckArgFlag::RemoveArg) { + using internal::ReadAsOption; + MOZ_ASSERT(aArgv && aArg); + + CharT** curarg = aArgv + 1; // skip argv[0] + ArgResult ar = ARG_NONE; + + while (*curarg) { + if (const auto arg = ReadAsOption(*curarg)) { + if (strimatch(aArg, arg.value())) { + if (aFlags & CheckArgFlag::RemoveArg) { + RemoveArg(aArgc, curarg); + } else { + ++curarg; + } + + if (!aParam) { + ar = ARG_FOUND; + break; + } + + if (*curarg) { + if (ReadAsOption(*curarg)) { + return ARG_BAD; + } + + *aParam = *curarg; + + if (aFlags & CheckArgFlag::RemoveArg) { + RemoveArg(aArgc, curarg); + } + + ar = ARG_FOUND; + break; + } + + return ARG_BAD; + } + } + + ++curarg; + } + + return ar; +} + +template <typename CharT> +inline ArgResult CheckArg(int& aArgc, CharT** aArgv, const char* aArg, + std::nullptr_t, + CheckArgFlag aFlags = CheckArgFlag::RemoveArg) { + return CheckArg<CharT>(aArgc, aArgv, aArg, + static_cast<const CharT**>(nullptr), aFlags); +} + +namespace internal { +// template <typename T> +// constexpr bool IsStringRange = +// std::convertible_to<std::ranges::range_value_t<T>, const char *>; + +template <typename CharT, typename ListT> +// requires IsStringRange<ListT> +static bool MatchesAnyOf(CharT const* unknown, ListT const& known) { + for (const char* k : known) { + if (strimatch(k, unknown)) { + return true; + } + } + return false; +} + +template <typename CharT, typename ReqContainerT, typename OptContainerT> +// requires IsStringRange<ReqContainerT> && IsStringRange<OptContainerT> +inline bool EnsureCommandlineSafeImpl(int aArgc, CharT** aArgv, + ReqContainerT const& requiredParams, + OptContainerT const& optionalParams) { + // We expect either no -osint, or the full commandline to be: + // + // app -osint [<optional-param>...] <required-param> <required-argument> + // + // Otherwise, we abort to avoid abuse of other command-line handlers from apps + // that do a poor job escaping links they give to the OS. + // + // Note that the above implies that optional parameters do not themselves take + // arguments. This is a security feature, to prevent the possible injection of + // additional parameters via such arguments. (See, e.g., bug 384384.) + + static constexpr const char* osintLit = "osint"; + + // If "-osint" (or the equivalent) is not present, then this is trivially + // satisfied. + if (CheckArg(aArgc, aArgv, osintLit, nullptr, CheckArgFlag::None) != + ARG_FOUND) { + return true; + } + + // There should be at least 4 items present: + // <app name> -osint <required param> <arg>. + if (aArgc < 4) { + return false; + } + + // The first parameter must be osint. + const auto arg1 = ReadAsOption(aArgv[1]); + if (!arg1) return false; + if (!strimatch(osintLit, arg1.value())) { + return false; + } + // Following this is any number of optional parameters, terminated by a + // required parameter. + int pos = 2; + while (true) { + if (pos >= aArgc) return false; + + auto const arg = ReadAsOption(aArgv[pos]); + if (!arg) return false; + + if (MatchesAnyOf(arg.value(), optionalParams)) { + ++pos; + continue; + } + + if (MatchesAnyOf(arg.value(), requiredParams)) { + ++pos; + break; + } + + return false; + } + + // There must be one argument remaining... + if (pos + 1 != aArgc) return false; + // ... which must not be another option. + if (ReadAsOption(aArgv[pos])) { + return false; + } + + // Nothing ill-formed was passed. + return true; +} + +// C (and so C++) disallows empty arrays. Rather than require callers to jump +// through hoops to specify an empty optional-argument list, allow either its +// omission or its specification as `nullptr`, and do the hoop-jumping here. +// +// No such facility is provided for requiredParams, which must have at least one +// entry. +template <typename CharT, typename ReqContainerT> +inline bool EnsureCommandlineSafeImpl(int aArgc, CharT** aArgv, + ReqContainerT const& requiredParams, + std::nullptr_t _ = nullptr) { + struct { + inline const char** begin() const { return nullptr; } + inline const char** end() const { return nullptr; } + } emptyContainer; + return EnsureCommandlineSafeImpl(aArgc, aArgv, requiredParams, + emptyContainer); +} +} // namespace internal + +template <typename CharT, typename ReqContainerT, + typename OptContainerT = std::nullptr_t> +inline void EnsureCommandlineSafe( + int aArgc, CharT** aArgv, ReqContainerT const& requiredParams, + OptContainerT const& optionalParams = nullptr) { + if (!internal::EnsureCommandlineSafeImpl(aArgc, aArgv, requiredParams, + optionalParams)) { + exit(127); + } +} + +#if defined(XP_WIN) +namespace internal { +/** + * Get the length that the string will take and takes into account the + * additional length if the string needs to be quoted and if characters need to + * be escaped. + */ +inline int ArgStrLen(const wchar_t* s) { + int backslashes = 0; + int i = wcslen(s); + bool hasDoubleQuote = wcschr(s, L'"') != nullptr; + // Only add doublequotes if the string contains a space or a tab + bool addDoubleQuotes = wcspbrk(s, kCommandLineDelimiter) != nullptr; + + if (addDoubleQuotes) { + i += 2; // initial and final duoblequote + } + + if (hasDoubleQuote) { + while (*s) { + if (*s == '\\') { + ++backslashes; + } else { + if (*s == '"') { + // Escape the doublequote and all backslashes preceding the + // doublequote + i += backslashes + 1; + } + + backslashes = 0; + } + + ++s; + } + } + + return i; +} + +/** + * Copy string "s" to string "d", quoting the argument as appropriate and + * escaping doublequotes along with any backslashes that immediately precede + * doublequotes. + * The CRT parses this to retrieve the original argc/argv that we meant, + * see STDARGV.C in the MSVC CRT sources. + * + * @return the end of the string + */ +inline wchar_t* ArgToString(wchar_t* d, const wchar_t* s) { + int backslashes = 0; + bool hasDoubleQuote = wcschr(s, L'"') != nullptr; + // Only add doublequotes if the string contains a space or a tab + bool addDoubleQuotes = wcspbrk(s, kCommandLineDelimiter) != nullptr; + + if (addDoubleQuotes) { + *d = '"'; // initial doublequote + ++d; + } + + if (hasDoubleQuote) { + int i; + while (*s) { + if (*s == '\\') { + ++backslashes; + } else { + if (*s == '"') { + // Escape the doublequote and all backslashes preceding the + // doublequote + for (i = 0; i <= backslashes; ++i) { + *d = '\\'; + ++d; + } + } + + backslashes = 0; + } + + *d = *s; + ++d; + ++s; + } + } else { + wcscpy(d, s); + d += wcslen(s); + } + + if (addDoubleQuotes) { + *d = '"'; // final doublequote + ++d; + } + + return d; +} + +} // namespace internal + +/** + * Creates a command line from a list of arguments. + * + * @param argc Number of elements in |argv| + * @param argv Array of arguments + * @param aArgcExtra Number of elements in |aArgvExtra| + * @param aArgvExtra Optional array of arguments to be appended to the resulting + * command line after those provided by |argv|. + */ +inline UniquePtr<wchar_t[]> MakeCommandLine( + int argc, const wchar_t* const* argv, int aArgcExtra = 0, + const wchar_t* const* aArgvExtra = nullptr) { + int i; + int len = 0; + + // The + 1 for each argument reserves space for either a ' ' or the null + // terminator, depending on the position of the argument. + for (i = 0; i < argc; ++i) { + len += internal::ArgStrLen(argv[i]) + 1; + } + + for (i = 0; i < aArgcExtra; ++i) { + len += internal::ArgStrLen(aArgvExtra[i]) + 1; + } + + // Protect against callers that pass 0 arguments + if (len == 0) { + len = 1; + } + + auto s = MakeUnique<wchar_t[]>(len); + + int totalArgc = argc + aArgcExtra; + + wchar_t* c = s.get(); + for (i = 0; i < argc; ++i) { + c = internal::ArgToString(c, argv[i]); + if (i + 1 != totalArgc) { + *c = ' '; + ++c; + } + } + + for (i = 0; i < aArgcExtra; ++i) { + c = internal::ArgToString(c, aArgvExtra[i]); + if (i + 1 != aArgcExtra) { + *c = ' '; + ++c; + } + } + + *c = '\0'; + + return s; +} + +inline bool SetArgv0ToFullBinaryPath(wchar_t* aArgv[]) { + if (!aArgv) { + return false; + } + + UniquePtr<wchar_t[]> newArgv_0(GetFullBinaryPath()); + if (!newArgv_0) { + return false; + } + + // We intentionally leak newArgv_0 into argv[0] + aArgv[0] = newArgv_0.release(); + MOZ_LSAN_INTENTIONALLY_LEAK_OBJECT(aArgv[0]); + return true; +} + +# if defined(MOZILLA_INTERNAL_API) +// This class converts a command line string into an array of the arguments. +// It's basically the opposite of MakeCommandLine. However, the behavior is +// different from ::CommandLineToArgvW in several ways, such as escaping a +// backslash or quoting an argument containing whitespaces. This satisfies +// the examples at: +// https://docs.microsoft.com/en-us/cpp/cpp/main-function-command-line-args#results-of-parsing-command-lines +// https://docs.microsoft.com/en-us/previous-versions/17w5ykft(v=vs.85) +template <typename T> +class CommandLineParserWin final { + int mArgc; + T** mArgv; + + void Release() { + if (mArgv) { + while (mArgc) { + delete[] mArgv[--mArgc]; + } + delete[] mArgv; + mArgv = nullptr; + } + } + + public: + CommandLineParserWin() : mArgc(0), mArgv(nullptr) {} + ~CommandLineParserWin() { Release(); } + + CommandLineParserWin(const CommandLineParserWin&) = delete; + CommandLineParserWin(CommandLineParserWin&&) = delete; + CommandLineParserWin& operator=(const CommandLineParserWin&) = delete; + CommandLineParserWin& operator=(CommandLineParserWin&&) = delete; + + int Argc() const { return mArgc; } + const T* const* Argv() const { return mArgv; } + + // Returns the number of characters handled + int HandleCommandLine(const nsTSubstring<T>& aCmdLineString) { + Release(); + + if (aCmdLineString.IsEmpty()) { + return 0; + } + + int justCounting = 1; + // Flags, etc. + int init = 1; + int between, quoted, bSlashCount; + const T* p; + const T* const pEnd = aCmdLineString.EndReading(); + nsTAutoString<T> arg; + + // We loop if we've not finished the second pass through. + while (1) { + // Initialize if required. + if (init) { + p = aCmdLineString.BeginReading(); + between = 1; + mArgc = quoted = bSlashCount = 0; + + init = 0; + } + + const T charCurr = (p < pEnd) ? *p : 0; + const T charNext = (p + 1 < pEnd) ? *(p + 1) : 0; + + if (between) { + // We are traversing whitespace between args. + // Check for start of next arg. + if (charCurr != 0 && !wcschr(kCommandLineDelimiter, charCurr)) { + // Start of another arg. + between = 0; + arg.Truncate(); + switch (charCurr) { + case '\\': + // Count the backslash. + bSlashCount = 1; + break; + case '"': + // Remember we're inside quotes. + quoted = 1; + break; + default: + // Add character to arg. + arg += charCurr; + break; + } + } else { + // Another space between args, ignore it. + } + } else { + // We are processing the contents of an argument. + // Check for whitespace or end. + if (charCurr == 0 || + (!quoted && wcschr(kCommandLineDelimiter, charCurr))) { + // Process pending backslashes (interpret them + // literally since they're not followed by a "). + while (bSlashCount) { + arg += '\\'; + bSlashCount--; + } + // End current arg. + if (!justCounting) { + mArgv[mArgc] = new T[arg.Length() + 1]; + memcpy(mArgv[mArgc], arg.get(), (arg.Length() + 1) * sizeof(T)); + } + mArgc++; + // We're now between args. + between = 1; + } else { + // Still inside argument, process the character. + switch (charCurr) { + case '"': + // First, digest preceding backslashes (if any). + while (bSlashCount > 1) { + // Put one backsplash in arg for each pair. + arg += '\\'; + bSlashCount -= 2; + } + if (bSlashCount) { + // Quote is literal. + arg += '"'; + bSlashCount = 0; + } else { + // Quote starts or ends a quoted section. + if (quoted) { + // Check for special case of consecutive double + // quotes inside a quoted section. + if (charNext == '"') { + // This implies a literal double-quote. Fake that + // out by causing next double-quote to look as + // if it was preceded by a backslash. + bSlashCount = 1; + } else { + quoted = 0; + } + } else { + quoted = 1; + } + } + break; + case '\\': + // Add to count. + bSlashCount++; + break; + default: + // Accept any preceding backslashes literally. + while (bSlashCount) { + arg += '\\'; + bSlashCount--; + } + // Just add next char to the current arg. + arg += charCurr; + break; + } + } + } + + // Check for end of input. + if (charCurr) { + // Go to next character. + p++; + } else { + // If on first pass, go on to second. + if (justCounting) { + // Allocate argv array. + mArgv = new T*[mArgc]; + + // Start second pass + justCounting = 0; + init = 1; + } else { + // Quit. + break; + } + } + } + + return p - aCmdLineString.BeginReading(); + } +}; +# endif // defined(MOZILLA_INTERNAL_API) + +#endif // defined(XP_WIN) + +// SaveToEnv and EnvHasValue are only available on Windows or when +// MOZILLA_INTERNAL_API is defined +#if defined(MOZILLA_INTERNAL_API) || defined(XP_WIN) + +// Save literal putenv string to environment variable. +MOZ_NEVER_INLINE inline void SaveToEnv(const char* aEnvString) { +# if defined(MOZILLA_INTERNAL_API) + char* expr = strdup(aEnvString); + if (expr) { + PR_SetEnv(expr); + } + + // We intentionally leak |expr| here since it is required by PR_SetEnv. + MOZ_LSAN_INTENTIONALLY_LEAK_OBJECT(expr); +# elif defined(XP_WIN) + // This is the same as the NSPR implementation + // (Note that we don't need to do a strdup for this case; the CRT makes a + // copy) + _putenv(aEnvString); +# endif +} + +inline bool EnvHasValue(const char* aVarName) { +# if defined(MOZILLA_INTERNAL_API) + const char* val = PR_GetEnv(aVarName); + return val && *val; +# elif defined(XP_WIN) + // This is the same as the NSPR implementation + const char* val = getenv(aVarName); + return val && *val; +# endif +} + +#endif // end windows/internal_api-only definitions + +#ifndef NS_NO_XPCOM +already_AddRefed<nsIFile> GetFileFromEnv(const char* name); +#endif + +} // namespace mozilla + +#endif // mozilla_CmdLineAndEnvUtils_h diff --git a/toolkit/xre/CreateAppData.cpp b/toolkit/xre/CreateAppData.cpp new file mode 100644 index 0000000000..dda5b37873 --- /dev/null +++ b/toolkit/xre/CreateAppData.cpp @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsXULAppAPI.h" +#include "nsINIParser.h" +#include "nsIFile.h" +#include "mozilla/XREAppData.h" + +// This include must appear early in the unified cpp file for toolkit/xre to +// make sure OSX APIs make use of the OSX TextRange before mozilla::TextRange is +// declared and made a global symbol by a "using namespace mozilla" declaration. +#ifdef XP_MACOSX +# include <Carbon/Carbon.h> +#endif + +using namespace mozilla; + +static void ReadString(nsINIParser& parser, const char* section, + const char* key, XREAppData::CharPtr& result) { + nsCString str; + nsresult rv = parser.GetString(section, key, str); + if (NS_SUCCEEDED(rv)) { + result = str.get(); + } +} + +struct ReadFlag { + const char* section; + const char* key; + uint32_t flag; +}; + +static void ReadFlag(nsINIParser& parser, const char* section, const char* key, + uint32_t flag, uint32_t& result) { + char buf[6]; // large enough to hold "false" + nsresult rv = parser.GetString(section, key, buf, sizeof(buf)); + if (NS_SUCCEEDED(rv) || rv == NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) { + if (buf[0] == '1' || buf[0] == 't' || buf[0] == 'T') { + result |= flag; + } + if (buf[0] == '0' || buf[0] == 'f' || buf[0] == 'F') { + result &= ~flag; + } + } +} + +nsresult XRE_ParseAppData(nsIFile* aINIFile, XREAppData& aAppData) { + NS_ENSURE_ARG(aINIFile); + + nsresult rv; + + nsINIParser parser; + rv = parser.Init(aINIFile); + if (NS_FAILED(rv)) return rv; + + ReadString(parser, "App", "Vendor", aAppData.vendor); + ReadString(parser, "App", "Name", aAppData.name); + ReadString(parser, "App", "RemotingName", aAppData.remotingName); + ReadString(parser, "App", "Version", aAppData.version); + ReadString(parser, "App", "BuildID", aAppData.buildID); + ReadString(parser, "App", "ID", aAppData.ID); + ReadString(parser, "App", "Copyright", aAppData.copyright); + ReadString(parser, "App", "Profile", aAppData.profile); + ReadString(parser, "Gecko", "MinVersion", aAppData.minVersion); + ReadString(parser, "Gecko", "MaxVersion", aAppData.maxVersion); + ReadString(parser, "Crash Reporter", "ServerURL", aAppData.crashReporterURL); + ReadString(parser, "App", "UAName", aAppData.UAName); + ReadString(parser, "AppUpdate", "URL", aAppData.updateURL); + ReadFlag(parser, "XRE", "EnableProfileMigrator", + NS_XRE_ENABLE_PROFILE_MIGRATOR, aAppData.flags); + ReadFlag(parser, "Crash Reporter", "Enabled", NS_XRE_ENABLE_CRASH_REPORTER, + aAppData.flags); + + return NS_OK; +} diff --git a/toolkit/xre/DllPrefetchExperimentRegistryInfo.cpp b/toolkit/xre/DllPrefetchExperimentRegistryInfo.cpp new file mode 100644 index 0000000000..829999ac45 --- /dev/null +++ b/toolkit/xre/DllPrefetchExperimentRegistryInfo.cpp @@ -0,0 +1,112 @@ +/* -*- 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 "DllPrefetchExperimentRegistryInfo.h" + +#include "mozilla/Assertions.h" +#include "mozilla/NativeNt.h" +#include "mozilla/ResultExtensions.h" + +namespace mozilla { + +const wchar_t DllPrefetchExperimentRegistryInfo::kExperimentSubKeyPath[] = + L"SOFTWARE" + L"\\" MOZ_APP_VENDOR L"\\" MOZ_APP_BASENAME L"\\DllPrefetchExperiment"; + +Result<Ok, nsresult> DllPrefetchExperimentRegistryInfo::Open() { + if (!!mRegKey) { + return Ok(); + } + + DWORD disposition; + HKEY rawKey; + LSTATUS result = ::RegCreateKeyExW( + HKEY_CURRENT_USER, kExperimentSubKeyPath, 0, nullptr, + REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, nullptr, &rawKey, &disposition); + + if (result != ERROR_SUCCESS) { + return Err(NS_ERROR_UNEXPECTED); + } + + mRegKey.own(rawKey); + + if (disposition == REG_CREATED_NEW_KEY || + disposition == REG_OPENED_EXISTING_KEY) { + return Ok(); + } + + MOZ_ASSERT_UNREACHABLE("Invalid disposition from RegCreateKeyExW"); + return Err(NS_ERROR_UNEXPECTED); +} + +Result<Ok, nsresult> DllPrefetchExperimentRegistryInfo::ReflectPrefToRegistry( + int32_t aVal) { + MOZ_TRY(Open()); + + mPrefetchMode = aVal; + + LSTATUS status = + ::RegSetValueExW(mRegKey.get(), mBinPath.get(), 0, REG_DWORD, + reinterpret_cast<PBYTE>(&aVal), sizeof(aVal)); + + if (status != ERROR_SUCCESS) { + return Err(NS_ERROR_UNEXPECTED); + } + + return Ok(); +} + +Result<Ok, nsresult> DllPrefetchExperimentRegistryInfo::ReadRegistryValueData( + DWORD expectedType) { + MOZ_TRY(Open()); + + int32_t data; + DWORD dataLen = sizeof((ULONG)data); + DWORD type; + LSTATUS status = + ::RegQueryValueExW(mRegKey.get(), mBinPath.get(), nullptr, &type, + reinterpret_cast<PBYTE>(&data), &dataLen); + + if (status == ERROR_FILE_NOT_FOUND) { + // The registry key has not been created, set to default 0 + mPrefetchMode = 0; + return Ok(); + } + + if (status != ERROR_SUCCESS) { + return Err(NS_ERROR_UNEXPECTED); + } + + if (type != expectedType) { + return Err(NS_ERROR_UNEXPECTED); + } + + mPrefetchMode = data; + return Ok(); +} + +AlteredDllPrefetchMode +DllPrefetchExperimentRegistryInfo::GetAlteredDllPrefetchMode() { + Result<Ok, nsresult> result = ReadRegistryValueData(REG_DWORD); + if (!result.isOk()) { + MOZ_ASSERT(false); + return AlteredDllPrefetchMode::CurrentPrefetch; + } + + switch (mPrefetchMode) { + case 0: + return AlteredDllPrefetchMode::CurrentPrefetch; + case 1: + return AlteredDllPrefetchMode::NoPrefetch; + case 2: + return AlteredDllPrefetchMode::OptimizedPrefetch; + default: + MOZ_ASSERT(false); + return AlteredDllPrefetchMode::CurrentPrefetch; + } +} + +} // namespace mozilla diff --git a/toolkit/xre/DllPrefetchExperimentRegistryInfo.h b/toolkit/xre/DllPrefetchExperimentRegistryInfo.h new file mode 100644 index 0000000000..26cba51dcf --- /dev/null +++ b/toolkit/xre/DllPrefetchExperimentRegistryInfo.h @@ -0,0 +1,50 @@ +/* -*- 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/. */ + +#ifndef mozilla_DllPrefetchExperimentRegistryInfo_h +#define mozilla_DllPrefetchExperimentRegistryInfo_h + +#include "mozilla/Maybe.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "nsWindowsHelpers.h" + +/** + * This is a temporary file in order to conduct an experiment on the impact of + * startup time on retention (see Bug 1640087). + * This was generally adapted from LauncherRegistryInfo.h + */ + +namespace mozilla { + +enum class AlteredDllPrefetchMode { + CurrentPrefetch, + NoPrefetch, + OptimizedPrefetch +}; + +class DllPrefetchExperimentRegistryInfo final { + public: + DllPrefetchExperimentRegistryInfo() : mBinPath(GetFullBinaryPath().get()) {} + ~DllPrefetchExperimentRegistryInfo() {} + + Result<Ok, nsresult> ReflectPrefToRegistry(int32_t aVal); + Result<Ok, nsresult> ReadRegistryValueData(DWORD expectedType); + + AlteredDllPrefetchMode GetAlteredDllPrefetchMode(); + + private: + Result<Ok, nsresult> Open(); + + nsAutoRegKey mRegKey; + nsString mBinPath; + int32_t mPrefetchMode; + + static const wchar_t kExperimentSubKeyPath[]; +}; + +} // namespace mozilla + +#endif // mozilla_DllPrefetchExperimentRegistryInfo_h diff --git a/toolkit/xre/EventTracer.cpp b/toolkit/xre/EventTracer.cpp new file mode 100644 index 0000000000..aae3fee8e3 --- /dev/null +++ b/toolkit/xre/EventTracer.cpp @@ -0,0 +1,219 @@ +/* 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/. */ + +/* + * Event loop instrumentation. This code attempts to measure the + * latency of the UI-thread event loop by firing native events at it from + * a background thread, and measuring how long it takes for them + * to be serviced. The measurement interval (kMeasureInterval, below) + * is also used as the upper bound of acceptable response time. + * When an event takes longer than that interval to be serviced, + * a sample will be written to the log. + * + * Usage: + * + * Set MOZ_INSTRUMENT_EVENT_LOOP=1 in the environment to enable + * this instrumentation. Currently only the UI process is instrumented. + * + * Set MOZ_INSTRUMENT_EVENT_LOOP_OUTPUT in the environment to a + * file path to contain the log output, the default is to log to stdout. + * + * Set MOZ_INSTRUMENT_EVENT_LOOP_THRESHOLD in the environment to an + * integer number of milliseconds to change the threshold for reporting. + * The default is 20 milliseconds. Unresponsive periods shorter than this + * threshold will not be reported. + * + * Set MOZ_INSTRUMENT_EVENT_LOOP_INTERVAL in the environment to an + * integer number of milliseconds to change the maximum sampling frequency. + * This variable controls how often events will be sent to the main + * thread's event loop to sample responsiveness. The sampler will not + * send events twice within LOOP_INTERVAL milliseconds. + * The default is 10 milliseconds. + * + * All logged output lines start with MOZ_EVENT_TRACE. All timestamps + * output are milliseconds since the epoch (PRTime / 1000). + * + * On startup, a line of the form: + * MOZ_EVENT_TRACE start <timestamp> + * will be output. + * + * On shutdown, a line of the form: + * MOZ_EVENT_TRACE stop <timestamp> + * will be output. + * + * When an event servicing time exceeds the threshold, a line of the form: + * MOZ_EVENT_TRACE sample <timestamp> <duration> + * will be output, where <duration> is the number of milliseconds that + * it took for the event to be serviced. Duration may contain a fractional + * component. + */ + +#include "GeckoProfiler.h" + +#include "EventTracer.h" + +#include <stdio.h> + +#include "mozilla/Preferences.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/WidgetTraceEvent.h" +#include "nsDebug.h" +#include <limits.h> +#include <prenv.h> +#include <prinrval.h> +#include <prthread.h> +#include <prtime.h> + +#include "nsThreadUtils.h" + +using mozilla::FireAndWaitForTracerEvent; +using mozilla::TimeDuration; +using mozilla::TimeStamp; + +namespace { + +PRThread* sTracerThread = nullptr; +bool sExit = false; + +struct TracerStartClosure { + bool mLogTracing; + int32_t mThresholdInterval; +}; + +/* + * The tracer thread fires events at the native event loop roughly + * every kMeasureInterval. It will sleep to attempt not to send them + * more quickly, but if the response time is longer than kMeasureInterval + * it will not send another event until the previous response is received. + * + * The output defaults to stdout, but can be redirected to a file by + * settting the environment variable MOZ_INSTRUMENT_EVENT_LOOP_OUTPUT + * to the name of a file to use. + */ +void TracerThread(void* arg) { + AUTO_PROFILER_REGISTER_THREAD("Event Tracer"); + NS_SetCurrentThreadName("Event Tracer"); + + TracerStartClosure* threadArgs = static_cast<TracerStartClosure*>(arg); + + // These are the defaults. They can be overridden by environment vars. + // This should be set to the maximum latency we'd like to allow + // for responsiveness. + int32_t thresholdInterval = threadArgs->mThresholdInterval; + PRIntervalTime threshold = PR_MillisecondsToInterval(thresholdInterval); + // This is the sampling interval. + PRIntervalTime interval = PR_MillisecondsToInterval(thresholdInterval / 2); + + sExit = false; + FILE* log = nullptr; + char* envfile = PR_GetEnv("MOZ_INSTRUMENT_EVENT_LOOP_OUTPUT"); + if (envfile) { + log = fopen(envfile, "w"); + } + if (log == nullptr) log = stdout; + + char* thresholdenv = PR_GetEnv("MOZ_INSTRUMENT_EVENT_LOOP_THRESHOLD"); + if (thresholdenv && *thresholdenv) { + int val = atoi(thresholdenv); + if (val != 0 && val != INT_MAX && val != INT_MIN) { + threshold = PR_MillisecondsToInterval(val); + } + } + + char* intervalenv = PR_GetEnv("MOZ_INSTRUMENT_EVENT_LOOP_INTERVAL"); + if (intervalenv && *intervalenv) { + int val = atoi(intervalenv); + if (val != 0 && val != INT_MAX && val != INT_MIN) { + interval = PR_MillisecondsToInterval(val); + } + } + + if (threadArgs->mLogTracing) { + long long now = PR_Now() / PR_USEC_PER_MSEC; + fprintf(log, "MOZ_EVENT_TRACE start %llu\n", now); + } + + while (!sExit) { + TimeStamp start(TimeStamp::Now()); + PRIntervalTime next_sleep = interval; + + // TODO: only wait up to a maximum of interval; return + // early if that threshold is exceeded and dump a stack trace + // or do something else useful. + if (FireAndWaitForTracerEvent()) { + TimeDuration duration = TimeStamp::Now() - start; + // Only report samples that exceed our measurement threshold. + long long now = PR_Now() / PR_USEC_PER_MSEC; + if (threadArgs->mLogTracing && duration.ToMilliseconds() > threshold) { + fprintf(log, "MOZ_EVENT_TRACE sample %llu %lf\n", now, + duration.ToMilliseconds()); + } + + if (next_sleep > duration.ToMilliseconds()) { + next_sleep -= int(duration.ToMilliseconds()); + } else { + // Don't sleep at all if this event took longer than the measure + // interval to deliver. + next_sleep = 0; + } + } + + if (next_sleep != 0 && !sExit) { + PR_Sleep(next_sleep); + } + } + + if (threadArgs->mLogTracing) { + long long now = PR_Now() / PR_USEC_PER_MSEC; + fprintf(log, "MOZ_EVENT_TRACE stop %llu\n", now); + } + + if (log != stdout) fclose(log); + + delete threadArgs; +} + +} // namespace + +namespace mozilla { + +bool InitEventTracing(bool aLog) { + if (sTracerThread) return true; + + // Initialize the widget backend. + if (!InitWidgetTracing()) return false; + + // The tracer thread owns the object and will delete it. + TracerStartClosure* args = new TracerStartClosure(); + args->mLogTracing = aLog; + + // Pass the default threshold interval. + int32_t thresholdInterval = 20; + Preferences::GetInt("devtools.eventlooplag.threshold", &thresholdInterval); + args->mThresholdInterval = thresholdInterval; + + // Create a thread that will fire events back at the + // main thread to measure responsiveness. + MOZ_ASSERT(!sTracerThread, "Event tracing already initialized!"); + sTracerThread = + PR_CreateThread(PR_USER_THREAD, TracerThread, args, PR_PRIORITY_NORMAL, + PR_GLOBAL_THREAD, PR_JOINABLE_THREAD, 0); + return sTracerThread != nullptr; +} + +void ShutdownEventTracing() { + if (!sTracerThread) return; + + sExit = true; + // Ensure that the tracer thread doesn't hang. + SignalTracerThread(); + + if (sTracerThread) PR_JoinThread(sTracerThread); + sTracerThread = nullptr; + + // Allow the widget backend to clean up. + CleanUpWidgetTracing(); +} + +} // namespace mozilla diff --git a/toolkit/xre/EventTracer.h b/toolkit/xre/EventTracer.h new file mode 100644 index 0000000000..67fa6661dd --- /dev/null +++ b/toolkit/xre/EventTracer.h @@ -0,0 +1,23 @@ +/* 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/. */ + +#ifndef XRE_EVENTTRACER_H_ +#define XRE_EVENTTRACER_H_ + +namespace mozilla { + +// Create a thread that will fire events back at the +// main thread to measure responsiveness. Return true +// if the thread was created successfully. +// aLog If the tracing results should be printed to +// the console. +bool InitEventTracing(bool aLog); + +// Signal the background thread to stop, and join it. +// Must be called from the same thread that called InitEventTracing. +void ShutdownEventTracing(); + +} // namespace mozilla + +#endif /* XRE_EVENTTRACER_H_ */ diff --git a/toolkit/xre/GeckoArgs.h b/toolkit/xre/GeckoArgs.h new file mode 100644 index 0000000000..667ddb4ba4 --- /dev/null +++ b/toolkit/xre/GeckoArgs.h @@ -0,0 +1,149 @@ +/* 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/. */ + +#ifndef mozilla_GeckoArgs_h +#define mozilla_GeckoArgs_h + +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/Maybe.h" + +#include <array> +#include <cctype> +#include <climits> +#include <string> +#include <vector> + +namespace mozilla { + +namespace geckoargs { + +template <typename T> +struct CommandLineArg { + Maybe<T> Get(int& aArgc, char** aArgv, + const CheckArgFlag aFlags = CheckArgFlag::RemoveArg); + + const char* Name() { return sName; }; + + void Put(std::vector<std::string>& aArgs); + void Put(T aValue, std::vector<std::string>& aArgs); + + const char* sName; + const char* sMatch; +}; + +/// Get() + +template <> +inline Maybe<const char*> CommandLineArg<const char*>::Get( + int& aArgc, char** aArgv, const CheckArgFlag aFlags) { + MOZ_ASSERT(aArgv, "aArgv must be initialized before CheckArg()"); + const char* rv = nullptr; + if (ARG_FOUND == CheckArg(aArgc, aArgv, sMatch, &rv, aFlags)) { + return Some(rv); + } + return Nothing(); +} + +template <> +inline Maybe<bool> CommandLineArg<bool>::Get(int& aArgc, char** aArgv, + const CheckArgFlag aFlags) { + MOZ_ASSERT(aArgv, "aArgv must be initialized before CheckArg()"); + if (ARG_FOUND == + CheckArg(aArgc, aArgv, sMatch, (const char**)nullptr, aFlags)) { + return Some(true); + } + return Nothing(); +} + +template <> +inline Maybe<uint64_t> CommandLineArg<uint64_t>::Get( + int& aArgc, char** aArgv, const CheckArgFlag aFlags) { + MOZ_ASSERT(aArgv, "aArgv must be initialized before CheckArg()"); + const char* rv = nullptr; + if (ARG_FOUND == CheckArg(aArgc, aArgv, sMatch, &rv, aFlags)) { + errno = 0; + char* endptr = nullptr; + uint64_t conv = std::strtoull(rv, &endptr, 10); + if (errno == 0 && endptr && *endptr == '\0') { + return Some(conv); + } + } + return Nothing(); +} + +/// Put() + +template <> +inline void CommandLineArg<const char*>::Put(const char* aValue, + std::vector<std::string>& aArgs) { + aArgs.push_back(Name()); + aArgs.push_back(aValue); +} + +template <> +inline void CommandLineArg<bool>::Put(bool aValue, + std::vector<std::string>& aArgs) { + if (aValue) { + aArgs.push_back(Name()); + } +} + +template <> +inline void CommandLineArg<bool>::Put(std::vector<std::string>& aArgs) { + Put(true, aArgs); +} + +template <> +inline void CommandLineArg<uint64_t>::Put(uint64_t aValue, + std::vector<std::string>& aArgs) { + aArgs.push_back(Name()); + aArgs.push_back(std::to_string(aValue)); +} + +#if defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunused-variable" +#endif + +static CommandLineArg<const char*> sParentBuildID{"-parentBuildID", + "parentbuildid"}; +static CommandLineArg<const char*> sAppDir{"-appDir", "appdir"}; +static CommandLineArg<const char*> sProfile{"-profile", "profile"}; + +static CommandLineArg<uint64_t> sJsInitHandle{"-jsInitHandle", "jsinithandle"}; +static CommandLineArg<uint64_t> sJsInitLen{"-jsInitLen", "jsinitlen"}; +static CommandLineArg<uint64_t> sPrefsHandle{"-prefsHandle", "prefshandle"}; +static CommandLineArg<uint64_t> sPrefsLen{"-prefsLen", "prefslen"}; +static CommandLineArg<uint64_t> sPrefMapHandle{"-prefMapHandle", + "prefmaphandle"}; +static CommandLineArg<uint64_t> sPrefMapSize{"-prefMapSize", "prefmapsize"}; + +static CommandLineArg<uint64_t> sChildID{"-childID", "childid"}; + +static CommandLineArg<uint64_t> sSandboxingKind{"-sandboxingKind", + "sandboxingkind"}; + +static CommandLineArg<bool> sSafeMode{"-safeMode", "safemode"}; + +static CommandLineArg<bool> sIsForBrowser{"-isForBrowser", "isforbrowser"}; +static CommandLineArg<bool> sNotForBrowser{"-notForBrowser", "notforbrowser"}; + +#if defined(XP_WIN) +# if defined(MOZ_SANDBOX) +static CommandLineArg<bool> sWin32kLockedDown{"-win32kLockedDown", + "win32klockeddown"}; +# endif // defined(MOZ_SANDBOX) +static CommandLineArg<bool> sDisableDynamicDllBlocklist{ + "-disableDynamicBlocklist", "disabledynamicblocklist"}; +#endif // defined(XP_WIN) + +#if defined(__GNUC__) +# pragma GCC diagnostic pop +#endif + +} // namespace geckoargs + +} // namespace mozilla + +#endif // mozilla_GeckoArgs_h diff --git a/toolkit/xre/LauncherRegistryInfo.cpp b/toolkit/xre/LauncherRegistryInfo.cpp new file mode 100644 index 0000000000..b80ee77f4d --- /dev/null +++ b/toolkit/xre/LauncherRegistryInfo.cpp @@ -0,0 +1,652 @@ +/* -*- 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 "LauncherRegistryInfo.h" + +#include "commonupdatedir.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/NativeNt.h" +#include "mozilla/UniquePtr.h" + +#include <cwctype> +#include <shlobj.h> +#include <string> +#include <type_traits> + +#define EXPAND_STRING_MACRO2(t) t +#define EXPAND_STRING_MACRO(t) EXPAND_STRING_MACRO2(t) + +// This function is copied from Chromium base/time/time_win.cc +// Returns the current value of the performance counter. +static uint64_t QPCNowRaw() { + LARGE_INTEGER perf_counter_now = {}; + // According to the MSDN documentation for QueryPerformanceCounter(), this + // will never fail on systems that run XP or later. + // https://docs.microsoft.com/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancecounter + ::QueryPerformanceCounter(&perf_counter_now); + return perf_counter_now.QuadPart; +} + +static mozilla::LauncherResult<DWORD> GetCurrentImageTimestamp() { + mozilla::nt::PEHeaders headers(::GetModuleHandleW(nullptr)); + if (!headers) { + return LAUNCHER_ERROR_FROM_WIN32(ERROR_BAD_EXE_FORMAT); + } + + DWORD timestamp; + if (!headers.GetTimeStamp(timestamp)) { + return LAUNCHER_ERROR_FROM_WIN32(ERROR_INVALID_DATA); + } + + return timestamp; +} + +template <typename T> +static mozilla::LauncherResult<mozilla::Maybe<T>> ReadRegistryValueData( + const nsAutoRegKey& key, const std::wstring& name, DWORD expectedType) { + static_assert(std::is_trivial_v<T> && std::is_standard_layout_v<T>, + "Registry value type must be primitive."); + T data; + DWORD dataLen = sizeof(data); + DWORD type; + LSTATUS status = ::RegQueryValueExW(key.get(), name.c_str(), nullptr, &type, + reinterpret_cast<PBYTE>(&data), &dataLen); + if (status == ERROR_FILE_NOT_FOUND) { + return mozilla::Maybe<T>(); + } + + if (status != ERROR_SUCCESS) { + return LAUNCHER_ERROR_FROM_WIN32(status); + } + + if (type != expectedType) { + return LAUNCHER_ERROR_FROM_WIN32(ERROR_DATATYPE_MISMATCH); + } + + return mozilla::Some(data); +} + +static mozilla::LauncherResult<mozilla::UniquePtr<wchar_t[]>> +ReadRegistryValueString(const nsAutoRegKey& aKey, const std::wstring& aName) { + mozilla::UniquePtr<wchar_t[]> buf; + DWORD dataLen; + LSTATUS status = ::RegGetValueW(aKey.get(), nullptr, aName.c_str(), + RRF_RT_REG_SZ, nullptr, nullptr, &dataLen); + if (status == ERROR_FILE_NOT_FOUND) { + return buf; + } + + if (status != ERROR_SUCCESS) { + return LAUNCHER_ERROR_FROM_WIN32(status); + } + + buf = mozilla::MakeUnique<wchar_t[]>(dataLen / sizeof(wchar_t)); + + status = ::RegGetValueW(aKey.get(), nullptr, aName.c_str(), RRF_RT_REG_SZ, + nullptr, buf.get(), &dataLen); + if (status != ERROR_SUCCESS) { + return LAUNCHER_ERROR_FROM_WIN32(status); + } + + return buf; +} + +static mozilla::LauncherVoidResult WriteRegistryValueString( + const nsAutoRegKey& aKey, const std::wstring& aName, + const std::wstring& aValue) { + DWORD dataBytes = (aValue.size() + 1) * sizeof(wchar_t); + LSTATUS status = ::RegSetValueExW( + aKey.get(), aName.c_str(), /*Reserved*/ 0, REG_SZ, + reinterpret_cast<const BYTE*>(aValue.c_str()), dataBytes); + if (status != ERROR_SUCCESS) { + return LAUNCHER_ERROR_FROM_WIN32(status); + } + + return mozilla::Ok(); +} + +template <typename T> +static mozilla::LauncherVoidResult WriteRegistryValueData( + const nsAutoRegKey& key, const std::wstring& name, DWORD type, T data) { + static_assert(std::is_trivial_v<T> && std::is_standard_layout_v<T>, + "Registry value type must be primitive."); + LSTATUS status = + ::RegSetValueExW(key.get(), name.c_str(), 0, type, + reinterpret_cast<PBYTE>(&data), sizeof(data)); + if (status != ERROR_SUCCESS) { + return LAUNCHER_ERROR_FROM_WIN32(status); + } + + return mozilla::Ok(); +} + +static mozilla::LauncherResult<bool> DeleteRegistryValueData( + const nsAutoRegKey& key, const std::wstring& name) { + LSTATUS status = ::RegDeleteValueW(key, name.c_str()); + if (status == ERROR_FILE_NOT_FOUND) { + return false; + } + + if (status != ERROR_SUCCESS) { + return LAUNCHER_ERROR_FROM_WIN32(status); + } + + return true; +} + +namespace mozilla { + +const wchar_t LauncherRegistryInfo::kLauncherSubKeyPath[] = + L"SOFTWARE\\" EXPAND_STRING_MACRO(MOZ_APP_VENDOR) L"\\" EXPAND_STRING_MACRO( + MOZ_APP_BASENAME) L"\\Launcher"; +const wchar_t LauncherRegistryInfo::kLauncherSuffix[] = L"|Launcher"; +const wchar_t LauncherRegistryInfo::kBrowserSuffix[] = L"|Browser"; +const wchar_t LauncherRegistryInfo::kImageTimestampSuffix[] = L"|Image"; +const wchar_t LauncherRegistryInfo::kTelemetrySuffix[] = L"|Telemetry"; +const wchar_t LauncherRegistryInfo::kBlocklistSuffix[] = L"|Blocklist"; + +bool LauncherRegistryInfo::sAllowCommit = true; + +LauncherResult<LauncherRegistryInfo::Disposition> LauncherRegistryInfo::Open() { + if (!!mRegKey) { + return Disposition::OpenedExisting; + } + + DWORD disposition; + HKEY rawKey; + LSTATUS result = ::RegCreateKeyExW( + HKEY_CURRENT_USER, kLauncherSubKeyPath, 0, nullptr, + REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, nullptr, &rawKey, &disposition); + if (result != ERROR_SUCCESS) { + return LAUNCHER_ERROR_FROM_WIN32(result); + } + + mRegKey.own(rawKey); + + switch (disposition) { + case REG_CREATED_NEW_KEY: + return Disposition::CreatedNew; + case REG_OPENED_EXISTING_KEY: + return Disposition::OpenedExisting; + default: + break; + } + + MOZ_ASSERT_UNREACHABLE("Invalid disposition from RegCreateKeyExW"); + return LAUNCHER_ERROR_GENERIC(); +} + +LauncherVoidResult LauncherRegistryInfo::ReflectPrefToRegistry( + const bool aEnable) { + LauncherResult<EnabledState> curEnabledState = IsEnabled(); + if (curEnabledState.isErr()) { + return curEnabledState.propagateErr(); + } + + bool isCurrentlyEnabled = + curEnabledState.inspect() != EnabledState::ForceDisabled; + if (isCurrentlyEnabled == aEnable) { + // Don't reflect to the registry unless the new enabled state is actually + // changing with respect to the current enabled state. + return Ok(); + } + + // Always delete the launcher timestamp + LauncherResult<bool> clearedLauncherTimestamp = ClearLauncherStartTimestamp(); + MOZ_ASSERT(clearedLauncherTimestamp.isOk()); + if (clearedLauncherTimestamp.isErr()) { + return clearedLauncherTimestamp.propagateErr(); + } + + // Allow commit when we enable the launcher, otherwise block. + sAllowCommit = aEnable; + + if (!aEnable) { + // Set the browser timestamp to 0 to indicate force-disabled + return WriteBrowserStartTimestamp(0ULL); + } + + // Otherwise we delete the browser timestamp to start over fresh + LauncherResult<bool> clearedBrowserTimestamp = ClearBrowserStartTimestamp(); + MOZ_ASSERT(clearedBrowserTimestamp.isOk()); + if (clearedBrowserTimestamp.isErr()) { + return clearedBrowserTimestamp.propagateErr(); + } + + return Ok(); +} + +LauncherVoidResult LauncherRegistryInfo::ReflectTelemetryPrefToRegistry( + const bool aEnable) { + LauncherResult<Disposition> disposition = Open(); + if (disposition.isErr()) { + return disposition.propagateErr(); + } + + return WriteRegistryValueData(mRegKey, ResolveTelemetryValueName(), REG_DWORD, + aEnable ? 1UL : 0UL); +} + +LauncherResult<LauncherRegistryInfo::ProcessType> LauncherRegistryInfo::Check( + const ProcessType aDesiredType, const CheckOption aOption) { + LauncherResult<Disposition> disposition = Open(); + if (disposition.isErr()) { + return disposition.propagateErr(); + } + + LauncherResult<DWORD> ourImageTimestamp = GetCurrentImageTimestamp(); + if (ourImageTimestamp.isErr()) { + return ourImageTimestamp.propagateErr(); + } + + LauncherResult<Maybe<DWORD>> savedImageTimestamp = GetSavedImageTimestamp(); + if (savedImageTimestamp.isErr()) { + return savedImageTimestamp.propagateErr(); + } + + // If we don't have a saved timestamp, or we do but it doesn't match with + // our current timestamp, clear previous values unless we're force-disabled. + if (savedImageTimestamp.inspect().isNothing() || + savedImageTimestamp.inspect().value() != ourImageTimestamp.inspect()) { + LauncherVoidResult clearResult = ClearStartTimestamps(); + if (clearResult.isErr()) { + return clearResult.propagateErr(); + } + + LauncherVoidResult writeResult = + WriteImageTimestamp(ourImageTimestamp.inspect()); + if (writeResult.isErr()) { + return writeResult.propagateErr(); + } + } + + // If we're going to be running as the browser process, or there is no + // existing values to check, just write our timestamp and return. + if (aDesiredType == ProcessType::Browser) { + mBrowserTimestampToWrite = Some(QPCNowRaw()); + return ProcessType::Browser; + } + + if (disposition.inspect() == Disposition::CreatedNew) { + mLauncherTimestampToWrite = Some(QPCNowRaw()); + return ProcessType::Launcher; + } + + if (disposition.inspect() != Disposition::OpenedExisting) { + MOZ_ASSERT_UNREACHABLE("Invalid |disposition|"); + return LAUNCHER_ERROR_GENERIC(); + } + + LauncherResult<Maybe<uint64_t>> lastLauncherTimestampResult = + GetLauncherStartTimestamp(); + if (lastLauncherTimestampResult.isErr()) { + return lastLauncherTimestampResult.propagateErr(); + } + + LauncherResult<Maybe<uint64_t>> lastBrowserTimestampResult = + GetBrowserStartTimestamp(); + if (lastBrowserTimestampResult.isErr()) { + return lastBrowserTimestampResult.propagateErr(); + } + + const Maybe<uint64_t>& lastLauncherTimestamp = + lastLauncherTimestampResult.inspect(); + const Maybe<uint64_t>& lastBrowserTimestamp = + lastBrowserTimestampResult.inspect(); + + ProcessType typeToRunAs = aDesiredType; + + if (lastLauncherTimestamp.isSome() != lastBrowserTimestamp.isSome()) { + // If we have a launcher timestamp but no browser timestamp (or vice versa), + // that's bad because it is indicating that the browser can't run with + // the launcher process. + typeToRunAs = ProcessType::Browser; + } else if (lastLauncherTimestamp.isSome()) { + // if we have both timestamps, we want to ensure that the launcher timestamp + // is earlier than the browser timestamp. + if (aDesiredType == ProcessType::Launcher) { + bool areTimestampsOk = + lastLauncherTimestamp.value() < lastBrowserTimestamp.value(); + if (!areTimestampsOk) { + typeToRunAs = ProcessType::Browser; + } + } + } else { + // If we have neither timestamp, then we should try running as suggested + // by |aDesiredType|. + // We shouldn't really have this scenario unless we're going to be running + // as the launcher process. + MOZ_ASSERT(typeToRunAs == ProcessType::Launcher); + // No change to typeToRunAs + } + + // Debugging setting that forces the desired type regardless of the various + // tests that have been performed. + if (aOption == CheckOption::Force) { + typeToRunAs = aDesiredType; + } + + switch (typeToRunAs) { + case ProcessType::Browser: + if (aDesiredType != typeToRunAs) { + // We were hoping to run as the launcher, but some failure has caused + // us to run as the browser. Set the browser timestamp to zero as an + // indicator. + mBrowserTimestampToWrite = Some(0ULL); + } else { + mBrowserTimestampToWrite = Some(QPCNowRaw()); + } + break; + case ProcessType::Launcher: + mLauncherTimestampToWrite = Some(QPCNowRaw()); + break; + default: + MOZ_ASSERT_UNREACHABLE("Invalid |typeToRunAs|"); + return LAUNCHER_ERROR_GENERIC(); + } + + return typeToRunAs; +} + +LauncherVoidResult LauncherRegistryInfo::DisableDueToFailure() { + LauncherResult<Disposition> disposition = Open(); + if (disposition.isErr()) { + return disposition.propagateErr(); + } + LauncherVoidResult result = WriteBrowserStartTimestamp(0ULL); + if (result.isOk()) { + // Block commit when we disable the launcher. It could be allowed + // when the image timestamp is updated. + sAllowCommit = false; + } + return result; +} + +LauncherVoidResult LauncherRegistryInfo::Commit() { + if (!sAllowCommit) { + Abort(); + return Ok(); + } + + LauncherResult<Disposition> disposition = Open(); + if (disposition.isErr()) { + return disposition.propagateErr(); + } + + if (mLauncherTimestampToWrite.isSome()) { + LauncherVoidResult writeResult = + WriteLauncherStartTimestamp(mLauncherTimestampToWrite.value()); + if (writeResult.isErr()) { + return writeResult.propagateErr(); + } + mLauncherTimestampToWrite = Nothing(); + } + + if (mBrowserTimestampToWrite.isSome()) { + LauncherVoidResult writeResult = + WriteBrowserStartTimestamp(mBrowserTimestampToWrite.value()); + if (writeResult.isErr()) { + return writeResult.propagateErr(); + } + mBrowserTimestampToWrite = Nothing(); + } + + return Ok(); +} + +void LauncherRegistryInfo::Abort() { + mLauncherTimestampToWrite = mBrowserTimestampToWrite = Nothing(); +} + +LauncherRegistryInfo::EnabledState LauncherRegistryInfo::GetEnabledState( + const Maybe<uint64_t>& aLauncherTs, const Maybe<uint64_t>& aBrowserTs) { + if (aBrowserTs.isSome()) { + if (aLauncherTs.isSome()) { + if (aLauncherTs.value() < aBrowserTs.value()) { + // Both timestamps exist and the browser's timestamp is later. + return EnabledState::Enabled; + } + } else if (aBrowserTs.value() == 0ULL) { + // Only browser's timestamp exists and its value is 0. + return EnabledState::ForceDisabled; + } + } else if (aLauncherTs.isNothing()) { + // Neither timestamps exist. + return EnabledState::Enabled; + } + + // Everything else is FailDisabled. + return EnabledState::FailDisabled; +} + +LauncherResult<LauncherRegistryInfo::EnabledState> +LauncherRegistryInfo::IsEnabled() { + LauncherResult<Disposition> disposition = Open(); + if (disposition.isErr()) { + return disposition.propagateErr(); + } + + LauncherResult<Maybe<uint64_t>> lastLauncherTimestamp = + GetLauncherStartTimestamp(); + if (lastLauncherTimestamp.isErr()) { + return lastLauncherTimestamp.propagateErr(); + } + + LauncherResult<Maybe<uint64_t>> lastBrowserTimestamp = + GetBrowserStartTimestamp(); + if (lastBrowserTimestamp.isErr()) { + return lastBrowserTimestamp.propagateErr(); + } + + return GetEnabledState(lastLauncherTimestamp.inspect(), + lastBrowserTimestamp.inspect()); +} + +LauncherResult<bool> LauncherRegistryInfo::IsTelemetryEnabled() { + LauncherResult<Disposition> disposition = Open(); + if (disposition.isErr()) { + return disposition.propagateErr(); + } + + LauncherResult<Maybe<DWORD>> result = ReadRegistryValueData<DWORD>( + mRegKey, ResolveTelemetryValueName(), REG_DWORD); + if (result.isErr()) { + return result.propagateErr(); + } + + if (result.inspect().isNothing()) { + // Value does not exist, treat as false + return false; + } + + return result.inspect().value() != 0; +} + +const std::wstring& LauncherRegistryInfo::ResolveLauncherValueName() { + if (mLauncherValueName.empty()) { + mLauncherValueName.assign(mBinPath); + mLauncherValueName.append(kLauncherSuffix, + ArrayLength(kLauncherSuffix) - 1); + } + + return mLauncherValueName; +} + +const std::wstring& LauncherRegistryInfo::ResolveBrowserValueName() { + if (mBrowserValueName.empty()) { + mBrowserValueName.assign(mBinPath); + mBrowserValueName.append(kBrowserSuffix, ArrayLength(kBrowserSuffix) - 1); + } + + return mBrowserValueName; +} + +const std::wstring& LauncherRegistryInfo::ResolveImageTimestampValueName() { + if (mImageValueName.empty()) { + mImageValueName.assign(mBinPath); + mImageValueName.append(kImageTimestampSuffix, + ArrayLength(kImageTimestampSuffix) - 1); + } + + return mImageValueName; +} + +const std::wstring& LauncherRegistryInfo::ResolveTelemetryValueName() { + if (mTelemetryValueName.empty()) { + mTelemetryValueName.assign(mBinPath); + mTelemetryValueName.append(kTelemetrySuffix, + ArrayLength(kTelemetrySuffix) - 1); + } + + return mTelemetryValueName; +} + +const std::wstring& LauncherRegistryInfo::ResolveBlocklistValueName() { + if (mBlocklistValueName.empty()) { + mBlocklistValueName.assign(mBinPath); + mBlocklistValueName.append(kBlocklistSuffix, + ArrayLength(kBlocklistSuffix) - 1); + } + + return mBlocklistValueName; +} + +LauncherVoidResult LauncherRegistryInfo::WriteLauncherStartTimestamp( + uint64_t aValue) { + return WriteRegistryValueData(mRegKey, ResolveLauncherValueName(), REG_QWORD, + aValue); +} + +LauncherVoidResult LauncherRegistryInfo::WriteBrowserStartTimestamp( + uint64_t aValue) { + return WriteRegistryValueData(mRegKey, ResolveBrowserValueName(), REG_QWORD, + aValue); +} + +LauncherVoidResult LauncherRegistryInfo::WriteImageTimestamp(DWORD aTimestamp) { + return WriteRegistryValueData(mRegKey, ResolveImageTimestampValueName(), + REG_DWORD, aTimestamp); +} + +LauncherResult<bool> LauncherRegistryInfo::ClearLauncherStartTimestamp() { + return DeleteRegistryValueData(mRegKey, ResolveLauncherValueName()); +} + +LauncherResult<bool> LauncherRegistryInfo::ClearBrowserStartTimestamp() { + return DeleteRegistryValueData(mRegKey, ResolveBrowserValueName()); +} + +LauncherVoidResult LauncherRegistryInfo::ClearStartTimestamps() { + LauncherResult<EnabledState> enabled = IsEnabled(); + if (enabled.isOk() && enabled.inspect() == EnabledState::ForceDisabled) { + // We don't clear anything when we're force disabled - we need to maintain + // the current registry state in this case. + return Ok(); + } + + LauncherResult<bool> clearedLauncherTimestamp = ClearLauncherStartTimestamp(); + if (clearedLauncherTimestamp.isErr()) { + return clearedLauncherTimestamp.propagateErr(); + } + + LauncherResult<bool> clearedBrowserTimestamp = ClearBrowserStartTimestamp(); + if (clearedBrowserTimestamp.isErr()) { + return clearedBrowserTimestamp.propagateErr(); + } + + // Reset both timestamps to align with registry deletion + mLauncherTimestampToWrite = mBrowserTimestampToWrite = Nothing(); + + // Disablement is gone. Let's allow commit. + sAllowCommit = true; + + return Ok(); +} + +LauncherResult<Maybe<DWORD>> LauncherRegistryInfo::GetSavedImageTimestamp() { + return ReadRegistryValueData<DWORD>(mRegKey, ResolveImageTimestampValueName(), + REG_DWORD); +} + +LauncherResult<Maybe<uint64_t>> +LauncherRegistryInfo::GetLauncherStartTimestamp() { + return ReadRegistryValueData<uint64_t>(mRegKey, ResolveLauncherValueName(), + REG_QWORD); +} + +LauncherResult<Maybe<uint64_t>> +LauncherRegistryInfo::GetBrowserStartTimestamp() { + return ReadRegistryValueData<uint64_t>(mRegKey, ResolveBrowserValueName(), + REG_QWORD); +} + +LauncherResult<std::wstring> +LauncherRegistryInfo::BuildDefaultBlocklistFilename() { + // These flags are chosen to avoid I/O, see bug 1363398. + const DWORD flags = + KF_FLAG_SIMPLE_IDLIST | KF_FLAG_DONT_VERIFY | KF_FLAG_NO_ALIAS; + PWSTR rawPath = nullptr; + HRESULT hr = + ::SHGetKnownFolderPath(FOLDERID_RoamingAppData, flags, nullptr, &rawPath); + if (FAILED(hr)) { + ::CoTaskMemFree(rawPath); + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + UniquePtr<wchar_t, CoTaskMemFreeDeleter> appDataPath(rawPath); + std::wstring defaultBlocklistPath(appDataPath.get()); + + UniquePtr<NS_tchar[]> hash; + std::wstring binPathLower; + binPathLower.reserve(mBinPath.size()); + std::transform(mBinPath.begin(), mBinPath.end(), + std::back_inserter(binPathLower), std::towlower); + if (!::GetInstallHash(reinterpret_cast<const char16_t*>(binPathLower.c_str()), + hash)) { + return LAUNCHER_ERROR_FROM_WIN32(ERROR_INVALID_DATA); + } + + defaultBlocklistPath.append( + L"\\" MOZ_APP_VENDOR L"\\" MOZ_APP_BASENAME L"\\blocklist-"); + defaultBlocklistPath.append(hash.get()); + + return defaultBlocklistPath; +} + +LauncherResult<std::wstring> LauncherRegistryInfo::GetBlocklistFileName() { + LauncherResult<Disposition> disposition = Open(); + if (disposition.isErr()) { + return disposition.propagateErr(); + } + + LauncherResult<UniquePtr<wchar_t[]>> readResult = + ReadRegistryValueString(mRegKey, ResolveBlocklistValueName()); + if (readResult.isErr()) { + return readResult.propagateErr(); + } + + if (readResult.inspect()) { + UniquePtr<wchar_t[]> buf = readResult.unwrap(); + return std::wstring(buf.get()); + } + + LauncherResult<std::wstring> defaultBlocklistPath = + BuildDefaultBlocklistFilename(); + if (defaultBlocklistPath.isErr()) { + return defaultBlocklistPath.propagateErr(); + } + + LauncherVoidResult writeResult = WriteRegistryValueString( + mRegKey, ResolveBlocklistValueName(), defaultBlocklistPath.inspect()); + if (writeResult.isErr()) { + return writeResult.propagateErr(); + } + + return defaultBlocklistPath; +} + +} // namespace mozilla diff --git a/toolkit/xre/LauncherRegistryInfo.h b/toolkit/xre/LauncherRegistryInfo.h new file mode 100644 index 0000000000..c89ed66a69 --- /dev/null +++ b/toolkit/xre/LauncherRegistryInfo.h @@ -0,0 +1,104 @@ +/* -*- 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/. */ + +#ifndef mozilla_LauncherRegistryInfo_h +#define mozilla_LauncherRegistryInfo_h + +#include "mozilla/Maybe.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "nsWindowsHelpers.h" + +#include <string> + +/** + * We use std::wstring here because this code must be usable within both the + * launcher process and Gecko itself. + */ + +namespace mozilla { + +class LauncherRegistryInfo final { + public: + enum class ProcessType { Launcher, Browser }; + + enum class EnabledState { + Enabled, + FailDisabled, + ForceDisabled, + }; + + enum class CheckOption { + Default, + Force, + }; + + LauncherRegistryInfo() : mBinPath(GetFullBinaryPath().get()) {} + ~LauncherRegistryInfo() { Abort(); } + + LauncherVoidResult ReflectPrefToRegistry(const bool aEnable); + LauncherResult<EnabledState> IsEnabled(); + LauncherResult<bool> IsTelemetryEnabled(); + LauncherVoidResult ReflectTelemetryPrefToRegistry(const bool aEnable); + LauncherResult<ProcessType> Check( + const ProcessType aDesiredType, + const CheckOption aOption = CheckOption::Default); + LauncherVoidResult DisableDueToFailure(); + LauncherVoidResult Commit(); + void Abort(); + LauncherResult<std::wstring> GetBlocklistFileName(); + + private: + enum class Disposition { CreatedNew, OpenedExisting }; + + private: + // This flag is to prevent the disabled state from being accidentally + // re-enabled by another instance. + static bool sAllowCommit; + + static EnabledState GetEnabledState(const Maybe<uint64_t>& aLauncherTs, + const Maybe<uint64_t>& aBrowserTs); + + LauncherResult<Disposition> Open(); + LauncherVoidResult WriteLauncherStartTimestamp(uint64_t aValue); + LauncherVoidResult WriteBrowserStartTimestamp(uint64_t aValue); + LauncherVoidResult WriteImageTimestamp(DWORD aTimestamp); + LauncherResult<bool> ClearLauncherStartTimestamp(); + LauncherResult<bool> ClearBrowserStartTimestamp(); + LauncherVoidResult ClearStartTimestamps(); + LauncherResult<Maybe<DWORD>> GetSavedImageTimestamp(); + LauncherResult<Maybe<uint64_t>> GetLauncherStartTimestamp(); + LauncherResult<Maybe<uint64_t>> GetBrowserStartTimestamp(); + LauncherResult<std::wstring> BuildDefaultBlocklistFilename(); + + const std::wstring& ResolveLauncherValueName(); + const std::wstring& ResolveBrowserValueName(); + const std::wstring& ResolveImageTimestampValueName(); + const std::wstring& ResolveTelemetryValueName(); + const std::wstring& ResolveBlocklistValueName(); + + private: + Maybe<uint64_t> mLauncherTimestampToWrite; + Maybe<uint64_t> mBrowserTimestampToWrite; + + nsAutoRegKey mRegKey; + std::wstring mBinPath; + std::wstring mImageValueName; + std::wstring mBrowserValueName; + std::wstring mLauncherValueName; + std::wstring mTelemetryValueName; + std::wstring mBlocklistValueName; + + static const wchar_t kLauncherSubKeyPath[]; + static const wchar_t kLauncherSuffix[]; + static const wchar_t kBrowserSuffix[]; + static const wchar_t kImageTimestampSuffix[]; + static const wchar_t kTelemetrySuffix[]; + static const wchar_t kBlocklistSuffix[]; +}; + +} // namespace mozilla + +#endif // mozilla_LauncherRegistryInfo_h diff --git a/toolkit/xre/MacApplicationDelegate.h b/toolkit/xre/MacApplicationDelegate.h new file mode 100644 index 0000000000..261cccc122 --- /dev/null +++ b/toolkit/xre/MacApplicationDelegate.h @@ -0,0 +1,17 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +// This file defines the interface between Cocoa-specific Obj-C++ and generic +// C++, so it itself cannot have any Obj-C bits in it. + +#ifndef MacApplicationDelegate_h_ +#define MacApplicationDelegate_h_ + +void EnsureUseCocoaDockAPI(void); +void SetupMacApplicationDelegate(bool* gRestartedByOS); +void ProcessPendingGetURLAppleEvents(void); +void DisableAppNap(void); + +#endif diff --git a/toolkit/xre/MacApplicationDelegate.mm b/toolkit/xre/MacApplicationDelegate.mm new file mode 100644 index 0000000000..215798fa24 --- /dev/null +++ b/toolkit/xre/MacApplicationDelegate.mm @@ -0,0 +1,412 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +// NSApplication delegate for Mac OS X Cocoa API. + +// As of 10.4 Tiger, the system can send six kinds of Apple Events to an application; +// a well-behaved XUL app should have some kind of handling for all of them. +// +// See +// http://developer.apple.com/documentation/Cocoa/Conceptual/ScriptableCocoaApplications/SApps_handle_AEs/chapter_11_section_3.html +// for details. + +#import <Cocoa/Cocoa.h> +#include "NativeMenuMac.h" +#import <Carbon/Carbon.h> + +#include "nsCOMPtr.h" +#include "nsINativeAppSupport.h" +#include "nsAppRunner.h" +#include "nsAppShell.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsIAppStartup.h" +#include "nsIObserverService.h" +#include "nsISupportsPrimitives.h" +#include "nsObjCExceptions.h" +#include "nsIFile.h" +#include "nsDirectoryServiceDefs.h" +#include "nsCommandLine.h" +#include "nsIMacDockSupport.h" +#include "nsIStandaloneNativeMenu.h" +#include "nsILocalFileMac.h" +#include "nsString.h" +#include "nsCommandLineServiceMac.h" +#include "nsCommandLine.h" +#include "nsStandaloneNativeMenu.h" +#include "nsCocoaUtils.h" + +class AutoAutoreleasePool { + public: + AutoAutoreleasePool() { mLocalPool = [[NSAutoreleasePool alloc] init]; } + ~AutoAutoreleasePool() { [mLocalPool release]; } + + private: + NSAutoreleasePool* mLocalPool; +}; + +@interface MacApplicationDelegate : NSObject <NSApplicationDelegate> { +} + +@end + +static bool sProcessedGetURLEvent = false; + +// Methods that can be called from non-Objective-C code. + +// This is needed, on relaunch, to force the OS to use the "Cocoa Dock API" +// instead of the "Carbon Dock API". For more info see bmo bug 377166. +void EnsureUseCocoaDockAPI() { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + [GeckoNSApplication sharedApplication]; + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +void DisableAppNap() { + // Prevent the parent process from entering App Nap. macOS does not put our + // child processes into App Nap and, as a result, when the parent is in + // App Nap, child processes continue to run normally generating IPC messages + // for the parent which can end up being queued. This can cause the browser + // to be unresponsive for a period of time after the App Nap until the parent + // process "catches up." NSAppSleepDisabled has to be set early during + // startup before the OS reads the value for the process. + [[NSUserDefaults standardUserDefaults] registerDefaults:@{ + @"NSAppSleepDisabled" : @YES, + }]; +} + +void SetupMacApplicationDelegate(bool* gRestartedByOS) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + // this is called during startup, outside an event loop, and therefore + // needs an autorelease pool to avoid cocoa object leakage (bug 559075) + AutoAutoreleasePool pool; + + // Ensure that ProcessPendingGetURLAppleEvents() doesn't regress bug 377166. + [GeckoNSApplication sharedApplication]; + + // This call makes it so that application:openFile: doesn't get bogus calls + // from Cocoa doing its own parsing of the argument string. And yes, we need + // to use a string with a boolean value in it. That's just how it works. + [[NSUserDefaults standardUserDefaults] setObject:@"NO" forKey:@"NSTreatUnknownArgumentsAsOpen"]; + + // Create the delegate. This should be around for the lifetime of the app. + id<NSApplicationDelegate> delegate = [[MacApplicationDelegate alloc] init]; + [[GeckoNSApplication sharedApplication] setDelegate:delegate]; + + *gRestartedByOS = !!nsCocoaUtils::ShouldRestoreStateDueToLaunchAtLogin(); + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +// Indirectly make the OS process any pending GetURL Apple events. This is +// done via _DPSNextEvent() (an undocumented AppKit function called from +// [NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:]). Apple +// events are only processed if 'dequeue' is 'YES' -- so we need to call +// [NSApplication sendEvent:] on any event that gets returned. 'event' will +// never itself be an Apple event, and it may be 'nil' even when Apple events +// are processed. +void ProcessPendingGetURLAppleEvents() { + AutoAutoreleasePool pool; + bool keepSpinning = true; + while (keepSpinning) { + sProcessedGetURLEvent = false; + NSEvent* event = [NSApp nextEventMatchingMask:NSEventMaskAny + untilDate:nil + inMode:NSDefaultRunLoopMode + dequeue:YES]; + if (event) [NSApp sendEvent:event]; + keepSpinning = sProcessedGetURLEvent; + } +} + +@implementation MacApplicationDelegate + +- (id)init { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + if ((self = [super init])) { + NSAppleEventManager* aeMgr = [NSAppleEventManager sharedAppleEventManager]; + + [aeMgr setEventHandler:self + andSelector:@selector(handleAppleEvent:withReplyEvent:) + forEventClass:kInternetEventClass + andEventID:kAEGetURL]; + + [aeMgr setEventHandler:self + andSelector:@selector(handleAppleEvent:withReplyEvent:) + forEventClass:'WWW!' + andEventID:'OURL']; + + [aeMgr setEventHandler:self + andSelector:@selector(handleAppleEvent:withReplyEvent:) + forEventClass:kCoreEventClass + andEventID:kAEOpenDocuments]; + + if (![NSApp windowsMenu]) { + // If the application has a windows menu, it will keep it up to date and + // prepend the window list to the Dock menu automatically. + NSMenu* windowsMenu = [[NSMenu alloc] initWithTitle:@"Window"]; + [NSApp setWindowsMenu:windowsMenu]; + [windowsMenu release]; + } + } + return self; + + NS_OBJC_END_TRY_BLOCK_RETURN(nil); +} + +- (void)dealloc { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + NSAppleEventManager* aeMgr = [NSAppleEventManager sharedAppleEventManager]; + [aeMgr removeEventHandlerForEventClass:kInternetEventClass andEventID:kAEGetURL]; + [aeMgr removeEventHandlerForEventClass:'WWW!' andEventID:'OURL']; + [aeMgr removeEventHandlerForEventClass:kCoreEventClass andEventID:kAEOpenDocuments]; + [super dealloc]; + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +// The method that NSApplication calls upon a request to reopen, such as when +// the Dock icon is clicked and no windows are open. A "visible" window may be +// miniaturized, so we can't skip nsCocoaNativeReOpen() if 'flag' is 'true'. +- (BOOL)applicationShouldHandleReopen:(NSApplication*)theApp hasVisibleWindows:(BOOL)flag { + nsCOMPtr<nsINativeAppSupport> nas = NS_GetNativeAppSupport(); + NS_ENSURE_TRUE(nas, NO); + + // Go to the common Carbon/Cocoa reopen method. + nsresult rv = nas->ReOpen(); + NS_ENSURE_SUCCESS(rv, NO); + + // NO says we don't want NSApplication to do anything else for us. + return NO; +} + +// The method that NSApplication calls when documents are requested to be opened. +// It will be called once for each selected document. +- (BOOL)application:(NSApplication*)theApplication openFile:(NSString*)filename { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + NSURL* url = [NSURL fileURLWithPath:filename]; + if (!url) return NO; + + NSString* urlString = [url absoluteString]; + if (!urlString) return NO; + + // Add the URL to any command line we're currently setting up. + if (CommandLineServiceMac::AddURLToCurrentCommandLine([urlString UTF8String])) return YES; + + nsCOMPtr<nsILocalFileMac> inFile; + nsresult rv = NS_NewLocalFileWithCFURL((CFURLRef)url, true, getter_AddRefs(inFile)); + if (NS_FAILED(rv)) return NO; + + nsCOMPtr<nsICommandLineRunner> cmdLine(new nsCommandLine()); + + nsCString filePath; + rv = inFile->GetNativePath(filePath); + if (NS_FAILED(rv)) return NO; + + nsCOMPtr<nsIFile> workingDir; + rv = NS_GetSpecialDirectory(NS_OS_CURRENT_WORKING_DIR, getter_AddRefs(workingDir)); + if (NS_FAILED(rv)) { + // Couldn't find a working dir. Uh oh. Good job cmdline::Init can cope. + workingDir = nullptr; + } + + const char* argv[3] = {nullptr, "-file", filePath.get()}; + rv = cmdLine->Init(3, argv, workingDir, nsICommandLine::STATE_REMOTE_EXPLICIT); + if (NS_FAILED(rv)) return NO; + + if (NS_SUCCEEDED(cmdLine->Run())) return YES; + + return NO; + + NS_OBJC_END_TRY_BLOCK_RETURN(NO); +} + +// The method that NSApplication calls when documents are requested to be printed +// from the Finder (under the "File" menu). +// It will be called once for each selected document. +- (BOOL)application:(NSApplication*)theApplication printFile:(NSString*)filename { + return NO; +} + +// Create the menu that shows up in the Dock. +- (NSMenu*)applicationDockMenu:(NSApplication*)sender { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + // Create the NSMenu that will contain the dock menu items. + NSMenu* menu = [[[NSMenu alloc] initWithTitle:@""] autorelease]; + [menu setAutoenablesItems:NO]; + + // Add application-specific dock menu items. On error, do not insert the + // dock menu items. + nsresult rv; + nsCOMPtr<nsIMacDockSupport> dockSupport = + do_GetService("@mozilla.org/widget/macdocksupport;1", &rv); + if (NS_FAILED(rv) || !dockSupport) return menu; + + nsCOMPtr<nsIStandaloneNativeMenu> dockMenuInterface; + rv = dockSupport->GetDockMenu(getter_AddRefs(dockMenuInterface)); + if (NS_FAILED(rv) || !dockMenuInterface) return menu; + + RefPtr<mozilla::widget::NativeMenuMac> dockMenu = + static_cast<nsStandaloneNativeMenu*>(dockMenuInterface.get())->GetNativeMenu(); + + // Give the menu the opportunity to update itself before display. + dockMenu->MenuWillOpen(); + + // Obtain a copy of the native menu. + NSMenu* nativeDockMenu = dockMenu->NativeNSMenu(); + if (!nativeDockMenu) { + return menu; + } + + // Loop through the application-specific dock menu and insert its + // contents into the dock menu that we are building for Cocoa. + int numDockMenuItems = [nativeDockMenu numberOfItems]; + if (numDockMenuItems > 0) { + if ([menu numberOfItems] > 0) [menu addItem:[NSMenuItem separatorItem]]; + + for (int i = 0; i < numDockMenuItems; i++) { + NSMenuItem* itemCopy = [[nativeDockMenu itemAtIndex:i] copy]; + [menu addItem:itemCopy]; + [itemCopy release]; + } + } + + return menu; + + NS_OBJC_END_TRY_BLOCK_RETURN(nil); +} + +- (void)applicationWillFinishLaunching:(NSNotification*)notification { + // We provide our own full screen menu item, so we don't want the OS providing + // one as well. + [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"NSFullScreenMenuItemEverywhere"]; +} + +// If we don't handle applicationShouldTerminate:, a call to [NSApp terminate:] +// (from the browser or from the OS) can result in an unclean shutdown. +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*)sender { + nsCOMPtr<nsIObserverService> obsServ = do_GetService("@mozilla.org/observer-service;1"); + if (!obsServ) return NSTerminateNow; + + nsCOMPtr<nsISupportsPRBool> cancelQuit = do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID); + if (!cancelQuit) return NSTerminateNow; + + cancelQuit->SetData(false); + obsServ->NotifyObservers(cancelQuit, "quit-application-requested", nullptr); + + bool abortQuit; + cancelQuit->GetData(&abortQuit); + if (abortQuit) return NSTerminateCancel; + + nsCOMPtr<nsIAppStartup> appService = do_GetService("@mozilla.org/toolkit/app-startup;1"); + if (appService) { + bool userAllowedQuit = true; + appService->Quit(nsIAppStartup::eForceQuit, 0, &userAllowedQuit); + if (!userAllowedQuit) { + return NSTerminateCancel; + } + } + + return NSTerminateNow; +} + +- (void)handleAppleEvent:(NSAppleEventDescriptor*)event + withReplyEvent:(NSAppleEventDescriptor*)replyEvent { + if (!event) return; + + AutoAutoreleasePool pool; + + bool isGetURLEvent = ([event eventClass] == kInternetEventClass && [event eventID] == kAEGetURL); + if (isGetURLEvent) sProcessedGetURLEvent = true; + + if (isGetURLEvent || ([event eventClass] == 'WWW!' && [event eventID] == 'OURL')) { + NSString* urlString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; + NSURL* url = [NSURL URLWithString:urlString]; + + [self openURL:url]; + } else if ([event eventClass] == kCoreEventClass && [event eventID] == kAEOpenDocuments) { + NSAppleEventDescriptor* fileListDescriptor = [event paramDescriptorForKeyword:keyDirectObject]; + if (!fileListDescriptor) return; + + // Descriptor list indexing is one-based... + NSInteger numberOfFiles = [fileListDescriptor numberOfItems]; + for (NSInteger i = 1; i <= numberOfFiles; i++) { + NSString* urlString = [[fileListDescriptor descriptorAtIndex:i] stringValue]; + if (!urlString) continue; + + // We need a path, not a URL + NSURL* url = [NSURL URLWithString:urlString]; + if (!url) continue; + + [self application:NSApp openFile:[url path]]; + } + } +} + +- (BOOL)application:(NSApplication*)application + willContinueUserActivityWithType:(NSString*)userActivityType { + return [userActivityType isEqualToString:NSUserActivityTypeBrowsingWeb]; +} + +- (BOOL)application:(NSApplication*)application + continueUserActivity:(NSUserActivity*)userActivity +#if defined(MAC_OS_X_VERSION_10_14) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_14 + restorationHandler:(void (^)(NSArray<id<NSUserActivityRestoring>>*))restorationHandler { +#else + restorationHandler:(void (^)(NSArray*))restorationHandler { +#endif + if (![userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { + return NO; + } + + return [self openURL:userActivity.webpageURL]; +} + +- (void)application:(NSApplication*)application + didFailToContinueUserActivityWithType:(NSString*)userActivityType + error:(NSError*)error { + NSLog(@"Failed to continue user activity %@: %@", userActivityType, error); +} + +- (BOOL)openURL:(NSURL*)url { + if (!url || !url.scheme || [url.scheme caseInsensitiveCompare:@"chrome"] == NSOrderedSame) { + return NO; + } + + const char* const urlString = [[url absoluteString] UTF8String]; + // Add the URL to any command line we're currently setting up. + if (CommandLineServiceMac::AddURLToCurrentCommandLine(urlString)) { + return NO; + } + + nsCOMPtr<nsICommandLineRunner> cmdLine(new nsCommandLine()); + nsCOMPtr<nsIFile> workingDir; + nsresult rv = NS_GetSpecialDirectory(NS_OS_CURRENT_WORKING_DIR, getter_AddRefs(workingDir)); + if (NS_FAILED(rv)) { + // Couldn't find a working dir. Uh oh. Good job cmdline::Init can cope. + workingDir = nullptr; + } + + const char* argv[3] = {nullptr, "-url", urlString}; + rv = cmdLine->Init(3, argv, workingDir, nsICommandLine::STATE_REMOTE_EXPLICIT); + if (NS_FAILED(rv)) { + return NO; + } + rv = cmdLine->Run(); + if (NS_FAILED(rv)) { + return NO; + } + + return YES; +} + +@end diff --git a/toolkit/xre/MacAutoreleasePool.h b/toolkit/xre/MacAutoreleasePool.h new file mode 100644 index 0000000000..6c574beef2 --- /dev/null +++ b/toolkit/xre/MacAutoreleasePool.h @@ -0,0 +1,31 @@ +/* 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/. */ + +#ifndef MacAutoreleasePool_h_ +#define MacAutoreleasePool_h_ + +// This needs to be #include-able from non-ObjC code in nsAppRunner.cpp +#ifdef __OBJC__ +@class NSAutoreleasePool; +#else +class NSAutoreleasePool; +#endif + +namespace mozilla { + +class MacAutoreleasePool { + public: + MacAutoreleasePool(); + ~MacAutoreleasePool(); + + private: + NSAutoreleasePool* mPool; + + MacAutoreleasePool(const MacAutoreleasePool&); + void operator=(const MacAutoreleasePool&); +}; + +} // namespace mozilla + +#endif // MacAutoreleasePool_h_ diff --git a/toolkit/xre/MacAutoreleasePool.mm b/toolkit/xre/MacAutoreleasePool.mm new file mode 100644 index 0000000000..0d2a47b3d8 --- /dev/null +++ b/toolkit/xre/MacAutoreleasePool.mm @@ -0,0 +1,17 @@ +/* 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 "MacAutoreleasePool.h" +#include "nsDebug.h" + +#import <Foundation/Foundation.h> + +using mozilla::MacAutoreleasePool; + +MacAutoreleasePool::MacAutoreleasePool() { + mPool = [[NSAutoreleasePool alloc] init]; + NS_ASSERTION(mPool != nullptr, "failed to create pool, objects will leak"); +} + +MacAutoreleasePool::~MacAutoreleasePool() { [mPool release]; } diff --git a/toolkit/xre/MacLaunchHelper.h b/toolkit/xre/MacLaunchHelper.h new file mode 100644 index 0000000000..a4a8fb1666 --- /dev/null +++ b/toolkit/xre/MacLaunchHelper.h @@ -0,0 +1,25 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef MacLaunchHelper_h_ +#define MacLaunchHelper_h_ + +#include <stdint.h> + +#include <unistd.h> + +extern "C" { +/** + * Passing an aPid parameter to LaunchChildMac will wait for the launched + * process to terminate. When the process terminates, aPid will be set to the + * pid of the terminated process to confirm that it executed successfully. + */ +void LaunchChildMac(int aArgc, char** aArgv, pid_t* aPid = 0); +bool LaunchElevatedUpdate(int aArgc, char** aArgv, pid_t* aPid = 0); +bool InstallPrivilegedHelper(); +void AbortElevatedUpdate(); +} + +#endif diff --git a/toolkit/xre/MacLaunchHelper.mm b/toolkit/xre/MacLaunchHelper.mm new file mode 100644 index 0000000000..555e15d084 --- /dev/null +++ b/toolkit/xre/MacLaunchHelper.mm @@ -0,0 +1,117 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "MacLaunchHelper.h" + +#include "MacAutoreleasePool.h" +#include "mozilla/UniquePtr.h" + +#include <Cocoa/Cocoa.h> +#include <crt_externs.h> +#include <ServiceManagement/ServiceManagement.h> +#include <Security/Authorization.h> +#include <spawn.h> +#include <stdio.h> + +using namespace mozilla; + +void LaunchChildMac(int aArgc, char** aArgv, pid_t* aPid) { + MacAutoreleasePool pool; + + @try { + NSString* launchPath = [NSString stringWithUTF8String:aArgv[0]]; + NSMutableArray* arguments = [NSMutableArray arrayWithCapacity:aArgc - 1]; + for (int i = 1; i < aArgc; i++) { + [arguments addObject:[NSString stringWithUTF8String:aArgv[i]]]; + } + NSTask* child = [NSTask launchedTaskWithLaunchPath:launchPath arguments:arguments]; + if (aPid) { + *aPid = [child processIdentifier]; + // We used to use waitpid to wait for the process to terminate. This is + // incompatible with NSTask and we wait for the process to exit here + // instead. + [child waitUntilExit]; + } + } @catch (NSException* e) { + NSLog(@"%@: %@", e.name, e.reason); + } +} + +bool InstallPrivilegedHelper() { + AuthorizationRef authRef = NULL; + OSStatus status = AuthorizationCreate( + NULL, kAuthorizationEmptyEnvironment, + kAuthorizationFlagDefaults | kAuthorizationFlagInteractionAllowed, &authRef); + if (status != errAuthorizationSuccess) { + // AuthorizationCreate really shouldn't fail. + NSLog(@"AuthorizationCreate failed! NSOSStatusErrorDomain / %d", (int)status); + return NO; + } + + BOOL result = NO; + AuthorizationItem authItem = {kSMRightBlessPrivilegedHelper, 0, NULL, 0}; + AuthorizationRights authRights = {1, &authItem}; + AuthorizationFlags flags = kAuthorizationFlagDefaults | kAuthorizationFlagInteractionAllowed | + kAuthorizationFlagPreAuthorize | kAuthorizationFlagExtendRights; + + // Obtain the right to install our privileged helper tool. + status = + AuthorizationCopyRights(authRef, &authRights, kAuthorizationEmptyEnvironment, flags, NULL); + if (status != errAuthorizationSuccess) { + NSLog(@"AuthorizationCopyRights failed! NSOSStatusErrorDomain / %d", (int)status); + } else { + CFErrorRef cfError; + // This does all the work of verifying the helper tool against the + // application and vice-versa. Once verification has passed, the embedded + // launchd.plist is extracted and placed in /Library/LaunchDaemons and then + // loaded. The executable is placed in /Library/PrivilegedHelperTools. + result = (BOOL)SMJobBless(kSMDomainSystemLaunchd, (CFStringRef) @"org.mozilla.updater", authRef, + &cfError); + if (!result) { + NSLog(@"Unable to install helper!"); + CFRelease(cfError); + } + } + + return result; +} + +void AbortElevatedUpdate() { + mozilla::MacAutoreleasePool pool; + + id updateServer = nil; + int currTry = 0; + const int numRetries = 10; // Number of IPC connection retries before + // giving up. + while (currTry < numRetries) { + @try { + updateServer = (id)[NSConnection + rootProxyForConnectionWithRegisteredName:@"org.mozilla.updater.server" + host:nil + usingNameServer:[NSSocketPortNameServer sharedInstance]]; + if (updateServer && [updateServer respondsToSelector:@selector(abort)]) { + [updateServer performSelector:@selector(abort)]; + return; + } + NSLog(@"Server doesn't exist or doesn't provide correct selectors."); + sleep(1); // Wait 1 second. + currTry++; + } @catch (NSException* e) { + NSLog(@"Encountered exception, retrying: %@: %@", e.name, e.reason); + sleep(1); // Wait 1 second. + currTry++; + } + } + NSLog(@"Unable to clean up updater."); +} + +bool LaunchElevatedUpdate(int aArgc, char** aArgv, pid_t* aPid) { + LaunchChildMac(aArgc, aArgv, aPid); + bool didSucceed = InstallPrivilegedHelper(); + if (!didSucceed) { + AbortElevatedUpdate(); + } + return didSucceed; +} diff --git a/toolkit/xre/MacRunFromDmgUtils.h b/toolkit/xre/MacRunFromDmgUtils.h new file mode 100644 index 0000000000..d93e6445b1 --- /dev/null +++ b/toolkit/xre/MacRunFromDmgUtils.h @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +// This file defines the interface between Cocoa-specific Obj-C++ and generic +// C++, so it itself cannot have any Obj-C bits in it. + +#ifndef MacRunFromDmgUtils_h_ +#define MacRunFromDmgUtils_h_ + +namespace mozilla::MacRunFromDmgUtils { + +/** + * Returns true if the app is running from the read-only filesystem of a + * mounted .dmg. Returns false if not, or if we fail to determine whether it + * is. + */ +bool IsAppRunningFromDmg(); + +/** + * Checks whether the app is running from a read-only .dmg image or a read-only + * app translocated location and, if so, asks the user for permission before + * attempting to install the app and launch it. + * + * Returns true if the app has been installed and relaunched, in which case + * this instance of the app should exit. + */ +bool MaybeInstallAndRelaunch(); + +} // namespace mozilla::MacRunFromDmgUtils + +#endif diff --git a/toolkit/xre/MacRunFromDmgUtils.mm b/toolkit/xre/MacRunFromDmgUtils.mm new file mode 100644 index 0000000000..38381b5c48 --- /dev/null +++ b/toolkit/xre/MacRunFromDmgUtils.mm @@ -0,0 +1,513 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 <AppKit/AppKit.h> +#include <ApplicationServices/ApplicationServices.h> +#include <CoreFoundation/CoreFoundation.h> +#include <CoreServices/CoreServices.h> +#include <IOKit/IOKitLib.h> +#include <stdlib.h> +#include <string.h> +#include <sys/mount.h> +#include <sys/param.h> + +#include "MacRunFromDmgUtils.h" +#include "MacLaunchHelper.h" + +#include "mozilla/ErrorResult.h" +#include "mozilla/glean/GleanMetrics.h" +#include "mozilla/intl/Localization.h" +#include "mozilla/Telemetry.h" +#include "nsCocoaFeatures.h" +#include "nsCocoaUtils.h" +#include "nsCommandLine.h" +#include "nsCommandLineServiceMac.h" +#include "nsILocalFileMac.h" +#include "nsIMacDockSupport.h" +#include "nsObjCExceptions.h" +#include "prenv.h" +#include "nsString.h" +#ifdef MOZ_UPDATER +# include "nsUpdateDriver.h" +#endif +#include "SDKDeclarations.h" + +// For IOKit docs, see: +// https://developer.apple.com/documentation/iokit +// https://developer.apple.com/library/archive/documentation/DeviceDrivers/Conceptual/IOKitFundamentals/ + +namespace mozilla::MacRunFromDmgUtils { + +/** + * Opens a dialog to ask the user whether the existing app in the Applications + * folder should be launched, or if the user wants to proceed with launching + * the app from the .dmg. + * Returns true if the dialog is successfully opened and the user chooses to + * launch the app from the Applications folder, otherwise returns false. + */ +static bool AskUserIfWeShouldLaunchExistingInstall() { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + // Try to get the localized strings: + nsTArray<nsCString> resIds = { + "branding/brand.ftl"_ns, + "toolkit/global/run-from-dmg.ftl"_ns, + }; + RefPtr<intl::Localization> l10n = intl::Localization::Create(resIds, true); + + ErrorResult rv; + nsAutoCString mozTitle, mozMessage, mozLaunchExisting, mozLaunchFromDMG; + l10n->FormatValueSync("prompt-to-launch-existing-app-title"_ns, {}, mozTitle, rv); + if (rv.Failed()) { + return false; + } + l10n->FormatValueSync("prompt-to-launch-existing-app-message"_ns, {}, mozMessage, rv); + if (rv.Failed()) { + return false; + } + l10n->FormatValueSync("prompt-to-launch-existing-app-yes-button"_ns, {}, mozLaunchExisting, rv); + if (rv.Failed()) { + return false; + } + l10n->FormatValueSync("prompt-to-launch-existing-app-no-button"_ns, {}, mozLaunchFromDMG, rv); + if (rv.Failed()) { + return false; + } + + NSString* title = [NSString stringWithUTF8String:reinterpret_cast<const char*>(mozTitle.get())]; + NSString* message = + [NSString stringWithUTF8String:reinterpret_cast<const char*>(mozMessage.get())]; + NSString* launchExisting = + [NSString stringWithUTF8String:reinterpret_cast<const char*>(mozLaunchExisting.get())]; + NSString* launchFromDMG = + [NSString stringWithUTF8String:reinterpret_cast<const char*>(mozLaunchFromDMG.get())]; + + NSAlert* alert = [[[NSAlert alloc] init] autorelease]; + + // Note that we don't set an icon since the app icon is used by default. + [alert setAlertStyle:NSAlertStyleInformational]; + [alert setMessageText:title]; + [alert setInformativeText:message]; + // Note that if the user hits 'Enter' the "Install" button is activated, + // whereas if they hit 'Space' the "Don't Install" button is activated. + // That's standard behavior so probably desirable. + [alert addButtonWithTitle:launchExisting]; + NSButton* launchFromDMGButton = [alert addButtonWithTitle:launchFromDMG]; + // Since the "Don't Install" button doesn't have the title "Cancel" we need + // to map the Escape key to it manually: + [launchFromDMGButton setKeyEquivalent:@"\e"]; + + __block NSInteger result = -1; + dispatch_async(dispatch_get_main_queue(), ^{ + result = [alert runModal]; + [NSApp stop:nil]; + }); + + // We need to call run on NSApp here for accessibility. See + // AskUserIfWeShouldInstall for a detailed explanation. + [NSApp run]; + MOZ_ASSERT(result != -1); + + return result == NSAlertFirstButtonReturn; + + NS_OBJC_END_TRY_BLOCK_RETURN(false); +} + +/** + * Opens a dialog to ask the user whether the app should be installed to their + * Applications folder. Returns true if the dialog is successfully opened and + * the user accept, otherwise returns false. + */ +static bool AskUserIfWeShouldInstall() { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + // Try to get the localized strings: + nsTArray<nsCString> resIds = { + "branding/brand.ftl"_ns, + "toolkit/global/run-from-dmg.ftl"_ns, + }; + RefPtr<intl::Localization> l10n = intl::Localization::Create(resIds, true); + + ErrorResult rv; + nsAutoCString mozTitle, mozMessage, mozInstall, mozDontInstall; + l10n->FormatValueSync("prompt-to-install-title"_ns, {}, mozTitle, rv); + if (rv.Failed()) { + return false; + } + l10n->FormatValueSync("prompt-to-install-message"_ns, {}, mozMessage, rv); + if (rv.Failed()) { + return false; + } + l10n->FormatValueSync("prompt-to-install-yes-button"_ns, {}, mozInstall, rv); + if (rv.Failed()) { + return false; + } + l10n->FormatValueSync("prompt-to-install-no-button"_ns, {}, mozDontInstall, rv); + if (rv.Failed()) { + return false; + } + + NSString* title = [NSString stringWithUTF8String:reinterpret_cast<const char*>(mozTitle.get())]; + NSString* message = + [NSString stringWithUTF8String:reinterpret_cast<const char*>(mozMessage.get())]; + NSString* install = + [NSString stringWithUTF8String:reinterpret_cast<const char*>(mozInstall.get())]; + NSString* dontInstall = + [NSString stringWithUTF8String:reinterpret_cast<const char*>(mozDontInstall.get())]; + + NSAlert* alert = [[[NSAlert alloc] init] autorelease]; + + // Note that we don't set an icon since the app icon is used by default. + [alert setAlertStyle:NSAlertStyleInformational]; + [alert setMessageText:title]; + [alert setInformativeText:message]; + // Note that if the user hits 'Enter' the "Install" button is activated, + // whereas if they hit 'Space' the "Don't Install" button is activated. + // That's standard behavior so probably desirable. + [alert addButtonWithTitle:install]; + NSButton* dontInstallButton = [alert addButtonWithTitle:dontInstall]; + // Since the "Don't Install" button doesn't have the title "Cancel" we need + // to map the Escape key to it manually: + [dontInstallButton setKeyEquivalent:@"\e"]; + + // We need to call run on NSApp to allow accessibility. We only run it + // for this specific alert which blocks the app's loop until the user + // responds, it then subsequently stops the app's loop. + // + // AskUserIfWeShouldInstall + // | + // | ---> [NSApp run] + // | | + // | | -----> task + // | | | -----------> [alert runModal] + // | | | | (User selects button) + // | | | <--------------- done + // | | | + // | | | -----------> [NSApp stop:nil] + // | | | <----------- + // | | <-------- + // | <------- + // done + __block NSInteger result = -1; + dispatch_async(dispatch_get_main_queue(), ^{ + result = [alert runModal]; + [NSApp stop:nil]; + }); + + [NSApp run]; + MOZ_ASSERT(result != -1); + + return result == NSAlertFirstButtonReturn; + + NS_OBJC_END_TRY_BLOCK_RETURN(false); +} + +static void ShowInstallFailedDialog() { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + // Try to get the localized strings: + nsTArray<nsCString> resIds = { + "branding/brand.ftl"_ns, + "toolkit/global/run-from-dmg.ftl"_ns, + }; + RefPtr<intl::Localization> l10n = intl::Localization::Create(resIds, true); + + ErrorResult rv; + nsAutoCString mozTitle, mozMessage; + l10n->FormatValueSync("install-failed-title"_ns, {}, mozTitle, rv); + if (rv.Failed()) { + return; + } + l10n->FormatValueSync("install-failed-message"_ns, {}, mozMessage, rv); + if (rv.Failed()) { + return; + } + + NSString* title = [NSString stringWithUTF8String:reinterpret_cast<const char*>(mozTitle.get())]; + NSString* message = + [NSString stringWithUTF8String:reinterpret_cast<const char*>(mozMessage.get())]; + + NSAlert* alert = [[[NSAlert alloc] init] autorelease]; + + [alert setAlertStyle:NSAlertStyleWarning]; + [alert setMessageText:title]; + [alert setInformativeText:message]; + + __block NSInteger result = -1; + dispatch_async(dispatch_get_main_queue(), ^{ + result = [alert runModal]; + [NSApp stop:nil]; + }); + + // We need to call run on NSApp here for accessibility. See + // AskUserIfWeShouldInstall for a detailed explanation. + [NSApp run]; + MOZ_ASSERT(result != -1); + (void)result; + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +/** + * Helper to launch macOS tasks via NSTask. + */ +static void LaunchTask(NSString* aPath, NSArray* aArguments) { + if (@available(macOS 10.13, *)) { + NSTask* task = [[NSTask alloc] init]; + [task setExecutableURL:[NSURL fileURLWithPath:aPath]]; + if (aArguments) { + [task setArguments:aArguments]; + } + [task launchAndReturnError:nil]; + [task release]; + } else { + NSArray* arguments = aArguments; + if (!arguments) { + arguments = @[]; + } + [NSTask launchedTaskWithLaunchPath:aPath arguments:arguments]; + } +} + +static void LaunchInstalledApp(NSString* aBundlePath) { + LaunchTask([[NSBundle bundleWithPath:aBundlePath] executablePath], nil); +} + +static void RegisterAppWithLaunchServices(NSString* aBundlePath) { + NSArray* arguments = @[ @"-f", aBundlePath ]; + LaunchTask(@"/System/Library/Frameworks/CoreServices.framework/Frameworks/" + @"LaunchServices.framework/Support/lsregister", + arguments); +} + +static void StripQuarantineBit(NSString* aBundlePath) { + NSArray* arguments = @[ @"-d", @"com.apple.quarantine", aBundlePath ]; + LaunchTask(@"/usr/bin/xattr", arguments); +} + +#ifdef MOZ_UPDATER +bool LaunchElevatedDmgInstall(NSString* aBundlePath, NSArray* aArguments) { + NSTask* task; + if (@available(macOS 10.13, *)) { + task = [[NSTask alloc] init]; + [task setExecutableURL:[NSURL fileURLWithPath:aBundlePath]]; + if (aArguments) { + [task setArguments:aArguments]; + } + [task launchAndReturnError:nil]; + } else { + NSArray* arguments = aArguments; + if (!arguments) { + arguments = @[]; + } + task = [NSTask launchedTaskWithLaunchPath:aBundlePath arguments:arguments]; + } + + bool didSucceed = InstallPrivilegedHelper(); + [task waitUntilExit]; + if (@available(macOS 10.13, *)) { + [task release]; + } + if (!didSucceed) { + AbortElevatedUpdate(); + } + + return didSucceed; +} +#endif + +// Note: both arguments are expected to contain the app name (to end with +// '.app'). +static bool InstallFromPath(NSString* aBundlePath, NSString* aDestPath) { + bool installSuccessful = false; + NSFileManager* fileManager = [NSFileManager defaultManager]; + if ([fileManager copyItemAtPath:aBundlePath toPath:aDestPath error:nil]) { + RegisterAppWithLaunchServices(aDestPath); + StripQuarantineBit(aDestPath); + installSuccessful = true; + } + +#ifdef MOZ_UPDATER + // The installation may have been unsuccessful if the user did not have the + // rights to write to the Applications directory. Check for this situation and + // launch an elevated installation if necessary. Rather than creating a new, + // dedicated executable for this installation and incurring the + // added maintenance burden of yet another executable, we are using the + // updater binary. Since bug 394984 landed, the updater has the ability to + // install and launch itself as a Privileged Helper tool, which is what is + // necessary here. + NSString* destDir = [aDestPath stringByDeletingLastPathComponent]; + if (!installSuccessful && ![fileManager isWritableFileAtPath:destDir]) { + NSString* updaterBinPath = [NSString pathWithComponents:@[ + aBundlePath, @"Contents", @"MacOS", [NSString stringWithUTF8String:UPDATER_APP], @"Contents", + @"MacOS", [NSString stringWithUTF8String:UPDATER_BIN] + ]]; + + NSArray* arguments = @[ @"-dmgInstall", aBundlePath, aDestPath ]; + LaunchElevatedDmgInstall(updaterBinPath, arguments); + installSuccessful = [fileManager fileExistsAtPath:aDestPath]; + } +#endif + + if (!installSuccessful) { + return false; + } + + // Pin to dock: + nsresult rv; + nsCOMPtr<nsIMacDockSupport> dockSupport = + do_GetService("@mozilla.org/widget/macdocksupport;1", &rv); + if (NS_SUCCEEDED(rv) && dockSupport) { + bool isInDock; + nsAutoString appPath, appToReplacePath; + nsCocoaUtils::GetStringForNSString(aDestPath, appPath); + nsCocoaUtils::GetStringForNSString(aBundlePath, appToReplacePath); + dockSupport->EnsureAppIsPinnedToDock(appPath, appToReplacePath, &isInDock); + } + + return true; +} + +bool IsAppRunningFromDmg() { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + const char* path = [[[NSBundle mainBundle] bundlePath] fileSystemRepresentation]; + + struct statfs statfsBuf; + if (statfs(path, &statfsBuf) != 0) { + return false; + } + + // Optimization to minimize impact on startup time: + if (!(statfsBuf.f_flags & MNT_RDONLY)) { + return false; + } + + // Get the "BSD device name" ("diskXsY", as found in /dev) of the filesystem + // our app is on so we can use IOKit to get its IOMedia object: + const char devDirPath[] = "/dev/"; + const int devDirPathLength = strlen(devDirPath); + if (strncmp(statfsBuf.f_mntfromname, devDirPath, devDirPathLength) != 0) { + // This has been observed to happen under App Translocation. In this case, + // path is the translocated path, and f_mntfromname is the path under + // /Volumes. Do another stat on that path. + nsCString volumesPath(statfsBuf.f_mntfromname); + if (statfs(volumesPath.get(), &statfsBuf) != 0) { + return false; + } + + if (strncmp(statfsBuf.f_mntfromname, devDirPath, devDirPathLength) != 0) { + // It still doesn't begin with /dev/, bail out. + return false; + } + } + const char* bsdDeviceName = statfsBuf.f_mntfromname + devDirPathLength; + + // Get the IOMedia object: + // (Note: IOServiceGetMatchingServices takes ownership of serviceDict's ref.) + CFMutableDictionaryRef serviceDict = IOBSDNameMatching(kIOMasterPortDefault, 0, bsdDeviceName); + if (!serviceDict) { + return false; + } + io_service_t media = IOServiceGetMatchingService(kIOMasterPortDefault, serviceDict); + if (!media || !IOObjectConformsTo(media, "IOMedia")) { + return false; + } + + // Search the parent chain for a service implementing the disk image class + // (taking care to start with `media` itself): + io_service_t imageDrive = IO_OBJECT_NULL; + io_iterator_t iter; + if (IORegistryEntryCreateIterator(media, kIOServicePlane, + kIORegistryIterateRecursively | kIORegistryIterateParents, + &iter) != KERN_SUCCESS) { + IOObjectRelease(media); + return false; + } + const char* imageClass = + nsCocoaFeatures::OnMontereyOrLater() ? "AppleDiskImageDevice" : "IOHDIXHDDrive"; + for (imageDrive = media; imageDrive; imageDrive = IOIteratorNext(iter)) { + if (IOObjectConformsTo(imageDrive, imageClass)) { + break; + } + IOObjectRelease(imageDrive); + } + IOObjectRelease(iter); + + if (imageDrive) { + IOObjectRelease(imageDrive); + return true; + } + return false; + + NS_OBJC_END_TRY_BLOCK_RETURN(false); +} + +bool MaybeInstallAndRelaunch() { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + @autoreleasepool { + bool isFromDmg = IsAppRunningFromDmg(); + bool isTranslocated = false; + if (!isFromDmg) { + NSString* bundlePath = [[NSBundle mainBundle] bundlePath]; + if ([bundlePath containsString:@"/AppTranslocation/"]) { + isTranslocated = true; + } + } + + if (!isFromDmg && !isTranslocated) { + return false; + } + + // The Applications directory may not be at /Applications, although in + // practice we're unlikely to encounter since run-from-.dmg is really an + // issue with novice mac users. Still, look it up correctly: + NSArray* applicationsDirs = + NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSLocalDomainMask, YES); + NSString* applicationsDir = applicationsDirs[0]; + + // Sanity check dir exists + NSFileManager* fileManager = [NSFileManager defaultManager]; + BOOL isDir; + if (![fileManager fileExistsAtPath:applicationsDir isDirectory:&isDir] || !isDir) { + return false; + } + + NSString* bundlePath = [[NSBundle mainBundle] bundlePath]; + NSString* appName = [bundlePath lastPathComponent]; + NSString* destPath = [applicationsDir stringByAppendingPathComponent:appName]; + + // If the app (an app of the same name) is already installed we can't really + // tell without asking if we're dealing with the edge case of an + // inexperienced user running from .dmg by mistake, or if we're dealing with + // a more sophisticated user intentionally running from .dmg. + if ([fileManager fileExistsAtPath:destPath]) { + if (AskUserIfWeShouldLaunchExistingInstall()) { + StripQuarantineBit(destPath); + LaunchInstalledApp(destPath); + return true; + } + return false; + } + + if (!AskUserIfWeShouldInstall()) { + return false; + } + + if (!InstallFromPath(bundlePath, destPath)) { + ShowInstallFailedDialog(); + return false; + } + + LaunchInstalledApp(destPath); + + return true; + } + + NS_OBJC_END_TRY_BLOCK_RETURN(false); +} + +} // namespace mozilla::MacRunFromDmgUtils diff --git a/toolkit/xre/MultiInstanceLock.cpp b/toolkit/xre/MultiInstanceLock.cpp new file mode 100644 index 0000000000..eb4db6367a --- /dev/null +++ b/toolkit/xre/MultiInstanceLock.cpp @@ -0,0 +1,297 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "MultiInstanceLock.h" + +#include "commonupdatedir.h" // for GetInstallHash +#include "mozilla/UniquePtr.h" +#include "nsPrintfCString.h" +#include "nsPromiseFlatString.h" +#include "nsXULAppAPI.h" +#include "updatedefines.h" // for NS_t* definitions + +#ifdef XP_WIN +# include <shlwapi.h> +#else +# include <fcntl.h> +# include <sys/stat.h> +# include <sys/types.h> +#endif + +#ifdef XP_WIN +# include "WinUtils.h" +#endif + +#ifdef MOZ_WIDGET_COCOA +# include "nsILocalFileMac.h" +#endif + +namespace mozilla { + +bool GetMultiInstanceLockFileName(const char* nameToken, + const char16_t* installPath, + nsCString& filePath) { +#ifdef XP_WIN + // On Windows, the lock file is placed at the path + // [updateDirectory]\[nameToken]-[pathHash], so first we need to get the + // update directory path and then append the file name. + + // Note: This will return something like + // C:\ProgramData\Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\updates\<hash> + // But we actually are going to want to return the root update directory, + // the grandparent of this directory, which will look something like this: + // C:\ProgramData\Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38 + mozilla::UniquePtr<wchar_t[]> updateDir; + HRESULT hr = GetCommonUpdateDirectory( + reinterpret_cast<const wchar_t*>(installPath), updateDir); + if (FAILED(hr)) { + return false; + } + + // For the path manipulation that we are about to do, it is important that + // the update directory have no trailing slash. + size_t len = wcslen(updateDir.get()); + if (len == 0) { + return false; + } + if (updateDir.get()[len - 1] == '/' || updateDir.get()[len - 1] == '\\') { + updateDir.get()[len - 1] = '\0'; + } + + wchar_t* hashPtr = PathFindFileNameW(updateDir.get()); + // PathFindFileNameW returns a pointer to the beginning of the string on + // failure. + if (hashPtr == updateDir.get()) { + return false; + } + + // We need to make a copy of the hash before we modify updateDir to get the + // root update dir. + size_t hashSize = wcslen(hashPtr) + 1; + mozilla::UniquePtr<wchar_t[]> hash = mozilla::MakeUnique<wchar_t[]>(hashSize); + errno_t error = wcscpy_s(hash.get(), hashSize, hashPtr); + if (error != 0) { + return false; + } + + // Get the root update dir from the update dir. + BOOL success = PathRemoveFileSpecW(updateDir.get()); + if (!success) { + return false; + } + success = PathRemoveFileSpecW(updateDir.get()); + if (!success) { + return false; + } + + filePath = + nsPrintfCString("%s\\%s-%s", NS_ConvertUTF16toUTF8(updateDir.get()).get(), + nameToken, NS_ConvertUTF16toUTF8(hash.get()).get()); + +#else + mozilla::UniquePtr<NS_tchar[]> pathHash; + if (!GetInstallHash(installPath, pathHash)) { + return false; + } + + // On POSIX platforms the base path is /tmp/[vendor][nameToken]-[pathHash]. + filePath = nsPrintfCString("/tmp/%s%s-%s", MOZ_APP_VENDOR, nameToken, + pathHash.get()); + +#endif + + return true; +} + +MultiInstLockHandle OpenMultiInstanceLock(const char* nameToken, + const char16_t* installPath) { + nsCString filePath; + if (!GetMultiInstanceLockFileName(nameToken, installPath, filePath)) { + return MULTI_INSTANCE_LOCK_HANDLE_ERROR; + } + + // Open a file handle with full privileges and sharing, and then attempt to + // take a shared (nonexclusive, read-only) lock on it. +#ifdef XP_WIN + HANDLE h = + ::CreateFileW(PromiseFlatString(NS_ConvertUTF8toUTF16(filePath)).get(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_ALWAYS, 0, nullptr); + if (h != INVALID_HANDLE_VALUE) { + // The LockFileEx functions always require an OVERLAPPED structure even + // though we did not open the lock file for overlapped I/O. + OVERLAPPED o = {0}; + if (!::LockFileEx(h, LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &o)) { + CloseHandle(h); + h = INVALID_HANDLE_VALUE; + } + } + return h; + +#else + int fd = ::open(PromiseFlatCString(filePath).get(), + O_CLOEXEC | O_CREAT | O_NOFOLLOW, + S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); + if (fd != -1) { + // We would like to ensure that the lock file is deleted when we are done + // with it. The normal way to do that would be to call unlink on it right + // now, but that would immediately delete the name from the file system, and + // we need other instances to be able to open that name and get the same + // inode, so we can't unlink the file before we're done with it. This means + // we accept some unreliability in getting the file deleted, but it's a zero + // byte file in the tmp directory, so having it stay around isn't the worst. + struct flock l = {0}; + l.l_start = 0; + l.l_len = 0; + l.l_type = F_RDLCK; + if (::fcntl(fd, F_SETLK, &l)) { + ::close(fd); + fd = -1; + } + } + return fd; + +#endif +} + +void ReleaseMultiInstanceLock(MultiInstLockHandle lock) { + if (lock != MULTI_INSTANCE_LOCK_HANDLE_ERROR) { +#ifdef XP_WIN + OVERLAPPED o = {0}; + ::UnlockFileEx(lock, 0, 1, 0, &o); + ::CloseHandle(lock); + +#else + // If we're the last instance, then unlink the lock file. There is a race + // condition here that may cause an instance to fail to open the same inode + // as another even though they use the same path, but there's no reasonable + // way to avoid that without skipping deleting the file at all, so we accept + // that risk. + bool otherInstance = true; + if (IsOtherInstanceRunning(lock, &otherInstance) && !otherInstance) { + // Recover the file's path so we can unlink it. + // There's no error checking in here because we're content to let the file + // hang around if any of this fails (which can happen if for example we're + // on a system where /proc/self/fd does not exist); this is a zero-byte + // file in the tmp directory after all. + UniquePtr<NS_tchar[]> linkPath = MakeUnique<NS_tchar[]>(MAXPATHLEN + 1); + NS_tsnprintf(linkPath.get(), MAXPATHLEN + 1, "/proc/self/fd/%d", lock); + UniquePtr<NS_tchar[]> lockFilePath = + MakeUnique<NS_tchar[]>(MAXPATHLEN + 1); + if (::readlink(linkPath.get(), lockFilePath.get(), MAXPATHLEN + 1) != + -1) { + ::unlink(lockFilePath.get()); + } + } + // Now close the lock file, which will release the lock. + ::close(lock); +#endif + } +} + +bool IsOtherInstanceRunning(MultiInstLockHandle lock, bool* aResult) { + // Every running instance has opened a readonly lock, and read locks prevent + // write locks from being opened, so to see if we are the only instance, we + // attempt to take a write lock, and if it succeeds then that must mean there + // are no other read locks open and therefore no other instances. + if (lock == MULTI_INSTANCE_LOCK_HANDLE_ERROR) { + return false; + } + +#ifdef XP_WIN + // We need to release the lock we're holding before we would be allowed to + // take an exclusive lock, and if that succeeds we need to release it too + // in order to get our shared lock back. This procedure is not atomic, so we + // accept the risk of the scheduler deciding to ruin our day between these + // operations; we'd get a false negative in a different instance's check. + OVERLAPPED o = {0}; + // Release our current shared lock. + if (!::UnlockFileEx(lock, 0, 1, 0, &o)) { + return false; + } + // Attempt to take an exclusive lock. + bool rv = false; + if (::LockFileEx(lock, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, 0, + 1, 0, &o)) { + // We got the exclusive lock, so now release it. + ::UnlockFileEx(lock, 0, 1, 0, &o); + *aResult = false; + rv = true; + } else if (::GetLastError() == ERROR_LOCK_VIOLATION) { + // We didn't get the exclusive lock because of outstanding shared locks. + *aResult = true; + rv = true; + } + // Attempt to reclaim the shared lock we released at the beginning. + if (!::LockFileEx(lock, LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &o)) { + rv = false; + } + return rv; + +#else + // See if we would be allowed to set a write lock (no need to actually do so). + struct flock l = {0}; + l.l_start = 0; + l.l_len = 0; + l.l_type = F_WRLCK; + if (::fcntl(lock, F_GETLK, &l)) { + return false; + } + *aResult = l.l_type != F_UNLCK; + return true; + +#endif +} + +already_AddRefed<nsIFile> GetNormalizedAppFile(nsIFile* aAppFile) { + // If we're given an app file, use it; otherwise, get it from the ambient + // directory service. + nsresult rv; + nsCOMPtr<nsIFile> appFile; + if (aAppFile) { + rv = aAppFile->Clone(getter_AddRefs(appFile)); + NS_ENSURE_SUCCESS(rv, nullptr); + } else { + nsCOMPtr<nsIProperties> dirSvc = + do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID); + NS_ENSURE_TRUE(dirSvc, nullptr); + + rv = dirSvc->Get(XRE_EXECUTABLE_FILE, NS_GET_IID(nsIFile), + getter_AddRefs(appFile)); + NS_ENSURE_SUCCESS(rv, nullptr); + } + + // It is possible that the path we have is on a case insensitive + // filesystem in which case the path may vary depending on how the + // application is called. We want to normalize the case somehow. + // On Linux XRE_EXECUTABLE_FILE already seems to be set to the correct path. + // + // See similar nsXREDirProvider::GetInstallHash. The main difference here is + // to allow lookup to fail on OSX, because some tests use a nonexistent + // appFile. +#ifdef XP_WIN + // Windows provides a way to get the correct case. + if (!mozilla::widget::WinUtils::ResolveJunctionPointsAndSymLinks(appFile)) { + NS_WARNING("Failed to resolve install directory."); + } +#elif defined(MOZ_WIDGET_COCOA) + // On OSX roundtripping through an FSRef fixes the case. + FSRef ref; + nsCOMPtr<nsILocalFileMac> macFile = do_QueryInterface(appFile); + if (macFile && NS_SUCCEEDED(macFile->GetFSRef(&ref)) && + NS_SUCCEEDED( + NS_NewLocalFileWithFSRef(&ref, true, getter_AddRefs(macFile)))) { + appFile = static_cast<nsIFile*>(macFile); + } else { + NS_WARNING("Failed to resolve install directory."); + } +#endif + + return appFile.forget(); +} + +}; // namespace mozilla diff --git a/toolkit/xre/MultiInstanceLock.h b/toolkit/xre/MultiInstanceLock.h new file mode 100644 index 0000000000..1d6a93e7a8 --- /dev/null +++ b/toolkit/xre/MultiInstanceLock.h @@ -0,0 +1,99 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#ifndef MULTIINSTANCELOCK_H +#define MULTIINSTANCELOCK_H + +#ifdef XP_WIN +# include <windows.h> +#endif +#include "nsIFile.h" + +// These functions manage "multi-instance locks", which are a type of lock +// specifically designed to allow instances of an application, process, or other +// task to detect when other instances relevant to them are running. Each +// instance opens a lock and holds it for the duration of the task of interest +// (which may be the lifetime of the process, or a shorter period). Then while +// the lock is open, it can be used to check whether any other instances of the +// same task are currently running out of the same copy of the binary, in the +// context of any OS user. A process can open any number of locks, so long as +// they use different names. It is necessary for the process to have permission +// to create files in /tmp/ on POSIX systems or ProgramData\[vendor]\ on +// Windows, so this mechanism may not work for sandboxed processes. + +// The implementation is based on file locking. An empty file is created in a +// systemwide (not per-user) location, and a shared (read) lock is taken on that +// file; the value that OpenMultiInstanceLock() returns is the file +// handle/descriptor. When you call IsOtherInstanceRunning(), it will attempt to +// convert that shared lock into an exclusive (write) lock. If that operation +// would succeed, it means that there must not be any other shared locks +// currently taken on that file, so we know there are no other instances +// running. This is a more complex design than most file locks or most other +// concurrency mechanisms, but it is necessary for this use case because of the +// requirement that an instance must be able to detect other instances that were +// started later than it was. If, say, a mutex were used, or another kind of +// exclusive lock, then the first instance that tried to take it would succeed, +// and be unable to tell that another instance had tried to take it later and +// failed. This mechanism allows any number of instances started at any time in +// relation to one another to always be able to detect that the others exist +// (although it does not allow you to know how many others exist). The lock is +// guaranteed to be released if the process holding it crashes or is exec'd into +// something else, because the file is closed when that happens. The file itself +// is not necessarily always deleted on POSIX, because it isn't possible (within +// reason) to guarantee that unlink() is called, but the file is empty and +// created in the /tmp directory, so should not be a serious problem. + +namespace mozilla { + +#ifdef XP_WIN +using MultiInstLockHandle = HANDLE; +# define MULTI_INSTANCE_LOCK_HANDLE_ERROR INVALID_HANDLE_VALUE +#else +using MultiInstLockHandle = int; +# define MULTI_INSTANCE_LOCK_HANDLE_ERROR -1 +#endif + +/* + * nameToken should be a string very briefly naming the lock you are creating + * creating, and it should be unique except for across multiple instances of the + * same application. The vendor name is included in the generated path, so it + * doesn't need to be present in your supplied name. Try to keep this name sort + * of short, ideally under about 64 characters, because creating the lock will + * fail if the final path string (the token + the path hash + the vendor name) + * is longer than the platform's maximum path and/or path component length. + * + * installPath should be the path to the directory containing the application, + * which will be used to form a path specific to that installation. + * + * Returns MULTI_INSTANCE_LOCK_HANDLE_ERROR upon failure, or a handle which can + * later be passed to the other functions declared here upon success. + */ +MultiInstLockHandle OpenMultiInstanceLock(const char* nameToken, + const char16_t* installPath); + +void ReleaseMultiInstanceLock(MultiInstLockHandle lock); + +// aResult will be set to true if another instance *was* found, false if not. +// Return value is true on success, false on error (and aResult won't be set). +bool IsOtherInstanceRunning(MultiInstLockHandle lock, bool* aResult); + +// It is possible that the path we have is on a case insensitive +// filesystem in which case the path may vary depending on how the +// application is called. We want to normalize the case somehow. +// When aAppFile is NULL, this function returns a nsIFile with a normalized +// path for the currently running binary. When aAppFile is not null, +// this function ensures the file path is properly normalized. +already_AddRefed<nsIFile> GetNormalizedAppFile(nsIFile* aAppFile); + +// Computes the file path of multi instance lock +// Returns true when successful - false otherwise +bool GetMultiInstanceLockFileName(const char* nameToken, + const char16_t* installPath, + nsCString& filePath); + +}; // namespace mozilla + +#endif // MULTIINSTANCELOCK_H diff --git a/toolkit/xre/PolicyChecks.h b/toolkit/xre/PolicyChecks.h new file mode 100644 index 0000000000..a876ff917d --- /dev/null +++ b/toolkit/xre/PolicyChecks.h @@ -0,0 +1,66 @@ +/* -*- 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/. */ + +#ifndef mozilla_PolicyChecks_h +#define mozilla_PolicyChecks_h + +#if defined(XP_WIN) + +# include <windows.h> +# include "mozilla/Maybe.h" + +# define POLICY_REGKEY_NAME L"SOFTWARE\\Policies\\Mozilla\\" MOZ_APP_BASENAME + +// NB: This code must be able to run apart from XPCOM + +namespace mozilla { + +// Returns Some(true) if the registry value is 1 +// Returns Some(false) if the registry value is not 1 +// Returns Nothing() if the registry value is not present +inline Maybe<bool> PolicyHasRegValueOfOne(HKEY aKey, LPCWSTR aName) { + { + DWORD len = sizeof(DWORD); + DWORD value; + LONG ret = ::RegGetValueW(aKey, POLICY_REGKEY_NAME, aName, RRF_RT_DWORD, + nullptr, &value, &len); + if (ret == ERROR_SUCCESS) { + return Some(value == 1); + } + } + { + ULONGLONG value; + DWORD len = sizeof(ULONGLONG); + LONG ret = ::RegGetValueW(aKey, POLICY_REGKEY_NAME, aName, RRF_RT_QWORD, + nullptr, &value, &len); + if (ret == ERROR_SUCCESS) { + return Some(value == 1); + } + } + return Nothing(); +} + +inline bool PolicyCheckBoolean(LPCWSTR aPolicyName) { + Maybe<bool> localMachineResult = + PolicyHasRegValueOfOne(HKEY_LOCAL_MACHINE, aPolicyName); + if (localMachineResult.isSome()) { + return localMachineResult.value(); + } + + Maybe<bool> currentUserResult = + PolicyHasRegValueOfOne(HKEY_CURRENT_USER, aPolicyName); + if (currentUserResult.isSome()) { + return currentUserResult.value(); + } + + return false; +} + +} // namespace mozilla + +#endif // defined(XP_WIN) + +#endif // mozilla_PolicyChecks_h diff --git a/toolkit/xre/ProfileReset.cpp b/toolkit/xre/ProfileReset.cpp new file mode 100644 index 0000000000..8ed01e6535 --- /dev/null +++ b/toolkit/xre/ProfileReset.cpp @@ -0,0 +1,146 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsIAppStartup.h" +#include "nsIFile.h" +#include "nsIStringBundle.h" +#include "nsIToolkitProfile.h" +#include "nsIWindowWatcher.h" + +#include "ProfileReset.h" + +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsPIDOMWindow.h" +#include "nsString.h" +#include "mozilla/Components.h" +#include "mozilla/XREAppData.h" + +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/Unused.h" + +using namespace mozilla; + +extern const XREAppData* gAppData; + +static const char kProfileProperties[] = + "chrome://mozapps/locale/profile/profileSelection.properties"; + +/** + * Spin up a thread to backup the old profile's main directory and delete the + * profile's local directory. Once complete have the profile service remove the + * old profile and if necessary make the new profile the default. + */ +nsresult ProfileResetCleanup(nsToolkitProfileService* aService, + nsIToolkitProfile* aOldProfile) { + nsresult rv; + nsCOMPtr<nsIFile> profileDir; + rv = aOldProfile->GetRootDir(getter_AddRefs(profileDir)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIFile> profileLocalDir; + rv = aOldProfile->GetLocalDir(getter_AddRefs(profileLocalDir)); + if (NS_FAILED(rv)) return rv; + + // Get the friendly name for the backup directory. + nsCOMPtr<nsIStringBundleService> sbs = + mozilla::components::StringBundle::Service(); + if (!sbs) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIStringBundle> sb; + Unused << sbs->CreateBundle(kProfileProperties, getter_AddRefs(sb)); + if (!sb) return NS_ERROR_FAILURE; + + NS_ConvertUTF8toUTF16 appName(gAppData->name); + AutoTArray<nsString, 2> params = {appName, appName}; + + nsAutoString resetBackupDirectoryName; + + static const char* kResetBackupDirectory = "resetBackupDirectory"; + rv = sb->FormatStringFromName(kResetBackupDirectory, params, + resetBackupDirectoryName); + if (NS_FAILED(rv)) return rv; + + // Get info to copy the old root profile dir to the desktop as a backup. + nsCOMPtr<nsIFile> backupDest, containerDest, profileDest; + rv = NS_GetSpecialDirectory(NS_OS_DESKTOP_DIR, getter_AddRefs(backupDest)); + if (NS_FAILED(rv)) { + // Fall back to the home directory if the desktop is not available. + rv = NS_GetSpecialDirectory(NS_OS_HOME_DIR, getter_AddRefs(backupDest)); + if (NS_FAILED(rv)) return rv; + } + + // Try to create a directory for all the backups + backupDest->Clone(getter_AddRefs(containerDest)); + containerDest->Append(resetBackupDirectoryName); + rv = containerDest->Create(nsIFile::DIRECTORY_TYPE, 0700); + // It's OK if it already exists, if and only if it is a directory + if (rv == NS_ERROR_FILE_ALREADY_EXISTS) { + bool containerIsDir; + rv = containerDest->IsDirectory(&containerIsDir); + if (NS_FAILED(rv) || !containerIsDir) { + return rv; + } + } else if (NS_FAILED(rv)) { + return rv; + } + + // Get the name of the profile + nsAutoString leafName; + rv = profileDir->GetLeafName(leafName); + if (NS_FAILED(rv)) return rv; + + // Try to create a unique directory for the profile: + containerDest->Clone(getter_AddRefs(profileDest)); + profileDest->Append(leafName); + rv = profileDest->CreateUnique(nsIFile::DIRECTORY_TYPE, 0700); + if (NS_FAILED(rv)) return rv; + + // Get the unique profile name + rv = profileDest->GetLeafName(leafName); + if (NS_FAILED(rv)) return rv; + + // Delete the empty directory that CreateUnique just created. + rv = profileDest->Remove(false); + if (NS_FAILED(rv)) return rv; + + // Show a progress window while the cleanup happens since the disk I/O can + // take time. + nsCOMPtr<nsIWindowWatcher> windowWatcher( + do_GetService(NS_WINDOWWATCHER_CONTRACTID)); + if (!windowWatcher) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIAppStartup> appStartup(components::AppStartup::Service()); + if (!appStartup) return NS_ERROR_FAILURE; + + nsCOMPtr<mozIDOMWindowProxy> progressWindow; + rv = windowWatcher->OpenWindow(nullptr, nsDependentCString(kResetProgressURL), + "_blank"_ns, "centerscreen,chrome,titlebar"_ns, + nullptr, getter_AddRefs(progressWindow)); + if (NS_FAILED(rv)) return rv; + + // Create a new thread to do the bulk of profile cleanup to stay responsive. + nsCOMPtr<nsIThread> cleanupThread; + rv = NS_NewNamedThread("ResetCleanup", getter_AddRefs(cleanupThread)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIRunnable> runnable = new ProfileResetCleanupAsyncTask( + profileDir, profileLocalDir, containerDest, leafName); + cleanupThread->Dispatch(runnable, nsIThread::DISPATCH_NORMAL); + // The result callback will shut down the worker thread. + + // Wait for the cleanup thread to complete. + SpinEventLoopUntil("xre:ProfileResetCreateBackup"_ns, + [&]() { return gProfileResetCleanupCompleted; }); + } else { + gProfileResetCleanupCompleted = true; + NS_WARNING("Cleanup thread creation failed"); + return rv; + } + // Close the progress window now that the cleanup thread is done. + auto* piWindow = nsPIDOMWindowOuter::From(progressWindow); + piWindow->Close(); + + return aService->ApplyResetProfile(aOldProfile); +} diff --git a/toolkit/xre/ProfileReset.h b/toolkit/xre/ProfileReset.h new file mode 100644 index 0000000000..71a52e7252 --- /dev/null +++ b/toolkit/xre/ProfileReset.h @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsToolkitProfileService.h" +#include "nsIFile.h" +#include "nsIThread.h" +#include "nsThreadUtils.h" + +static bool gProfileResetCleanupCompleted = false; +static const char kResetProgressURL[] = + "chrome://global/content/resetProfileProgress.xhtml"; + +nsresult ProfileResetCleanup(nsToolkitProfileService* aService, + nsIToolkitProfile* aOldProfile); + +class ProfileResetCleanupResultTask : public mozilla::Runnable { + public: + ProfileResetCleanupResultTask() + : mozilla::Runnable("ProfileResetCleanupResultTask"), + mWorkerThread(do_GetCurrentThread()) { + MOZ_ASSERT(!NS_IsMainThread()); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + mWorkerThread->Shutdown(); + return NS_OK; + } + + private: + nsCOMPtr<nsIThread> mWorkerThread; +}; + +class ProfileResetCleanupAsyncTask : public mozilla::Runnable { + public: + ProfileResetCleanupAsyncTask(nsIFile* aProfileDir, nsIFile* aProfileLocalDir, + nsIFile* aTargetDir, const nsAString& aLeafName) + : mozilla::Runnable("ProfileResetCleanupAsyncTask"), + mProfileDir(aProfileDir), + mProfileLocalDir(aProfileLocalDir), + mTargetDir(aTargetDir), + mLeafName(aLeafName) {} + + /** + * Copy a root profile to a backup folder before deleting it. Then delete the + * local profile dir. + */ + NS_IMETHOD Run() override { + // Copy profile's files to the destination. The profile folder will be + // removed after the changes to the known profiles have been flushed to disk + // in nsToolkitProfileService::ApplyResetProfile which isn't called until + // after this thread finishes copying the files. + nsresult rv = mProfileDir->CopyToFollowingLinks(mTargetDir, mLeafName); + // I guess we just warn if we fail to make the backup? + if (NS_WARN_IF(NS_FAILED(rv))) { + NS_WARNING("Could not backup the root profile directory"); + } + + // If we have a separate local cache profile directory, just delete it. + // Don't return an error if this fails so that reset can proceed if it can't + // be deleted. + bool sameDir; + nsresult rvLocal = mProfileDir->Equals(mProfileLocalDir, &sameDir); + if (NS_SUCCEEDED(rvLocal) && !sameDir) { + rvLocal = mProfileLocalDir->Remove(true); + if (NS_FAILED(rvLocal)) { + NS_WARNING("Could not remove the old local profile directory (cache)"); + } + } + gProfileResetCleanupCompleted = true; + + nsCOMPtr<nsIRunnable> resultRunnable = new ProfileResetCleanupResultTask(); + NS_DispatchToMainThread(resultRunnable); + return NS_OK; + } + + private: + nsCOMPtr<nsIFile> mProfileDir; + nsCOMPtr<nsIFile> mProfileLocalDir; + nsCOMPtr<nsIFile> mTargetDir; + nsString mLeafName; +}; diff --git a/toolkit/xre/SafeMode.h b/toolkit/xre/SafeMode.h new file mode 100644 index 0000000000..9323ff1a9c --- /dev/null +++ b/toolkit/xre/SafeMode.h @@ -0,0 +1,90 @@ +/* -*- 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/. */ + +#ifndef mozilla_SafeMode_h +#define mozilla_SafeMode_h + +// NB: This code must be able to run apart from XPCOM + +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/Maybe.h" + +#if defined(XP_WIN) +# include "mozilla/PolicyChecks.h" +# include <windows.h> +#endif // defined(XP_WIN) + +// Undo X11/X.h's definition of None +#undef None + +namespace mozilla { + +enum class SafeModeFlag : uint32_t { + None = 0, + Unset = (1 << 0), + NoKeyPressCheck = (1 << 1), +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(SafeModeFlag) + +template <typename CharT> +inline Maybe<bool> IsSafeModeRequested( + int& aArgc, CharT* aArgv[], + const SafeModeFlag aFlags = SafeModeFlag::Unset) { + CheckArgFlag checkArgFlags = CheckArgFlag::None; + if (aFlags & SafeModeFlag::Unset) { + checkArgFlags |= CheckArgFlag::RemoveArg; + } + + ArgResult ar = CheckArg(aArgc, aArgv, "safe-mode", nullptr, checkArgFlags); + if (ar == ARG_BAD) { + return Nothing(); + } + + bool result = ar == ARG_FOUND; + +#if defined(XP_WIN) + // If the shift key is pressed and the ctrl and / or alt keys are not pressed + // during startup, start in safe mode. GetKeyState returns a short and the + // high order bit will be 1 if the key is pressed. By masking the returned + // short with 0x8000 the result will be 0 if the key is not pressed and + // non-zero otherwise. + if (!(aFlags & SafeModeFlag::NoKeyPressCheck) && + (GetKeyState(VK_SHIFT) & 0x8000) && !(GetKeyState(VK_CONTROL) & 0x8000) && + !(GetKeyState(VK_MENU) & 0x8000) && + !EnvHasValue("MOZ_DISABLE_SAFE_MODE_KEY")) { + result = true; + } + + if (result && PolicyCheckBoolean(L"DisableSafeMode")) { + result = false; + } +#endif // defined(XP_WIN) + +#if defined(XP_MACOSX) + if (!(aFlags & SafeModeFlag::NoKeyPressCheck) && + (GetCurrentEventKeyModifiers() & optionKey) && + !EnvHasValue("MOZ_DISABLE_SAFE_MODE_KEY")) { + result = true; + } +#endif // defined(XP_MACOSX) + + // The Safe Mode Policy should not be enforced for the env var case + // (used by updater and crash-recovery). + if (EnvHasValue("MOZ_SAFE_MODE_RESTART")) { + result = true; + if (aFlags & SafeModeFlag::Unset) { + // unset the env variable + SaveToEnv("MOZ_SAFE_MODE_RESTART="); + } + } + + return Some(result); +} + +} // namespace mozilla + +#endif // mozilla_SafeMode_h diff --git a/toolkit/xre/UIKitDirProvider.h b/toolkit/xre/UIKitDirProvider.h new file mode 100644 index 0000000000..ff7ad6b3b9 --- /dev/null +++ b/toolkit/xre/UIKitDirProvider.h @@ -0,0 +1,13 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef TOOLKIT_XRE_UIKITDIRPROVIDER_H_ +#define TOOLKIT_XRE_UIKITDIRPROVIDER_H_ + +#include "nsString.h" + +bool GetUIKitDirectory(bool aLocal, nsACString& aUserDir); + +#endif // TOOLKIT_XRE_UIKITDIRPROVIDER_H_ diff --git a/toolkit/xre/UIKitDirProvider.mm b/toolkit/xre/UIKitDirProvider.mm new file mode 100644 index 0000000000..043ecbe2ec --- /dev/null +++ b/toolkit/xre/UIKitDirProvider.mm @@ -0,0 +1,18 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 <Foundation/Foundation.h> + +#include "UIKitDirProvider.h" + +bool GetUIKitDirectory(bool aLocal, nsACString& aUserDir) { + NSSearchPathDirectory directory = aLocal ? NSCachesDirectory : NSApplicationSupportDirectory; + NSArray* paths = NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES); + if ([paths count] == 0) { + return false; + } + aUserDir = [[paths objectAtIndex:0] UTF8String]; + return true; +} diff --git a/toolkit/xre/WinTokenUtils.cpp b/toolkit/xre/WinTokenUtils.cpp new file mode 100644 index 0000000000..4774ec6dad --- /dev/null +++ b/toolkit/xre/WinTokenUtils.cpp @@ -0,0 +1,72 @@ +/* -*- 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 "WinTokenUtils.h" +#include "nsWindowsHelpers.h" + +using namespace mozilla; + +// If |aToken| is nullptr, CheckTokenMembership uses the calling thread's +// primary token to check membership for. +static LauncherResult<bool> IsMemberOfAdministrators( + const nsAutoHandle& aToken) { + BYTE adminsGroupSid[SECURITY_MAX_SID_SIZE]; + DWORD adminsGroupSidSize = sizeof(adminsGroupSid); + if (!CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, adminsGroupSid, + &adminsGroupSidSize)) { + return LAUNCHER_ERROR_FROM_LAST(); + } + + BOOL isMember; + if (!CheckTokenMembership(aToken, adminsGroupSid, &isMember)) { + return LAUNCHER_ERROR_FROM_LAST(); + } + return !!isMember; +} + +static LauncherResult<bool> IsUacEnabled() { + DWORD len = sizeof(DWORD); + DWORD value; + LSTATUS status = RegGetValueW( + HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System", + L"EnableLUA", RRF_RT_DWORD, nullptr, &value, &len); + if (status != ERROR_SUCCESS) { + return LAUNCHER_ERROR_FROM_WIN32(status); + } + + // UAC is disabled only when EnableLUA is 0. + return (value != 0); +} + +namespace mozilla { + +LauncherResult<bool> IsAdminWithoutUac() { + // To check whether the process was launched with Administrator priviledges + // or not, we cannot simply check the integrity level of the current process + // because the launcher process spawns the browser process with the medium + // integrity level even though the launcher process is high integrity level. + // We check whether the thread's token contains Administratos SID or not + // instead. + LauncherResult<bool> containsAdminGroup = + IsMemberOfAdministrators(nsAutoHandle()); + if (containsAdminGroup.isErr()) { + return containsAdminGroup.propagateErr(); + } + + if (!containsAdminGroup.unwrap()) { + return false; + } + + LauncherResult<bool> isUacEnabled = IsUacEnabled(); + if (isUacEnabled.isErr()) { + return isUacEnabled.propagateErr(); + } + + return !isUacEnabled.unwrap(); +} + +} // namespace mozilla diff --git a/toolkit/xre/WinTokenUtils.h b/toolkit/xre/WinTokenUtils.h new file mode 100644 index 0000000000..48b9ec5a8b --- /dev/null +++ b/toolkit/xre/WinTokenUtils.h @@ -0,0 +1,18 @@ +/* -*- 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/. */ + +#ifndef mozilla_WinTokenUtils_h +#define mozilla_WinTokenUtils_h + +#include "mozilla/WinHeaderOnlyUtils.h" + +namespace mozilla { + +LauncherResult<bool> IsAdminWithoutUac(); + +} // namespace mozilla + +#endif // mozilla_WinTokenUtils_h diff --git a/toolkit/xre/components.conf b/toolkit/xre/components.conf new file mode 100644 index 0000000000..b59964e794 --- /dev/null +++ b/toolkit/xre/components.conf @@ -0,0 +1,71 @@ +# -*- 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/. + +if defined('MOZ_CRASHREPORTER'): + crash_reporter = ['@mozilla.org/toolkit/crash-reporter;1'] +else: + crash_reporter = [] + +Classes = [ + { + 'name': 'XULRuntime', + 'js_name': 'appinfo', + 'cid': '{95d89e3e-a169-41a3-8e56-719978e15b12}', + 'contract_ids': [ + '@mozilla.org/xre/app-info;1', + '@mozilla.org/xre/runtime;1', + ] + crash_reporter, + 'interfaces': ['nsIXULRuntime', 'nsIXULAppInfo', 'nsICrashReporter'], + 'legacy_constructor': 'mozilla::AppInfoConstructor', + 'headers': ['nsAppRunner.h'], + 'processes': ProcessSelector.ALLOW_IN_SOCKET_PROCESS, + 'overridable': True, + }, + { + 'cid': '{471f4944-1dd2-11b2-87ac-90be0a51d609}', + 'contract_ids': ['@mozilla.org/embedcomp/rangefind;1'], + 'type': 'nsFind', + 'headers': ['/toolkit/components/find/nsFind.h'], + }, + { + 'cid': '{7e677795-c582-4cd1-9e8d-8271b3474d2a}', + 'contract_ids': ['@mozilla.org/embedding/browser/nsWebBrowserPersist;1'], + 'type': 'nsWebBrowserPersist', + 'headers': ['/dom/webbrowserpersist/nsWebBrowserPersist.h'], + }, + { + 'js_name': 'ww', + 'cid': '{a21bfa01-f349-4394-a84c-8de5cf0737d0}', + 'contract_ids': ['@mozilla.org/embedcomp/window-watcher;1'], + 'interfaces': ['nsIWindowWatcher'], + 'type': 'nsWindowWatcher', + 'headers': ['nsWindowWatcher.h'], + 'init_method': 'Init', + 'overridable': True, + }, + { + 'cid': '{5573967d-f6cf-4c63-8e0e-9ac06e04d62b}', + 'contract_ids': ['@mozilla.org/xre/directory-provider;1'], + 'singleton': True, + 'type': 'nsXREDirProvider', + 'constructor': 'nsXREDirProvider::GetSingleton', + 'headers': ['/toolkit/xre/nsXREDirProvider.h'], + }, + { + 'cid': '{4e4aae11-8901-46cc-8217-dad7c5415873}', + 'contract_ids': ['@mozilla.org/embedcomp/dialogparam;1'], + 'type': 'nsDialogParamBlock', + 'headers': ['/toolkit/components/windowwatcher/nsDialogParamBlock.h'], + }, + { + 'cid': '{5f5e59ce-27bc-47eb-9d1f-b09ca9049836}', + 'contract_ids': ['@mozilla.org/toolkit/profile-service;1'], + 'singleton': True, + 'constructor': 'NS_GetToolkitProfileService', + 'type': 'nsToolkitProfileService', + 'headers': ['/toolkit/profile/nsToolkitProfileService.h'], + }, +] diff --git a/toolkit/xre/detect_win32k_conflicts.h b/toolkit/xre/detect_win32k_conflicts.h new file mode 100644 index 0000000000..62ff737bcd --- /dev/null +++ b/toolkit/xre/detect_win32k_conflicts.h @@ -0,0 +1,20 @@ +/* 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/. */ + +#ifndef TOOLKIT_XRE_DETECT_WIN32K_CONFLICTS_H +#define TOOLKIT_XRE_DETECT_WIN32K_CONFLICTS_H +#include <cinttypes> + +// C interface for the `detect_win32k_conflicts` Rust crate + +struct ConflictingMitigationStatus { + bool caller_check; + bool sim_exec; + bool stack_pivot; +}; + +extern "C" bool detect_win32k_conflicting_mitigations( + ConflictingMitigationStatus* aConflictingMitigationStatus); + +#endif // TOOLKIT_XRE_DETECT_WIN32K_CONFLICTS_H diff --git a/toolkit/xre/detect_win32k_conflicts/.gitignore b/toolkit/xre/detect_win32k_conflicts/.gitignore new file mode 100644 index 0000000000..869df07dae --- /dev/null +++ b/toolkit/xre/detect_win32k_conflicts/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock
\ No newline at end of file diff --git a/toolkit/xre/detect_win32k_conflicts/Cargo.toml b/toolkit/xre/detect_win32k_conflicts/Cargo.toml new file mode 100644 index 0000000000..a94402df9e --- /dev/null +++ b/toolkit/xre/detect_win32k_conflicts/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "detect_win32k_conflicts" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[dependencies] +log = "0.4" +thiserror = "1" +winapi = { version = "0.3", features = ["winerror", "winreg"] } diff --git a/toolkit/xre/detect_win32k_conflicts/src/detect.rs b/toolkit/xre/detect_win32k_conflicts/src/detect.rs new file mode 100644 index 0000000000..e21c80e157 --- /dev/null +++ b/toolkit/xre/detect_win32k_conflicts/src/detect.rs @@ -0,0 +1,165 @@ +/* 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/. */ + +//! This module contains the actual functions that explore the registry for the status +//! of the conflicting mitigations. + +use super::error::DetectConflictError; +use super::registry::{RegKey, RegValue}; + +use std::ffi::OsStr; + +/// Subkey that the Windows Exploit Protection status is located in +const EXPLOIT_PROTECTION_SUBKEY: &str = + "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options"; + +/// Name of the value within that subkey that contains the exploit protection status +const MITIGATION_VALUE_NAME: &str = "MitigationOptions"; + +/// The bit number within the value with the StackPivot status +const STACK_PIVOT_BIT: usize = 80; + +/// The bit number within the value with the CallerCheck status +const CALLER_CHECK_BIT: usize = 84; + +/// The bit number within the value with the SimExec status +const SIM_EXEC_BIT: usize = 88; + +/// Printable, FFI-safe structure containing the status of the conflicting mitigations +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct ConflictingMitigationStatus { + /// The status of the CallerCheck feature. 0 = disabled, 1 = enabled + caller_check: bool, + /// The status of the SimExec feature. 0 = disabled, 1 = enabled + sim_exec: bool, + /// The status of the StackPivot feature. 0 = disabled, 1 = enabled + stack_pivot: bool, +} + +impl std::fmt::Display for ConflictingMitigationStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + use std::fmt::Write; + write_status(f, "CallerCheck", self.caller_check)?; + f.write_char(' ')?; + write_status(f, "SimExec", self.sim_exec)?; + f.write_char(' ')?; + write_status(f, "StackPivot", self.stack_pivot) + } +} + +/// Search the registry for win32k-conflicting mitigations for the given process +/// +/// `process_name` is the name of the process that will be checked for conflicting mitigations +/// It uses the same case-insensitive algorithm that the Windows Registry uses itself to detect +/// whether a process needs mitigations or not +/// +/// (This will usually be `firefox.exe`) +/// +/// # Caveat +/// +/// Windows allows an arbitrary number of "filters", which are subkeys of the main key +/// that also have a `MitigationOptions` value. They are used to allow filtering by path. +/// +/// For simplicity, we "flatten" the path filters when we figure this out, and so the +/// returned value will be the logical "OR" of all mitigations enabled with any filter +/// +/// So if there are two entries for "C:\Firefox\firefox.exe" with mitigations `A` and `B` enabled +/// and another entry for "C:\Program Files\Mozilla Firefox\firefox.exe" with mitigation `C` +/// enabled, we will return that `A`, `B`, and `C` are all enabled +pub fn get_conflicting_mitigations( + process_name: impl AsRef<OsStr>, +) -> Result<ConflictingMitigationStatus, DetectConflictError> { + let process_name = process_name.as_ref(); + + let key = RegKey::root_local_machine() + .try_open_subkey(EXPLOIT_PROTECTION_SUBKEY)? + .ok_or(DetectConflictError::ExploitProtectionKeyMissing)?; + + let process_key = match key.try_open_subkey(process_name)? { + Some(key) => key, + None => { + log::info!( + "process name {:?} not found in exploit protection", + process_name + ); + return Ok(ConflictingMitigationStatus::default()); + } + }; + + // First get mitigation status for the root key + let mut status = get_conflicting_mitigations_for_key(&process_key)?; + + // Then get mitigation status for each subkey and logical-or them all together + for subkey_name in process_key.subkey_names() { + let subkey_name = subkey_name?; + + let subkey = process_key + .try_open_subkey(subkey_name)? + .expect("a subkey somehow doesn't exist after enumerating it"); + + let subkey_status = get_conflicting_mitigations_for_key(&subkey)?; + status.caller_check |= subkey_status.caller_check; + status.sim_exec |= subkey_status.sim_exec; + status.stack_pivot |= subkey_status.stack_pivot; + } + + log::info!( + "process name {:?} has mitigation status {:?}", + process_name, + status + ); + + Ok(status) +} + +/// Check a key to see if it has a "MitigationOptions" value with any conflicting mitigations +/// enabled +fn get_conflicting_mitigations_for_key( + key: &RegKey, +) -> Result<ConflictingMitigationStatus, DetectConflictError> { + let value = match key.try_get_value(MITIGATION_VALUE_NAME)? { + Some(value) => value, + None => return Ok(ConflictingMitigationStatus::default()), + }; + + let bits = match value { + RegValue::Binary(bits) => bits, + _ => return Ok(ConflictingMitigationStatus::default()), + }; + + Ok(ConflictingMitigationStatus { + caller_check: get_bit(&bits, CALLER_CHECK_BIT)?, + sim_exec: get_bit(&bits, SIM_EXEC_BIT)?, + stack_pivot: get_bit(&bits, STACK_PIVOT_BIT)?, + }) +} + +/// Retrieve a bit from a vector-of-bitflags, which is what will be returned by the registry +/// entry for exploit protection. The bits are numbered first by increasing value within a byte, +/// and then by increasing index within the slice. +/// +/// Ex: bit 0 = (index 0, mask 0x01), bit 7 = (index 0, mask 0x80), bit 8 = (index 1, mask 0x01) +fn get_bit(bytes: &[u8], bit_idx: usize) -> Result<bool, DetectConflictError> { + let byte = bytes + .get(bit_idx / 8) + .ok_or(DetectConflictError::RegValueTooShort)?; + + Ok(if byte & (1 << (bit_idx % 8)) != 0 { + true + } else { + false + }) +} + +/// Write the status of a feature to the given formatter. `bit == 0` means feature is disabled, +/// `bit == 1` means enabled +fn write_status( + f: &mut std::fmt::Formatter<'_>, + name: &str, + bit: bool, +) -> Result<(), std::fmt::Error> { + let status_str = if bit { "enabled" } else { "disabled" }; + write!(f, "{} is {}.", name, status_str) +} diff --git a/toolkit/xre/detect_win32k_conflicts/src/error.rs b/toolkit/xre/detect_win32k_conflicts/src/error.rs new file mode 100644 index 0000000000..cc73d04b2d --- /dev/null +++ b/toolkit/xre/detect_win32k_conflicts/src/error.rs @@ -0,0 +1,36 @@ +/* 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/. */ + +//! Contains the error type for this crate + +use thiserror::Error; + +/// The error type for this crate +#[derive(Debug, Error)] +pub enum DetectConflictError { + /// An exploit protection key was not found in the registry + #[error("exploit protection key missing")] + ExploitProtectionKeyMissing, + /// Failed to enumerate the next registry subkey + #[error("failed to enumerate next registry subkey. code: {0}")] + RegEnumKeyFailed(u32), + /// Failed to get a value from the registry + #[error("failed to get registry value. code: {0}")] + RegGetValueFailed(u32), + /// Failed to get the length of a value from the registry + #[error("failed to get registry value length. code: {0}")] + RegGetValueLenFailed(u32), + /// Failed to open a registry key + #[error("failed to open registry key. code: {0}")] + RegOpenKeyFailed(u32), + /// Exploit Protection registry value was too short + #[error("exploit protection registry value too short")] + RegValueTooShort, + /// Failed to query key for subkey length + #[error("failed to query key for max subkey length. code: {0}")] + RegQueryInfoKeyFailed(u32), + /// A registry value had an unsupported type + #[error("key has unsupported value type: {0}")] + UnsupportedValueType(u32), +} diff --git a/toolkit/xre/detect_win32k_conflicts/src/ffi.rs b/toolkit/xre/detect_win32k_conflicts/src/ffi.rs new file mode 100644 index 0000000000..826f452726 --- /dev/null +++ b/toolkit/xre/detect_win32k_conflicts/src/ffi.rs @@ -0,0 +1,57 @@ +/* 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/. */ + +//! C foreign-function interface for this crate + +use super::detect::{get_conflicting_mitigations, ConflictingMitigationStatus}; + +/// C FFI for `get_conflicting_mitigations` +/// +/// Checks the current process to see if any conflicting mitigations would be enabled +/// by Windows Exploit Protection +/// +/// `conflicting_mitigations` is an out pointer that will receive the status of the +/// Win32k conflicting mitigations +/// +/// On success, returns `1`. On failure, returns `0` and `conflicting_mitigations` is left +/// untouched +/// +/// # Safety +/// +/// `conflicting_mitigations` must not be a non-const, non-null ptr and **must be initialized** +/// +/// # Example +/// +/// ```C++ +/// void myFunc() { +/// ConflictingMitigationStatus status = {}; // GOOD - value-initializes the object +/// if(!detect_win32k_conflicting_mitigations(&status)) { +/// // error +/// } +/// } +/// ``` +#[no_mangle] +pub unsafe extern "C" fn detect_win32k_conflicting_mitigations( + conflicting_mitigations: &mut ConflictingMitigationStatus, +) -> bool { + let process_path = + std::env::current_exe().expect("unable to determine the path of our executable"); + let file_name = process_path + .file_name() + .expect("executable doesn't have a file name"); + + match get_conflicting_mitigations(file_name) { + Ok(status) => { + *conflicting_mitigations = status; + true + } + Err(e) => { + log::error!( + "Failed to get Win32k Lockdown conflicting mitigation status: {}", + e + ); + false + } + } +} diff --git a/toolkit/xre/detect_win32k_conflicts/src/lib.rs b/toolkit/xre/detect_win32k_conflicts/src/lib.rs new file mode 100644 index 0000000000..4333603b92 --- /dev/null +++ b/toolkit/xre/detect_win32k_conflicts/src/lib.rs @@ -0,0 +1,18 @@ +/* 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/. */ + +//! Crate to detect Windows Exploit Protection mitigations that may interfere with Win32k +//! Lockdown + +#![deny(missing_docs)] + +mod detect; +mod error; +mod ffi; +mod registry; + +pub use self::detect::get_conflicting_mitigations; +pub use self::detect::ConflictingMitigationStatus; +pub use self::error::DetectConflictError; +pub use self::ffi::detect_win32k_conflicting_mitigations; diff --git a/toolkit/xre/detect_win32k_conflicts/src/registry.rs b/toolkit/xre/detect_win32k_conflicts/src/registry.rs new file mode 100644 index 0000000000..f375a64284 --- /dev/null +++ b/toolkit/xre/detect_win32k_conflicts/src/registry.rs @@ -0,0 +1,360 @@ +/* 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/. */ + +//! This module contains helpers for exploring the Windows Registry + +use super::error::DetectConflictError; + +use std::ffi::{OsStr, OsString}; +use std::os::windows::ffi::{OsStrExt, OsStringExt}; + +use winapi::{ + shared::{ + minwindef::HKEY, + winerror::{ERROR_FILE_NOT_FOUND, ERROR_NO_MORE_ITEMS, ERROR_SUCCESS}, + }, + um::{ + winnt::{KEY_READ, REG_BINARY, REG_DWORD, REG_SZ}, + winreg::{ + RegCloseKey, RegEnumKeyExW, RegGetValueW, RegOpenKeyExW, RegQueryInfoKeyW, + HKEY_LOCAL_MACHINE, RRF_RT_REG_BINARY, RRF_RT_REG_DWORD, RRF_RT_REG_SZ, + }, + }, +}; + +/// An open key in the Windows Registry +/// +/// (Note that Windows ref-counts keys internally, so we don't need subkeys to "hang on" to +/// their parents) +#[derive(Debug)] +pub struct RegKey { + /// The Win32 handle of the open key + handle: HKEY, +} + +impl RegKey { + /// Open the global HKEY_LOCAL_MACHINE handle + pub fn root_local_machine() -> RegKey { + RegKey { + handle: HKEY_LOCAL_MACHINE, + } + } + /// Try to open a subkey relative to this key with read-only access rights + /// + /// Returns `None` if the subkey doesn't exist + pub fn try_open_subkey( + &self, + subkey: impl AsRef<OsStr>, + ) -> Result<Option<RegKey>, DetectConflictError> { + let win32_subkey = to_win32_string(subkey.as_ref()); + + let mut subkey_handle = std::ptr::null_mut(); + let rv = unsafe { + RegOpenKeyExW( + self.handle, + win32_subkey.as_ptr(), + 0, // options + KEY_READ, + &mut subkey_handle, + ) + } + .try_into() + .unwrap(); + + match rv { + ERROR_SUCCESS => { + assert!(!subkey_handle.is_null()); + + Ok(Some(RegKey { + handle: subkey_handle, + })) + } + ERROR_FILE_NOT_FOUND => Ok(None), + _ => Err(DetectConflictError::RegOpenKeyFailed(rv)), + } + } + /// Try to get a value with the given name + /// + /// Returns `None` if there is no value with that name + /// + /// Note that you can pass the empty string to get the default value + pub fn try_get_value( + &self, + value_name: impl AsRef<OsStr>, + ) -> Result<Option<RegValue>, DetectConflictError> { + let win32_value_name = to_win32_string(value_name.as_ref()); + + // First we query the value's length and type so we know how to create the data buffer + let mut value_type = 0; + let mut value_len = 0; + + let rv = unsafe { + RegGetValueW( + self.handle, + std::ptr::null(), // subkey + win32_value_name.as_ptr(), + RRF_RT_REG_BINARY | RRF_RT_REG_DWORD | RRF_RT_REG_SZ, + &mut value_type, + std::ptr::null_mut(), // no data ptr, just query length & type + &mut value_len, + ) + } + .try_into() + .unwrap(); + + if rv == ERROR_FILE_NOT_FOUND { + return Ok(None); + } + + if rv != ERROR_SUCCESS || value_len == 0 { + return Err(DetectConflictError::RegGetValueLenFailed(rv)); + } + + if value_type == REG_SZ { + // buffer length is in wide characters + let buffer_len = value_len / 2; + + assert_eq!(buffer_len * 2, value_len); + + let mut buffer = vec![0u16; buffer_len.try_into().unwrap()]; + + let rv = unsafe { + RegGetValueW( + self.handle, + std::ptr::null(), // subkey + win32_value_name.as_ptr(), + RRF_RT_REG_SZ, + std::ptr::null_mut(), // no need to query type again + buffer.as_mut_ptr().cast(), + &mut value_len, + ) + } + .try_into() + .unwrap(); + + if rv != ERROR_SUCCESS { + return Err(DetectConflictError::RegGetValueFailed(rv)); + } + + Ok(Some(RegValue::String(from_win32_string(&buffer)))) + } else if value_type == REG_BINARY { + let mut buffer = vec![0u8; value_len.try_into().unwrap()]; + + let rv = unsafe { + RegGetValueW( + self.handle, + std::ptr::null(), // subkey + win32_value_name.as_ptr(), + RRF_RT_REG_BINARY, + std::ptr::null_mut(), // no need to query type again + buffer.as_mut_ptr().cast(), + &mut value_len, + ) + } + .try_into() + .unwrap(); + + if rv != ERROR_SUCCESS { + return Err(DetectConflictError::RegGetValueFailed(rv)); + } + + Ok(Some(RegValue::Binary(buffer))) + } else if value_type == REG_DWORD { + assert_eq!(value_len, 4); + + let mut buffer = 0u32; + + let rv = unsafe { + RegGetValueW( + self.handle, + std::ptr::null(), // subkey + win32_value_name.as_ptr(), + RRF_RT_REG_DWORD, + std::ptr::null_mut(), // no need to query type again + (&mut buffer as *mut u32).cast(), + &mut value_len, + ) + } + .try_into() + .unwrap(); + + if rv != ERROR_SUCCESS { + return Err(DetectConflictError::RegGetValueFailed(rv)); + } + + Ok(Some(RegValue::Dword(buffer))) + } else { + Err(DetectConflictError::UnsupportedValueType(value_type)) + } + } + /// Get an iterator that returns the names of all the subkeys of this key + pub fn subkey_names(&self) -> SubkeyNames<'_> { + SubkeyNames::new(self) + } +} + +impl Drop for RegKey { + fn drop(&mut self) { + assert!(!self.handle.is_null()); + + if self.handle == HKEY_LOCAL_MACHINE { + return; + } + + let rv: u32 = unsafe { RegCloseKey(self.handle) }.try_into().unwrap(); + + if rv != ERROR_SUCCESS { + log::warn!("failed to close opened registry key"); + } + } +} + +/// An iterator through the subkeys of a key +/// +/// Returns items of type `Result<OsString, DetectConflictError>`. If an error occurs, calling `next()` again +/// will likely just continue to result in errors, so it's best to just abandon ship at that +/// point +#[derive(Debug)] +pub struct SubkeyNames<'a> { + /// The key that is being iterated + key: &'a RegKey, + /// The current index of the subkey -- Incremented each successful call to `next()` + index: u32, + /// A buffer that is large enough to hold the longest subkey name + buffer: Option<Vec<u16>>, +} + +impl<'a> SubkeyNames<'a> { + /// Create a new subkey name iterator + fn new(key: &'a RegKey) -> SubkeyNames<'a> { + SubkeyNames { + key, + index: 0, + buffer: None, + } + } + /// Initialize `self.buffer` with a vector that's long enough to hold the longest subkey name + /// (if it isn't already) + fn create_buffer_if_needed(&mut self) -> Result<(), DetectConflictError> { + if self.buffer.is_some() { + return Ok(()); + } + + let mut max_key_length = 0; + + let rv = unsafe { + RegQueryInfoKeyW( + self.key.handle, + std::ptr::null_mut(), // class ptr + std::ptr::null_mut(), // class len ptr + std::ptr::null_mut(), // reserved + std::ptr::null_mut(), // num of subkeys ptr + &mut max_key_length, + std::ptr::null_mut(), // max class length ptr + std::ptr::null_mut(), // number of values ptr + std::ptr::null_mut(), // longest value name ptr + std::ptr::null_mut(), // longest value data ptr + std::ptr::null_mut(), // DACL ptr + std::ptr::null_mut(), // last write time ptr + ) + } + .try_into() + .unwrap(); + + if rv != ERROR_SUCCESS { + return Err(DetectConflictError::RegQueryInfoKeyFailed(rv)); + } + + // +1 for the null terminator + let max_key_length = usize::try_from(max_key_length).unwrap() + 1; + + self.buffer = Some(vec![0u16; max_key_length]); + + Ok(()) + } +} + +impl<'a> Iterator for SubkeyNames<'a> { + type Item = Result<OsString, DetectConflictError>; + + fn next(&mut self) -> Option<Self::Item> { + if let Err(e) = self.create_buffer_if_needed() { + return Some(Err(e)); + } + + let buffer = self.buffer.as_mut().unwrap(); + let mut name_len = u32::try_from(buffer.len()).unwrap(); + + let rv = unsafe { + RegEnumKeyExW( + self.key.handle, + self.index, + buffer.as_mut_ptr(), + &mut name_len, + std::ptr::null_mut(), // reserved + std::ptr::null_mut(), // class ptr + std::ptr::null_mut(), // class len ptr + std::ptr::null_mut(), // last write time ptr + ) + } + .try_into() + .unwrap(); + + if rv == ERROR_NO_MORE_ITEMS { + return None; + } + + if rv != ERROR_SUCCESS { + return Some(Err(DetectConflictError::RegEnumKeyFailed(rv))); + } + + self.index += 1; + + // +1 for the null terminator + let name_len = usize::try_from(name_len).unwrap() + 1; + + Some(Ok(from_win32_string(&buffer[0..name_len]))) + } +} + +/// A single value in the registry +#[derive(Clone, Debug, PartialEq)] +pub enum RegValue { + /// A REG_BINARY value + Binary(Vec<u8>), + /// A REG_DWORD value + Dword(u32), + /// A REG_SZ value + String(OsString), +} + +/// Convert an OsStr to a null-terminated wide character string +/// +/// The input string must not contain any interior null values, and the resulting wide +/// character array will have a null terminator appended +fn to_win32_string(s: &OsStr) -> Vec<u16> { + // +1 for the null terminator + let mut result = Vec::with_capacity(s.len() + 1); + + for wc in s.encode_wide() { + assert_ne!(wc, 0); + result.push(wc); + } + + result.push(0); + result +} + +/// Convert a null-terminated wide character string into an OsString +/// +/// The passed-in slice must have exactly one wide null character as the last element +fn from_win32_string(s: &[u16]) -> OsString { + for (idx, wc) in s.iter().enumerate() { + if *wc == 0 { + assert_eq!(idx, s.len() - 1); + return OsString::from_wide(&s[0..idx]); + } + } + panic!("missing null terminator at end of win32 string"); +} diff --git a/toolkit/xre/detect_win32k_conflicts/tests/basic.rs b/toolkit/xre/detect_win32k_conflicts/tests/basic.rs new file mode 100644 index 0000000000..d16a0bd976 --- /dev/null +++ b/toolkit/xre/detect_win32k_conflicts/tests/basic.rs @@ -0,0 +1,14 @@ +/* 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/. */ + +use detect_win32k_conflicts::{detect_win32k_conflicting_mitigations, ConflictingMitigationStatus}; + +#[test] +fn basic() { + let mut status = ConflictingMitigationStatus::default(); + + let result = unsafe { detect_win32k_conflicting_mitigations(&mut status) }; + + assert!(result); +} diff --git a/toolkit/xre/dllservices/DynamicBlocklist.h b/toolkit/xre/dllservices/DynamicBlocklist.h new file mode 100644 index 0000000000..a8db60d4ea --- /dev/null +++ b/toolkit/xre/dllservices/DynamicBlocklist.h @@ -0,0 +1,291 @@ +/* -*- 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/. */ + +#ifndef mozilla_DynamicBlocklist_h +#define mozilla_DynamicBlocklist_h + +#include <winternl.h> + +#include "nsWindowsHelpers.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/NativeNt.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/Vector.h" + +#if defined(MOZILLA_INTERNAL_API) +# include "mozilla/dom/Promise.h" +# include "nsIOutputStream.h" +# include "nsTHashSet.h" +#endif // defined(MOZILLA_INTERNAL_API) + +#include "mozilla/WindowsDllBlocklistInfo.h" + +#if !defined(MOZILLA_INTERNAL_API) && defined(ENABLE_TESTS) +# include "mozilla/CmdLineAndEnvUtils.h" +# define BLOCKLIST_INSERT_TEST_ENTRY +#endif // !defined(MOZILLA_INTERNAL_API) && defined(ENABLE_TESTS) + +namespace mozilla { +using DllBlockInfo = DllBlockInfoT<UNICODE_STRING>; + +struct DynamicBlockListBase { + static constexpr uint32_t kSignature = 'FFBL'; // Firefox Blocklist + static constexpr uint32_t kCurrentVersion = 1; + + struct FileHeader { + uint32_t mSignature; + uint32_t mFileVersion; + uint32_t mPayloadSize; + }; +}; +// Define this class in a header so that TestCrossProcessWin +// can include and test it. +class DynamicBlockList final : public DynamicBlockListBase { + uint32_t mPayloadSize; + UniquePtr<uint8_t[]> mPayload; + +#ifdef ENABLE_TESTS + // These two definitions are needed for the DynamicBlocklistWriter to avoid + // writing this test entry out to the blocklist file, so compile these in + // even if MOZILLA_INTERNAL_API is defined. + public: + static constexpr wchar_t kTestDll[] = L"TestDllBlocklist_UserBlocked.dll"; + // kTestDll is null-terminated, but we don't want that for the blocklist + // file + static constexpr size_t kTestDllBytes = sizeof(kTestDll) - sizeof(wchar_t); + + private: +# ifdef BLOCKLIST_INSERT_TEST_ENTRY + // Set up a test entry in the blocklist, used for testing purposes. + void AssignTestEntry(DllBlockInfo* testEntry, uint32_t nameOffset) { + testEntry->mName.Length = kTestDllBytes; + testEntry->mName.MaximumLength = kTestDllBytes; + testEntry->mName.Buffer = reinterpret_cast<PWSTR>(nameOffset); + testEntry->mMaxVersion = DllBlockInfo::ALL_VERSIONS; + testEntry->mFlags = DllBlockInfoFlags::FLAGS_DEFAULT; + } + + void CreateListWithTestEntry() { + mPayloadSize = sizeof(DllBlockInfo) * 2 + kTestDllBytes; + mPayload = MakeUnique<uint8_t[]>(mPayloadSize); + DllBlockInfo* entry = reinterpret_cast<DllBlockInfo*>(mPayload.get()); + AssignTestEntry(entry, sizeof(DllBlockInfo) * 2); + memcpy(mPayload.get() + sizeof(DllBlockInfo) * 2, kTestDll, kTestDllBytes); + ++entry; + entry->mName.Length = 0; + entry->mName.MaximumLength = 0; + } +# endif // BLOCKLIST_INSERT_TEST_ENTRY +#endif // ENABLE_TESTS + + bool LoadFile(const wchar_t* aPath) { +#ifdef BLOCKLIST_INSERT_TEST_ENTRY + const bool shouldInsertBlocklistTestEntry = + MOZ_UNLIKELY(mozilla::EnvHasValue("MOZ_DISABLE_NONLOCAL_CONNECTIONS") || + mozilla::EnvHasValue("MOZ_RUN_GTEST")); +#endif + + nsAutoHandle file( + ::CreateFileW(aPath, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); + if (!file) { +#ifdef BLOCKLIST_INSERT_TEST_ENTRY + if (shouldInsertBlocklistTestEntry) { + // If the blocklist file doesn't exist, we still want to include a test + // entry for testing purposes. + CreateListWithTestEntry(); + return true; + } +#endif + return false; + } + + DWORD bytesRead = 0; + FileHeader header; + BOOL ok = + ::ReadFile(file.get(), &header, sizeof(header), &bytesRead, nullptr); + if (!ok) { +#ifdef BLOCKLIST_INSERT_TEST_ENTRY + if (shouldInsertBlocklistTestEntry) { + // If we have a problem reading the blocklist file, we still want to + // include a test entry for testing purposes. + CreateListWithTestEntry(); + return true; + } +#endif + return false; + } + if (bytesRead != sizeof(header)) { + return false; + } + + if (header.mSignature != kSignature || + header.mFileVersion != kCurrentVersion) { + return false; + } + + uint32_t destinationPayloadSize = header.mPayloadSize; +#ifdef BLOCKLIST_INSERT_TEST_ENTRY + if (shouldInsertBlocklistTestEntry) { + // Write an extra entry for testing purposes + destinationPayloadSize += sizeof(DllBlockInfo) + kTestDllBytes; + } +#endif + UniquePtr<uint8_t[]> payload = + MakeUnique<uint8_t[]>(destinationPayloadSize); + ok = ::ReadFile(file.get(), payload.get(), header.mPayloadSize, &bytesRead, + nullptr); + if (!ok || bytesRead != header.mPayloadSize) { + return false; + } + + uint32_t sizeOfPayloadToIterateOver = header.mPayloadSize; +#ifdef BLOCKLIST_INSERT_TEST_ENTRY + bool haveWrittenTestEntry = false; + UNICODE_STRING testUnicodeString; + // Cast the const-ness away since we're only going to use + // this to compare against strings. + testUnicodeString.Buffer = const_cast<PWSTR>(kTestDll); + testUnicodeString.Length = kTestDllBytes; + testUnicodeString.MaximumLength = kTestDllBytes; +#endif + for (uint32_t offset = 0; offset < sizeOfPayloadToIterateOver; + offset += sizeof(DllBlockInfo)) { + DllBlockInfo* entry = + reinterpret_cast<DllBlockInfo*>(payload.get() + offset); + if (!entry->mName.Length) { +#ifdef BLOCKLIST_INSERT_TEST_ENTRY + if (shouldInsertBlocklistTestEntry && !haveWrittenTestEntry) { + // Shift everything forward + memmove(payload.get() + offset + sizeof(DllBlockInfo), + payload.get() + offset, header.mPayloadSize - offset); + AssignTestEntry(entry, destinationPayloadSize - kTestDllBytes); + haveWrittenTestEntry = true; + } +#endif + break; + } + + size_t stringOffset = reinterpret_cast<size_t>(entry->mName.Buffer); + if (stringOffset + entry->mName.Length > header.mPayloadSize) { + entry->mName.Length = 0; + break; + } + entry->mName.MaximumLength = entry->mName.Length; +#ifdef BLOCKLIST_INSERT_TEST_ENTRY + if (shouldInsertBlocklistTestEntry && !haveWrittenTestEntry) { + UNICODE_STRING currentUnicodeString; + currentUnicodeString.Buffer = + reinterpret_cast<PWSTR>(payload.get() + stringOffset); + currentUnicodeString.Length = entry->mName.Length; + currentUnicodeString.MaximumLength = entry->mName.Length; + if (RtlCompareUnicodeString(¤tUnicodeString, &testUnicodeString, + TRUE) > 0) { + // Shift everything forward + memmove(payload.get() + offset + sizeof(DllBlockInfo), + payload.get() + offset, header.mPayloadSize - offset); + AssignTestEntry(entry, destinationPayloadSize - kTestDllBytes); + haveWrittenTestEntry = true; + // Now we have expanded the area of valid memory, so there is more + // allowable space to iterate over. + sizeOfPayloadToIterateOver = destinationPayloadSize; + offset += sizeof(DllBlockInfo); + ++entry; + } + // The start of the string section will be moving ahead (or has already + // moved ahead) to make room for the test entry + entry->mName.Buffer += + sizeof(DllBlockInfo) / sizeof(entry->mName.Buffer[0]); + } +#endif + } +#ifdef BLOCKLIST_INSERT_TEST_ENTRY + if (shouldInsertBlocklistTestEntry) { + memcpy(payload.get() + destinationPayloadSize - kTestDllBytes, kTestDll, + kTestDllBytes); + } +#endif + + mPayloadSize = destinationPayloadSize; + mPayload = std::move(payload); + return true; + } + + public: + DynamicBlockList() : mPayloadSize(0) {} + explicit DynamicBlockList(const wchar_t* aPath) : mPayloadSize(0) { + LoadFile(aPath); + } + + DynamicBlockList(DynamicBlockList&&) = default; + DynamicBlockList& operator=(DynamicBlockList&&) = default; + DynamicBlockList(const DynamicBlockList&) = delete; + DynamicBlockList& operator=(const DynamicBlockList&) = delete; + + constexpr uint32_t GetPayloadSize() const { return mPayloadSize; } + + // Return the number of bytes copied + size_t CopyTo(void* aBuffer, size_t aBufferLength) const { + if (mPayloadSize > aBufferLength) { + return 0; + } + memcpy(aBuffer, mPayload.get(), mPayloadSize); + return mPayloadSize; + } +}; + +#if defined(MOZILLA_INTERNAL_API) && defined(MOZ_LAUNCHER_PROCESS) + +class DynamicBlocklistWriter final : public DynamicBlockListBase { + template <typename T> + static nsresult WriteValue(nsIOutputStream* aOutputStream, const T& aValue) { + uint32_t written; + return aOutputStream->Write(reinterpret_cast<const char*>(&aValue), + sizeof(T), &written); + } + + template <typename T> + static nsresult WriteBuffer(nsIOutputStream* aOutputStream, const T* aBuffer, + uint32_t aBufferSize) { + uint32_t written; + return aOutputStream->Write(reinterpret_cast<const char*>(aBuffer), + aBufferSize, &written); + } + + RefPtr<dom::Promise> mPromise; + Vector<DllBlockInfo> mArray; + // All strings are packed in this buffer without null characters + UniquePtr<uint8_t[]> mStringBuffer; + + size_t mArraySize; + size_t mStringBufferSize; + + nsresult WriteToFile(const nsAString& aName) const; + + public: + DynamicBlocklistWriter( + RefPtr<dom::Promise> aPromise, + const nsTHashSet<nsStringCaseInsensitiveHashKey>& aBlocklist); + ~DynamicBlocklistWriter() = default; + + DynamicBlocklistWriter(DynamicBlocklistWriter&&) = default; + DynamicBlocklistWriter& operator=(DynamicBlocklistWriter&&) = default; + DynamicBlocklistWriter(const DynamicBlocklistWriter&) = delete; + DynamicBlocklistWriter& operator=(const DynamicBlocklistWriter&) = delete; + + bool IsReady() const { return mStringBuffer.get(); } + + void Run(); + void Cancel(); +}; + +#endif // defined(MOZILLA_INTERNAL_API) && defined(MOZ_LAUNCHER_PROCESS) + +} // namespace mozilla + +#endif // mozilla_DynamicBlocklist_h diff --git a/toolkit/xre/dllservices/DynamicBlocklistWriter.cpp b/toolkit/xre/dllservices/DynamicBlocklistWriter.cpp new file mode 100644 index 0000000000..38a9972e94 --- /dev/null +++ b/toolkit/xre/dllservices/DynamicBlocklistWriter.cpp @@ -0,0 +1,139 @@ +/* -*- 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/DynamicBlocklist.h" +#include "mozilla/LauncherRegistryInfo.h" + +#include "nsISafeOutputStream.h" +#include "nsNetUtil.h" + +namespace mozilla { +#if ENABLE_TESTS +nsDependentString testEntryString(DynamicBlockList::kTestDll, + DynamicBlockList::kTestDllBytes / + sizeof(DynamicBlockList::kTestDll[0])); +#endif + +bool ShouldWriteEntry(const nsAString& name) { +#if ENABLE_TESTS + return name != testEntryString; +#else + return true; +#endif +} + +DynamicBlocklistWriter::DynamicBlocklistWriter( + RefPtr<dom::Promise> aPromise, + const nsTHashSet<nsStringCaseInsensitiveHashKey>& aBlocklist) + : mPromise(aPromise), mArraySize(0), mStringBufferSize(0) { + CheckedUint32 payloadSize; + bool hasTestEntry = false; + for (const nsAString& name : aBlocklist) { + if (ShouldWriteEntry(name)) { + payloadSize += name.Length() * sizeof(char16_t); + if (!payloadSize.isValid()) { + return; + } + } else { + hasTestEntry = true; + } + } + + uint32_t entriesToWrite = aBlocklist.Count(); + if (hasTestEntry) { + entriesToWrite -= 1; + } + + mStringBufferSize = payloadSize.value(); + mArraySize = (entriesToWrite + 1) * sizeof(DllBlockInfo); + + payloadSize += mArraySize; + if (!payloadSize.isValid()) { + return; + } + + mStringBuffer = MakeUnique<uint8_t[]>(mStringBufferSize); + Unused << mArray.resize(entriesToWrite + 1); // aBlockEntries + sentinel + + size_t currentStringOffset = 0; + size_t i = 0; + for (const nsAString& name : aBlocklist) { + if (!ShouldWriteEntry(name)) { + continue; + } + const uint32_t nameSize = name.Length() * sizeof(char16_t); + + mArray[i].mMaxVersion = DllBlockInfo::ALL_VERSIONS; + mArray[i].mFlags = DllBlockInfoFlags::FLAGS_DEFAULT; + + // Copy the module's name to the string buffer and store its offset + // in mName.Buffer + memcpy(mStringBuffer.get() + currentStringOffset, name.BeginReading(), + nameSize); + mArray[i].mName.Buffer = + reinterpret_cast<wchar_t*>(mArraySize + currentStringOffset); + // Only keep mName.Length and leave mName.MaximumLength to be zero + mArray[i].mName.Length = nameSize; + + currentStringOffset += nameSize; + ++i; + } +} + +nsresult DynamicBlocklistWriter::WriteToFile(const nsAString& aName) const { + nsCOMPtr<nsIFile> file; + MOZ_TRY(NS_NewLocalFile(aName, true, getter_AddRefs(file))); + + nsCOMPtr<nsIOutputStream> stream; + MOZ_TRY(NS_NewSafeLocalFileOutputStream(getter_AddRefs(stream), file)); + + MOZ_TRY(WriteValue(stream, kSignature)); + MOZ_TRY(WriteValue(stream, kCurrentVersion)); + MOZ_TRY(WriteValue(stream, + static_cast<uint32_t>(mArraySize + mStringBufferSize))); + MOZ_TRY(WriteBuffer(stream, mArray.begin(), mArraySize)); + MOZ_TRY(WriteBuffer(stream, mStringBuffer.get(), mStringBufferSize)); + + nsresult rv; + nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(stream, &rv); + if (NS_FAILED(rv)) { + return rv; + } + + return safeStream->Finish(); +} + +void DynamicBlocklistWriter::Run() { + MOZ_ASSERT(!NS_IsMainThread()); + + nsresult rv = NS_ERROR_ABORT; + + LauncherRegistryInfo regInfo; + LauncherResult<std::wstring> blocklistFile = regInfo.GetBlocklistFileName(); + if (blocklistFile.isOk()) { + const wchar_t* rawBuffer = blocklistFile.inspect().c_str(); + rv = WriteToFile(nsDependentString(rawBuffer)); + } + + NS_DispatchToMainThread( + // Don't capture mPromise by copy because we're not in the main thread + NS_NewRunnableFunction(__func__, [promise = std::move(mPromise), rv]() { + if (NS_SUCCEEDED(rv)) { + promise->MaybeResolve(JS::NullHandleValue); + } else { + promise->MaybeReject(rv); + } + })); +} + +void DynamicBlocklistWriter::Cancel() { + MOZ_ASSERT(NS_IsMainThread()); + if (mPromise) { + mPromise->MaybeReject(NS_ERROR_ABORT); + } +} + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/ModuleEvaluator.cpp b/toolkit/xre/dllservices/ModuleEvaluator.cpp new file mode 100644 index 0000000000..6e8a3c3493 --- /dev/null +++ b/toolkit/xre/dllservices/ModuleEvaluator.cpp @@ -0,0 +1,251 @@ +/* -*- 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 "ModuleEvaluator.h" + +#include <algorithm> // For std::find() +#include <type_traits> + +#include <windows.h> +#include <shlobj.h> + +#include "mozilla/ArrayUtils.h" +#include "mozilla/ModuleVersionInfo.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/WinDllServices.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "nsReadableUtils.h" +#include "nsWindowsHelpers.h" +#include "nsXULAppAPI.h" + +namespace mozilla { + +// Fills a Vector with keyboard layout DLLs found in the registry. +// These are leaf names only, not full paths. Here we will convert them to +// lowercase before returning, to facilitate case-insensitive searches. +// On error, this may return partial results. +static Vector<nsString> GetKeyboardLayoutDlls() { + Vector<nsString> result; + + HKEY rawKey; + if (::RegOpenKeyExW(HKEY_LOCAL_MACHINE, + L"SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts", + 0, KEY_ENUMERATE_SUB_KEYS, &rawKey) != ERROR_SUCCESS) { + return result; + } + nsAutoRegKey key(rawKey); + + DWORD iKey = 0; + wchar_t strTemp[MAX_PATH] = {}; + while (true) { + DWORD strTempSize = ArrayLength(strTemp); + if (RegEnumKeyExW(rawKey, iKey, strTemp, &strTempSize, nullptr, nullptr, + nullptr, nullptr) != ERROR_SUCCESS) { + // ERROR_NO_MORE_ITEMS or a real error: bail with what we have. + return result; + } + iKey++; + + strTempSize = sizeof(strTemp); + if (::RegGetValueW(rawKey, strTemp, L"Layout File", RRF_RT_REG_SZ, nullptr, + strTemp, &strTempSize) == ERROR_SUCCESS && + strTempSize) { + nsString ws(strTemp, ((strTempSize + 1) / sizeof(wchar_t)) - 1); + ToLowerCase(ws); // To facilitate case-insensitive searches + Unused << result.emplaceBack(std::move(ws)); + } + } +} + +/* static */ +bool ModuleEvaluator::ResolveKnownFolder(REFKNOWNFOLDERID aFolderId, + nsIFile** aOutFile) { + if (!aOutFile) { + return false; + } + + *aOutFile = nullptr; + + // Since we're running off main thread, we can't use NS_GetSpecialDirectory + PWSTR rawPath = nullptr; + HRESULT hr = + ::SHGetKnownFolderPath(aFolderId, KF_FLAG_DEFAULT, nullptr, &rawPath); + if (FAILED(hr)) { + return false; + } + + using ShellStringUniquePtr = + UniquePtr<std::remove_pointer_t<PWSTR>, CoTaskMemFreeDeleter>; + + ShellStringUniquePtr path(rawPath); + + nsresult rv = NS_NewLocalFile(nsDependentString(path.get()), false, aOutFile); + return NS_SUCCEEDED(rv); +} + +ModuleEvaluator::ModuleEvaluator() + : mKeyboardLayoutDlls(GetKeyboardLayoutDlls()) { + MOZ_ASSERT(XRE_IsParentProcess()); + +#if defined(_M_IX86) + // We want to resolve to SYSWOW64 when applicable + REFKNOWNFOLDERID systemFolderId = FOLDERID_SystemX86; +#else + REFKNOWNFOLDERID systemFolderId = FOLDERID_System; +#endif // defined(_M_IX86) + + bool resolveOk = + ResolveKnownFolder(systemFolderId, getter_AddRefs(mSysDirectory)); + MOZ_ASSERT(resolveOk); + if (!resolveOk) { + return; + } + + nsCOMPtr<nsIFile> winSxSDir; + resolveOk = ResolveKnownFolder(FOLDERID_Windows, getter_AddRefs(winSxSDir)); + MOZ_ASSERT(resolveOk); + if (!resolveOk) { + return; + } + + nsresult rv = winSxSDir->Append(u"WinSxS"_ns); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (NS_FAILED(rv)) { + return; + } + + mWinSxSDirectory = std::move(winSxSDir); + + nsCOMPtr<nsIFile> exeFile; + rv = XRE_GetBinaryPath(getter_AddRefs(exeFile)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (NS_FAILED(rv)) { + return; + } + + rv = exeFile->GetParent(getter_AddRefs(mExeDirectory)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (NS_FAILED(rv)) { + return; + } + + nsAutoString exePath; + rv = exeFile->GetPath(exePath); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (NS_FAILED(rv)) { + return; + } + + ModuleVersionInfo exeVi; + if (!exeVi.GetFromImage(exePath)) { + return; + } + + mExeVersion = Some(ModuleVersion(exeVi.mFileVersion.Version64())); +} + +ModuleEvaluator::operator bool() const { + return mExeVersion.isSome() && mExeDirectory && mSysDirectory && + mWinSxSDirectory; +} + +Maybe<ModuleTrustFlags> ModuleEvaluator::GetTrust( + const ModuleRecord& aModuleRecord) const { + MOZ_ASSERT(XRE_IsParentProcess()); + + // We start by checking authenticode signatures, as the presence of any + // signature will produce an immediate pass/fail. + if (aModuleRecord.mVendorInfo.isSome() && + aModuleRecord.mVendorInfo.ref().mSource == + VendorInfo::Source::Signature) { + const nsString& signedBy = aModuleRecord.mVendorInfo.ref().mVendor; + + if (signedBy.EqualsLiteral("Microsoft Windows")) { + return Some(ModuleTrustFlags::MicrosoftWindowsSignature); + } else if (signedBy.EqualsLiteral("Microsoft Corporation")) { + return Some(ModuleTrustFlags::MicrosoftWindowsSignature); + } else if (signedBy.EqualsLiteral("Mozilla Corporation")) { + return Some(ModuleTrustFlags::MozillaSignature); + } else { + // Being signed by somebody who is neither Microsoft nor us is an + // automatic and immediate disqualification. + return Some(ModuleTrustFlags::None); + } + } + + const nsCOMPtr<nsIFile>& dllFile = aModuleRecord.mResolvedDosName; + MOZ_ASSERT(!!dllFile); + if (!dllFile) { + return Nothing(); + } + + nsAutoString dllLeafLower; + if (NS_FAILED(dllFile->GetLeafName(dllLeafLower))) { + return Nothing(); + } + + ToLowerCase(dllLeafLower); // To facilitate case-insensitive searching + + // The JIT profiling module doesn't really have any other practical way to + // match; hard-code it as being trusted. + if (dllLeafLower.EqualsLiteral("jitpi.dll")) { + return Some(ModuleTrustFlags::JitPI); + } + + ModuleTrustFlags result = ModuleTrustFlags::None; + + nsresult rv; + bool contained; + + // Is the DLL in the system directory? + rv = mSysDirectory->Contains(dllFile, &contained); + if (NS_SUCCEEDED(rv) && contained) { + result |= ModuleTrustFlags::SystemDirectory; + } + + // Is the DLL in the WinSxS directory? Some Microsoft DLLs (e.g. comctl32) are + // loaded from here and don't have digital signatures. So while this is not a + // guarantee of trustworthiness, but is at least as valid as system32. + rv = mWinSxSDirectory->Contains(dllFile, &contained); + if (NS_SUCCEEDED(rv) && contained) { + result |= ModuleTrustFlags::WinSxSDirectory; + } + + // Is it a keyboard layout DLL? + if (std::find(mKeyboardLayoutDlls.begin(), mKeyboardLayoutDlls.end(), + dllLeafLower) != mKeyboardLayoutDlls.end()) { + result |= ModuleTrustFlags::KeyboardLayout; + // This doesn't guarantee trustworthiness by itself. Keyboard layouts also + // must be in the system directory. + } + + if (aModuleRecord.mVendorInfo.isSome() && + aModuleRecord.mVendorInfo.ref().mSource == + VendorInfo::Source::VersionInfo) { + const nsString& companyName = aModuleRecord.mVendorInfo.ref().mVendor; + + if (companyName.EqualsLiteral("Microsoft Corporation")) { + result |= ModuleTrustFlags::MicrosoftVersion; + } + } + + rv = mExeDirectory->Contains(dllFile, &contained); + if (NS_SUCCEEDED(rv) && contained) { + result |= ModuleTrustFlags::FirefoxDirectory; + + // If the DLL is in the Firefox directory, does it also share the Firefox + // version info? + if (mExeVersion.isSome() && aModuleRecord.mVersion.isSome() && + mExeVersion.value() == aModuleRecord.mVersion.value()) { + result |= ModuleTrustFlags::FirefoxDirectoryAndVersion; + } + } + + return Some(result); +} + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/ModuleEvaluator.h b/toolkit/xre/dllservices/ModuleEvaluator.h new file mode 100644 index 0000000000..565b5d4da4 --- /dev/null +++ b/toolkit/xre/dllservices/ModuleEvaluator.h @@ -0,0 +1,50 @@ +/* -*- 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/. */ + +#ifndef mozilla_ModuleEvaluator_h +#define mozilla_ModuleEvaluator_h + +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/UntrustedModulesData.h" +#include "mozilla/Vector.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "nsCOMPtr.h" +#include "nsIFile.h" +#include "nsString.h" + +#include <shtypes.h> + +namespace mozilla { + +class ModuleRecord; + +/** + * This class performs trustworthiness evaluation for incoming DLLs. + */ +class MOZ_RAII ModuleEvaluator final { + public: + ModuleEvaluator(); + + explicit operator bool() const; + + Maybe<ModuleTrustFlags> GetTrust(const ModuleRecord& aModuleRecord) const; + + private: + static bool ResolveKnownFolder(REFKNOWNFOLDERID aFolderId, + nsIFile** aOutFile); + + private: + Maybe<ModuleVersion> mExeVersion; // Version number of the running EXE image + nsCOMPtr<nsIFile> mExeDirectory; + nsCOMPtr<nsIFile> mSysDirectory; + nsCOMPtr<nsIFile> mWinSxSDirectory; + Vector<nsString> mKeyboardLayoutDlls; +}; + +} // namespace mozilla + +#endif // mozilla_ModuleEvaluator_h diff --git a/toolkit/xre/dllservices/ModuleVersionInfo.cpp b/toolkit/xre/dllservices/ModuleVersionInfo.cpp new file mode 100644 index 0000000000..c8c2956a74 --- /dev/null +++ b/toolkit/xre/dllservices/ModuleVersionInfo.cpp @@ -0,0 +1,111 @@ +/* -*- 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 "ModuleVersionInfo.h" + +#include "mozilla/UniquePtr.h" + +namespace mozilla { + +/** + * Gets a string value from a version info block with the specified translation + * and field name. + * + * @param aBlock [in] The binary version resource block + * @param aTranslation [in] The translation ID as obtained in the version + * translation list. + * @param aFieldName [in] Null-terminated name of the desired field + * @param aResult [out] Receives the string value, if successful. + * @return true if successful. aResult is unchanged upon failure. + */ +static bool QueryStringValue(const void* aBlock, DWORD aTranslation, + const wchar_t* aFieldName, nsAString& aResult) { + nsAutoString path; + path.AppendPrintf("\\StringFileInfo\\%02lX%02lX%02lX%02lX\\%S", + (aTranslation & 0x0000ff00) >> 8, + (aTranslation & 0x000000ff), + (aTranslation & 0xff000000) >> 24, + (aTranslation & 0x00ff0000) >> 16, aFieldName); + + wchar_t* lpBuffer = nullptr; + UINT len = 0; + if (!::VerQueryValueW(aBlock, path.get(), (PVOID*)&lpBuffer, &len)) { + return false; + } + aResult.Assign(lpBuffer, (size_t)len - 1); + return true; +} + +/** + * Searches through translations in the version resource for the requested + * field. English(US) is preferred, otherwise we take the first translation + * that succeeds. + * + * @param aBlock [in] The binary version resource block + * @param aTranslations [in] The list of translation IDs available + * @param aNumTrans [in] Number of items in aTranslations + * @param aFieldName [in] Null-terminated name of the desired field + * @param aResult [in] Receives the string value, if successful. + * @return true if successful. aResult is unchanged upon failure. + */ +static bool QueryStringValue(const void* aBlock, const DWORD* aTranslations, + size_t aNumTrans, const wchar_t* aFieldName, + nsAString& aResult) { + static const DWORD kPreferredTranslation = + 0x04b00409; // English (US), Unicode + if (QueryStringValue(aBlock, kPreferredTranslation, aFieldName, aResult)) { + return true; + } + for (size_t i = 0; i < aNumTrans; ++i) { + if (QueryStringValue(aBlock, aTranslations[i], aFieldName, aResult)) { + return true; + } + } + return false; +} + +bool ModuleVersionInfo::GetFromImage(const nsAString& aPath) { + nsString path(aPath); + DWORD infoSize = GetFileVersionInfoSizeW(path.get(), nullptr); + if (!infoSize) { + return false; + } + + auto verInfo = MakeUnique<BYTE[]>(infoSize); + if (!::GetFileVersionInfoW(path.get(), 0, infoSize, verInfo.get())) { + return false; + } + + VS_FIXEDFILEINFO* vInfo = nullptr; + UINT vInfoLen = 0; + if (::VerQueryValueW(verInfo.get(), L"\\", (LPVOID*)&vInfo, &vInfoLen)) { + mFileVersion = + VersionNumber(vInfo->dwFileVersionMS, vInfo->dwFileVersionLS); + mProductVersion = + VersionNumber(vInfo->dwProductVersionMS, vInfo->dwProductVersionLS); + } + + // Note that regardless the character set indicated, strings are always + // returned as Unicode by the Windows APIs. + DWORD* pTrans = nullptr; + UINT cbTrans = 0; + if (::VerQueryValueW(verInfo.get(), L"\\VarFileInfo\\Translation", + (PVOID*)&pTrans, &cbTrans)) { + size_t numTrans = cbTrans / sizeof(DWORD); + QueryStringValue(verInfo.get(), pTrans, numTrans, L"CompanyName", + mCompanyName); + QueryStringValue(verInfo.get(), pTrans, numTrans, L"ProductName", + mProductName); + QueryStringValue(verInfo.get(), pTrans, numTrans, L"LegalCopyright", + mLegalCopyright); + QueryStringValue(verInfo.get(), pTrans, numTrans, L"FileDescription", + mFileDescription); + } + + return true; +} + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/ModuleVersionInfo.h b/toolkit/xre/dllservices/ModuleVersionInfo.h new file mode 100644 index 0000000000..2b46817417 --- /dev/null +++ b/toolkit/xre/dllservices/ModuleVersionInfo.h @@ -0,0 +1,71 @@ +/* -*- 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/. */ + +#ifndef mozilla_ModuleVersionInfo_h +#define mozilla_ModuleVersionInfo_h + +#include <windows.h> +#include "nsString.h" + +namespace mozilla { + +// Obtains basic version info from a module image's version info resource. +class ModuleVersionInfo { + public: + // We favor English(US) for these fields, otherwise we take the first + // translation provided in the version resource. + nsString mCompanyName; + nsString mProductName; + nsString mLegalCopyright; + nsString mFileDescription; + + // Represents an A.B.C.D style version number, internally stored as a uint64_t + class VersionNumber { + uint64_t mVersion64 = 0; + + public: + VersionNumber() = default; + + VersionNumber(DWORD aMostSig, DWORD aLeastSig) + : mVersion64((uint64_t)aMostSig << 32 | aLeastSig) {} + + uint16_t A() const { + return (uint16_t)((mVersion64 & 0xffff000000000000) >> 48); + } + + uint16_t B() const { + return (uint16_t)((mVersion64 & 0x0000ffff00000000) >> 32); + } + + uint16_t C() const { + return (uint16_t)((mVersion64 & 0x00000000ffff0000) >> 16); + } + + uint16_t D() const { return (uint16_t)(mVersion64 & 0x000000000000ffff); } + + uint64_t Version64() const { return mVersion64; } + + bool operator==(const VersionNumber& aOther) const { + return mVersion64 == aOther.mVersion64; + } + + nsCString ToString() const { + nsCString ret; + ret.AppendPrintf("%d.%d.%d.%d", (int)A(), (int)B(), (int)C(), (int)D()); + return ret; + } + }; + + VersionNumber mFileVersion; + VersionNumber mProductVersion; + + // Returns false if it has no version resource or has no fixed version info. + bool GetFromImage(const nsAString& aPath); +}; + +} // namespace mozilla + +#endif // mozilla_ModuleVersionInfo_h diff --git a/toolkit/xre/dllservices/UntrustedModulesData.cpp b/toolkit/xre/dllservices/UntrustedModulesData.cpp new file mode 100644 index 0000000000..297dbac554 --- /dev/null +++ b/toolkit/xre/dllservices/UntrustedModulesData.cpp @@ -0,0 +1,453 @@ +/* -*- 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 "UntrustedModulesData.h" + +#include <windows.h> + +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/DynamicallyLinkedFunctionPtr.h" +#include "mozilla/FileUtilsWin.h" +#include "mozilla/Likely.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/WinDllServices.h" +#include "ModuleEvaluator.h" +#include "ModuleVersionInfo.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsXULAppAPI.h" +#include "WinUtils.h" + +// Some utility functions + +static LONGLONG GetQPCFreq() { + static const LONGLONG sFreq = []() -> LONGLONG { + LARGE_INTEGER freq; + ::QueryPerformanceFrequency(&freq); + return freq.QuadPart; + }(); + + return sFreq; +} + +template <typename ReturnT> +static ReturnT QPCToTimeUnits(const LONGLONG aTimeStamp, + const LONGLONG aUnitsPerSec) { + return ReturnT(aTimeStamp * aUnitsPerSec) / ReturnT(GetQPCFreq()); +} + +template <typename ReturnT> +static ReturnT QPCToMilliseconds(const LONGLONG aTimeStamp) { + const LONGLONG kMillisecondsPerSec = 1000; + return QPCToTimeUnits<ReturnT>(aTimeStamp, kMillisecondsPerSec); +} + +template <typename ReturnT> +static ReturnT QPCToMicroseconds(const LONGLONG aTimeStamp) { + const LONGLONG kMicrosecondsPerSec = 1000000; + return QPCToTimeUnits<ReturnT>(aTimeStamp, kMicrosecondsPerSec); +} + +static LONGLONG TimeUnitsToQPC(const LONGLONG aTimeStamp, + const LONGLONG aUnitsPerSec) { + MOZ_ASSERT(aUnitsPerSec != 0); + + LONGLONG result = aTimeStamp; + result *= GetQPCFreq(); + result /= aUnitsPerSec; + return result; +} + +namespace mozilla { + +static Maybe<double> QPCLoadDurationToMilliseconds( + const ModuleLoadInfo& aNtInfo) { + if (aNtInfo.IsBare()) { + return Nothing(); + } + + return Some(QPCToMilliseconds<double>(aNtInfo.mLoadTimeInfo.QuadPart)); +} + +ModuleRecord::ModuleRecord() : mTrustFlags(ModuleTrustFlags::None) {} + +ModuleRecord::ModuleRecord(const nsAString& aResolvedNtPath) + : mResolvedNtName(aResolvedNtPath), mTrustFlags(ModuleTrustFlags::None) { + if (aResolvedNtPath.IsEmpty()) { + return; + } + + MOZ_ASSERT(XRE_IsParentProcess()); + + nsAutoString resolvedDosPath; + if (!NtPathToDosPath(aResolvedNtPath, resolvedDosPath)) { +#if defined(DEBUG) + nsAutoCString msg; + msg.AppendLiteral("NtPathToDosPath failed for path \""); + msg.Append(NS_ConvertUTF16toUTF8(aResolvedNtPath)); + msg.AppendLiteral("\""); + NS_WARNING(msg.get()); +#endif // defined(DEBUG) + return; + } + + nsresult rv = + NS_NewLocalFile(resolvedDosPath, false, getter_AddRefs(mResolvedDosName)); + if (NS_FAILED(rv) || !mResolvedDosName) { + return; + } + + GetVersionAndVendorInfo(resolvedDosPath); + + // Now sanitize the resolved DLL name. If we cannot sanitize this then this + // record must not be considered valid. + nsAutoString strSanitizedPath(resolvedDosPath); + if (!widget::WinUtils::PreparePathForTelemetry(strSanitizedPath)) { + return; + } + + mSanitizedDllName = strSanitizedPath; +} + +void ModuleRecord::GetVersionAndVendorInfo(const nsAString& aPath) { + RefPtr<DllServices> dllSvc(DllServices::Get()); + + // WinVerifyTrust is too slow and of limited utility for our purposes, so + // we pass SkipTrustVerification here to avoid it. + UniquePtr<wchar_t[]> signedBy( + dllSvc->GetBinaryOrgName(PromiseFlatString(aPath).get(), + AuthenticodeFlags::SkipTrustVerification)); + if (signedBy) { + mVendorInfo = Some(VendorInfo(VendorInfo::Source::Signature, + nsDependentString(signedBy.get()))); + } + + ModuleVersionInfo verInfo; + if (!verInfo.GetFromImage(aPath)) { + return; + } + + if (verInfo.mFileVersion.Version64()) { + mVersion = Some(ModuleVersion(verInfo.mFileVersion.Version64())); + } + + if (!mVendorInfo && !verInfo.mCompanyName.IsEmpty()) { + mVendorInfo = + Some(VendorInfo(VendorInfo::Source::VersionInfo, verInfo.mCompanyName)); + } +} + +bool ModuleRecord::IsXUL() const { + if (!mResolvedDosName) { + return false; + } + + nsAutoString leafName; + nsresult rv = mResolvedDosName->GetLeafName(leafName); + if (NS_FAILED(rv)) { + return false; + } + + return leafName.EqualsIgnoreCase("xul.dll"); +} + +int32_t ModuleRecord::GetScoreThreshold() const { +#ifdef ENABLE_TESTS + // Check whether we are running as an xpcshell test. + if (MOZ_UNLIKELY(mozilla::EnvHasValue("XPCSHELL_TEST_PROFILE_DIR"))) { + nsAutoString dllLeaf; + if (NS_SUCCEEDED(mResolvedDosName->GetLeafName(dllLeaf))) { + // During xpcshell tests, this DLL is hard-coded to pass through all + // criteria checks and still result in "untrusted" status, so it shows up + // in the untrusted modules ping for the test to examine. + // Setting the threshold very high ensures the test will cover all + // criteria. + if (dllLeaf.EqualsIgnoreCase("modules-test.dll")) { + return 99999; + } + } + } +#endif + + return 100; +} + +bool ModuleRecord::IsTrusted() const { + if (mTrustFlags == ModuleTrustFlags::None) { + return false; + } + + // These flags are immediate passes + if (mTrustFlags & + (ModuleTrustFlags::MicrosoftWindowsSignature | + ModuleTrustFlags::MozillaSignature | ModuleTrustFlags::JitPI)) { + return true; + } + + // The remaining flags, when set, each count for 50 points toward a + // trustworthiness score. + int32_t score = static_cast<int32_t>( + CountPopulation32(static_cast<uint32_t>(mTrustFlags))) * + 50; + return score >= GetScoreThreshold(); +} + +ProcessedModuleLoadEvent::ProcessedModuleLoadEvent() + : mProcessUptimeMS(0ULL), + mThreadId(0UL), + mBaseAddress(0U), + mIsDependent(false), + mLoadStatus(0) {} + +ProcessedModuleLoadEvent::ProcessedModuleLoadEvent( + glue::EnhancedModuleLoadInfo&& aModLoadInfo, + RefPtr<ModuleRecord>&& aModuleRecord) + : mProcessUptimeMS(QPCTimeStampToProcessUptimeMilliseconds( + aModLoadInfo.mNtLoadInfo.mBeginTimestamp)), + mLoadDurationMS(QPCLoadDurationToMilliseconds(aModLoadInfo.mNtLoadInfo)), + mThreadId(aModLoadInfo.mNtLoadInfo.mThreadId), + mThreadName(std::move(aModLoadInfo.mThreadName)), + mBaseAddress( + reinterpret_cast<uintptr_t>(aModLoadInfo.mNtLoadInfo.mBaseAddr)), + mModule(std::move(aModuleRecord)), + mIsDependent(aModLoadInfo.mNtLoadInfo.mIsDependent), + mLoadStatus(static_cast<uint32_t>(aModLoadInfo.mNtLoadInfo.mStatus)) { + if (!mModule || !(*mModule)) { + return; + } + + mRequestedDllName = aModLoadInfo.mNtLoadInfo.mRequestedDllName.AsString(); + + // If we're in the main process, sanitize the requested DLL name here. + // If not, we cannot use PreparePathForTelemetry because it may try to + // delayload shlwapi.dll and could fail if the process is sandboxed. + // We leave mRequestedDllName unsanitized here and sanitize it when + // transferring it to the main process. + // (See ParamTraits<mozilla::UntrustedModulesData>::ReadEvent) + if (XRE_IsParentProcess()) { + SanitizeRequestedDllName(); + } +} + +void ProcessedModuleLoadEvent::SanitizeRequestedDllName() { + if (!mRequestedDllName.IsEmpty() && + !widget::WinUtils::PreparePathForTelemetry(mRequestedDllName)) { + // If we cannot sanitize a path, we simply do not provide that field to + // Telemetry. + mRequestedDllName.Truncate(); + } +} + +/* static */ +Maybe<LONGLONG> +ProcessedModuleLoadEvent::ComputeQPCTimeStampForProcessCreation() { + // This is similar to the algorithm used by TimeStamp::ProcessCreation: + + // 1. Get the process creation timestamp as FILETIME; + FILETIME creationTime, exitTime, kernelTime, userTime; + if (!::GetProcessTimes(::GetCurrentProcess(), &creationTime, &exitTime, + &kernelTime, &userTime)) { + return Nothing(); + } + + // 2. Get current timestamps as both QPC and FILETIME; + LARGE_INTEGER nowQPC; + ::QueryPerformanceCounter(&nowQPC); + + static const StaticDynamicallyLinkedFunctionPtr<void(WINAPI*)(LPFILETIME)> + pGetSystemTimePreciseAsFileTime(L"kernel32.dll", + "GetSystemTimePreciseAsFileTime"); + + FILETIME nowFile; + if (pGetSystemTimePreciseAsFileTime) { + pGetSystemTimePreciseAsFileTime(&nowFile); + } else { + ::GetSystemTimeAsFileTime(&nowFile); + } + + // 3. Take the difference between the FILETIMEs from (1) and (2), + // respectively, yielding the elapsed process uptime in microseconds. + ULARGE_INTEGER ulCreation = { + {creationTime.dwLowDateTime, creationTime.dwHighDateTime}}; + ULARGE_INTEGER ulNow = {{nowFile.dwLowDateTime, nowFile.dwHighDateTime}}; + + ULONGLONG timeSinceCreationMicroSec = + (ulNow.QuadPart - ulCreation.QuadPart) / 10ULL; + + // 4. Convert the QPC timestamp from (1) to microseconds. + LONGLONG nowQPCMicroSec = QPCToMicroseconds<LONGLONG>(nowQPC.QuadPart); + + // 5. Convert the elapsed uptime to an absolute timestamp by subtracting + // from (4), which yields the absolute timestamp for process creation. + // We convert back to QPC units before returning. + const LONGLONG kMicrosecondsPerSec = 1000000; + return Some(TimeUnitsToQPC(nowQPCMicroSec - timeSinceCreationMicroSec, + kMicrosecondsPerSec)); +} + +/* static */ +uint64_t ProcessedModuleLoadEvent::QPCTimeStampToProcessUptimeMilliseconds( + const LARGE_INTEGER& aTimeStamp) { + static const Maybe<LONGLONG> sProcessCreationTimeStamp = + ComputeQPCTimeStampForProcessCreation(); + + if (!sProcessCreationTimeStamp) { + return 0ULL; + } + + LONGLONG diff = aTimeStamp.QuadPart - sProcessCreationTimeStamp.value(); + return QPCToMilliseconds<uint64_t>(diff); +} + +bool ProcessedModuleLoadEvent::IsXULLoad() const { + if (!mModule) { + return false; + } + + return mModule->IsXUL(); +} + +bool ProcessedModuleLoadEvent::IsTrusted() const { + if (!mModule) { + return false; + } + + return mModule->IsTrusted(); +} + +void UntrustedModulesData::AddNewLoads( + const ModulesMap& aModules, UntrustedModuleLoadingEvents&& aEvents, + Vector<Telemetry::ProcessedStack>&& aStacks) { + MOZ_ASSERT(aEvents.length() == aStacks.length()); + for (const auto& entry : aModules) { + if (entry.GetData()->IsTrusted()) { + // Filter out trusted module records + continue; + } + + Unused << mModules.LookupOrInsert(entry.GetKey(), entry.GetData()); + } + + MOZ_ASSERT(mEvents.length() <= kMaxEvents); + + mNumEvents += aStacks.length(); + mEvents.extendBack(std::move(aEvents)); + for (auto&& stack : aStacks) { + mStacks.AddStack(stack); + } + Truncate(false); +} + +void UntrustedModulesData::MergeModules(UntrustedModulesData& aNewData) { + for (auto item : aNewData.mEvents) { + mModules.WithEntryHandle(item->mEvent.mModule->mResolvedNtName, + [&](auto&& addPtr) { + if (addPtr) { + // Even though the path of a ModuleRecord + // matches, the object of ModuleRecord can be + // different. Make sure the event's mModule + // points to an object in mModules. + item->mEvent.mModule = addPtr.Data(); + } else { + addPtr.Insert(item->mEvent.mModule); + } + }); + } +} + +void UntrustedModulesData::Merge(UntrustedModulesData&& aNewData) { + // Don't merge loading events of a different process + MOZ_ASSERT((mProcessType == aNewData.mProcessType) && + (mPid == aNewData.mPid)); + + UntrustedModulesData newData(std::move(aNewData)); + + if (!mNumEvents) { + mNumEvents = newData.mNumEvents; + mModules = std::move(newData.mModules); + mEvents = std::move(newData.mEvents); + mStacks = std::move(newData.mStacks); + return; + } + + MergeModules(newData); + mNumEvents += newData.mNumEvents; + mEvents.extendBack(std::move(newData.mEvents)); + mStacks.AddStacks(newData.mStacks); + Truncate(false); +} + +void UntrustedModulesData::Truncate(bool aDropCallstackData) { + if (aDropCallstackData) { + mStacks.Clear(); + } + + if (mNumEvents <= kMaxEvents) { + return; + } + + UntrustedModuleLoadingEvents events; + events.splice(0, mEvents, mNumEvents - kMaxEvents, kMaxEvents); + std::swap(events, mEvents); + mNumEvents = kMaxEvents; + // mStacks only keeps the latest kMaxEvents stacks, so mEvents will + // still be lined up with mStacks. +} + +void UntrustedModulesData::MergeWithoutStacks(UntrustedModulesData&& aNewData) { + // Don't merge loading events of a different process + MOZ_ASSERT((mProcessType == aNewData.mProcessType) && + (mPid == aNewData.mPid)); + MOZ_ASSERT(!mStacks.GetStackCount()); + + UntrustedModulesData newData(std::move(aNewData)); + + if (mNumEvents > 0) { + MergeModules(newData); + } else { + mModules = std::move(newData.mModules); + } + + mNumEvents += newData.mNumEvents; + mEvents.extendBack(std::move(newData.mEvents)); + + Truncate(true); +} + +void UntrustedModulesData::Swap(UntrustedModulesData& aOther) { + GeckoProcessType tmpProcessType = mProcessType; + mProcessType = aOther.mProcessType; + aOther.mProcessType = tmpProcessType; + + DWORD tmpPid = mPid; + mPid = aOther.mPid; + aOther.mPid = tmpPid; + + TimeDuration tmpElapsed = mElapsed; + mElapsed = aOther.mElapsed; + aOther.mElapsed = tmpElapsed; + + mModules.SwapElements(aOther.mModules); + std::swap(mNumEvents, aOther.mNumEvents); + std::swap(mEvents, aOther.mEvents); + mStacks.Swap(aOther.mStacks); + + Maybe<double> tmpXULLoadDurationMS = mXULLoadDurationMS; + mXULLoadDurationMS = aOther.mXULLoadDurationMS; + aOther.mXULLoadDurationMS = tmpXULLoadDurationMS; + + uint32_t tmpSanitizationFailures = mSanitizationFailures; + mSanitizationFailures = aOther.mSanitizationFailures; + aOther.mSanitizationFailures = tmpSanitizationFailures; + + uint32_t tmpTrustTestFailures = mTrustTestFailures; + mTrustTestFailures = aOther.mTrustTestFailures; + aOther.mTrustTestFailures = tmpTrustTestFailures; +} + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/UntrustedModulesData.h b/toolkit/xre/dllservices/UntrustedModulesData.h new file mode 100644 index 0000000000..158cc24746 --- /dev/null +++ b/toolkit/xre/dllservices/UntrustedModulesData.h @@ -0,0 +1,653 @@ +/* -*- 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/. */ + +#ifndef mozilla_UntrustedModulesData_h +#define mozilla_UntrustedModulesData_h + +#if defined(XP_WIN) + +# include "ipc/IPCMessageUtils.h" +# include "mozilla/CombinedStacks.h" +# include "mozilla/DebugOnly.h" +# include "mozilla/LinkedList.h" +# include "mozilla/Maybe.h" +# include "mozilla/RefPtr.h" +# include "mozilla/TypedEnumBits.h" +# include "mozilla/Unused.h" +# include "mozilla/Variant.h" +# include "mozilla/Vector.h" +# include "mozilla/WinHeaderOnlyUtils.h" +# include "nsCOMPtr.h" +# include "nsHashKeys.h" +# include "nsIFile.h" +# include "nsISupportsImpl.h" +# include "nsRefPtrHashtable.h" +# include "nsString.h" +# include "nsXULAppAPI.h" + +namespace mozilla { +namespace glue { +struct EnhancedModuleLoadInfo; +} // namespace glue + +enum class ModuleTrustFlags : uint32_t { + None = 0, + MozillaSignature = 1, + MicrosoftWindowsSignature = 2, + MicrosoftVersion = 4, + FirefoxDirectory = 8, + FirefoxDirectoryAndVersion = 0x10, + SystemDirectory = 0x20, + KeyboardLayout = 0x40, + JitPI = 0x80, + WinSxSDirectory = 0x100, +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(ModuleTrustFlags); + +class VendorInfo final { + public: + enum class Source : uint32_t { + None, + Signature, + VersionInfo, + }; + + VendorInfo() : mSource(Source::None) {} + VendorInfo(const Source aSource, const nsAString& aVendor) + : mSource(aSource), mVendor(aVendor) { + MOZ_ASSERT(aSource != Source::None && !aVendor.IsEmpty()); + } + + Source mSource; + nsString mVendor; +}; + +class ModulesMap; + +class ModuleRecord final { + public: + explicit ModuleRecord(const nsAString& aResolvedNtPath); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ModuleRecord) + + nsString mResolvedNtName; + nsCOMPtr<nsIFile> mResolvedDosName; + nsString mSanitizedDllName; + Maybe<ModuleVersion> mVersion; + Maybe<VendorInfo> mVendorInfo; + ModuleTrustFlags mTrustFlags; + + explicit operator bool() const { return !mSanitizedDllName.IsEmpty(); } + bool IsXUL() const; + bool IsTrusted() const; + + ModuleRecord(const ModuleRecord&) = delete; + ModuleRecord(ModuleRecord&&) = delete; + + ModuleRecord& operator=(const ModuleRecord&) = delete; + ModuleRecord& operator=(ModuleRecord&&) = delete; + + private: + ModuleRecord(); + ~ModuleRecord() = default; + void GetVersionAndVendorInfo(const nsAString& aPath); + int32_t GetScoreThreshold() const; + + friend struct ::IPC::ParamTraits<ModulesMap>; +}; + +/** + * This type holds module path data using one of two internal representations. + * It may be created from either a nsTHashtable or a Vector, and may be + * serialized from either representation into a common format over the wire. + * Deserialization always uses the Vector representation. + */ +struct ModulePaths final { + using SetType = nsTHashtable<nsStringCaseInsensitiveHashKey>; + using VecType = Vector<nsString>; + + Variant<SetType, VecType> mModuleNtPaths; + + template <typename T> + explicit ModulePaths(T&& aPaths) + : mModuleNtPaths(AsVariant(std::forward<T>(aPaths))) {} + + ModulePaths() : mModuleNtPaths(VecType()) {} + + ModulePaths(const ModulePaths& aOther) = delete; + ModulePaths(ModulePaths&& aOther) = default; + ModulePaths& operator=(const ModulePaths&) = delete; + ModulePaths& operator=(ModulePaths&&) = default; +}; + +class ProcessedModuleLoadEvent final { + public: + ProcessedModuleLoadEvent(); + ProcessedModuleLoadEvent(glue::EnhancedModuleLoadInfo&& aModLoadInfo, + RefPtr<ModuleRecord>&& aModuleRecord); + + explicit operator bool() const { return mModule && *mModule; } + bool IsXULLoad() const; + bool IsTrusted() const; + + uint64_t mProcessUptimeMS; + Maybe<double> mLoadDurationMS; + DWORD mThreadId; + nsCString mThreadName; + nsString mRequestedDllName; + // We intentionally store mBaseAddress as part of the event and not the + // module, as relocation may cause it to change between loads. If so, we want + // to know about it. + uintptr_t mBaseAddress; + RefPtr<ModuleRecord> mModule; + bool mIsDependent; + uint32_t mLoadStatus; // corresponding to enum ModuleLoadInfo::Status + + ProcessedModuleLoadEvent(const ProcessedModuleLoadEvent&) = delete; + ProcessedModuleLoadEvent& operator=(const ProcessedModuleLoadEvent&) = delete; + + ProcessedModuleLoadEvent(ProcessedModuleLoadEvent&&) = default; + ProcessedModuleLoadEvent& operator=(ProcessedModuleLoadEvent&&) = default; + + void SanitizeRequestedDllName(); + + private: + static Maybe<LONGLONG> ComputeQPCTimeStampForProcessCreation(); + static uint64_t QPCTimeStampToProcessUptimeMilliseconds( + const LARGE_INTEGER& aTimeStamp); +}; + +// Declaring ModulesMap this way makes it much easier to forward declare than +// if we had used |using| or |typedef|. +class ModulesMap final + : public nsRefPtrHashtable<nsStringCaseInsensitiveHashKey, ModuleRecord> { + public: + ModulesMap() + : nsRefPtrHashtable<nsStringCaseInsensitiveHashKey, ModuleRecord>() {} +}; + +struct ProcessedModuleLoadEventContainer final + : public LinkedListElement<ProcessedModuleLoadEventContainer> { + ProcessedModuleLoadEvent mEvent; + ProcessedModuleLoadEventContainer() = default; + explicit ProcessedModuleLoadEventContainer(ProcessedModuleLoadEvent&& aEvent) + : mEvent(std::move(aEvent)) {} + + ProcessedModuleLoadEventContainer(ProcessedModuleLoadEventContainer&&) = + default; + ProcessedModuleLoadEventContainer& operator=( + ProcessedModuleLoadEventContainer&&) = default; + ProcessedModuleLoadEventContainer(const ProcessedModuleLoadEventContainer&) = + delete; + ProcessedModuleLoadEventContainer& operator=( + const ProcessedModuleLoadEventContainer&) = delete; +}; +using UntrustedModuleLoadingEvents = + AutoCleanLinkedList<ProcessedModuleLoadEventContainer>; + +class UntrustedModulesData final { + // Merge aNewData.mEvents into this->mModules and also + // make module entries in aNewData point to items in this->mModules. + void MergeModules(UntrustedModulesData& aNewData); + + public: + // Ensure mEvents will never retain more than kMaxEvents events. + // This constant matches the maximum in Telemetry::CombinedStacks. + // Truncate() relies on these being the same. + static constexpr size_t kMaxEvents = 50; + + UntrustedModulesData() + : mProcessType(XRE_GetProcessType()), + mPid(::GetCurrentProcessId()), + mNumEvents(0), + mSanitizationFailures(0), + mTrustTestFailures(0) { + MOZ_ASSERT(kMaxEvents == mStacks.GetMaxStacksCount()); + } + + UntrustedModulesData(UntrustedModulesData&&) = default; + UntrustedModulesData& operator=(UntrustedModulesData&&) = default; + + UntrustedModulesData(const UntrustedModulesData&) = delete; + UntrustedModulesData& operator=(const UntrustedModulesData&) = delete; + + explicit operator bool() const { + return !mEvents.isEmpty() || mSanitizationFailures || mTrustTestFailures || + mXULLoadDurationMS.isSome(); + } + + void AddNewLoads(const ModulesMap& aModulesMap, + UntrustedModuleLoadingEvents&& aEvents, + Vector<Telemetry::ProcessedStack>&& aStacks); + void Merge(UntrustedModulesData&& aNewData); + void MergeWithoutStacks(UntrustedModulesData&& aNewData); + void Swap(UntrustedModulesData& aOther); + + // Drop callstack data and old loading events. + void Truncate(bool aDropCallstackData); + + GeckoProcessType mProcessType; + DWORD mPid; + TimeDuration mElapsed; + ModulesMap mModules; + uint32_t mNumEvents; + UntrustedModuleLoadingEvents mEvents; + Telemetry::CombinedStacks mStacks; + Maybe<double> mXULLoadDurationMS; + uint32_t mSanitizationFailures; + uint32_t mTrustTestFailures; +}; + +class ModulesMapResult final { + public: + ModulesMapResult() : mTrustTestFailures(0) {} + + ModulesMapResult(const ModulesMapResult& aOther) = delete; + ModulesMapResult(ModulesMapResult&& aOther) = default; + ModulesMapResult& operator=(const ModulesMapResult& aOther) = delete; + ModulesMapResult& operator=(ModulesMapResult&& aOther) = default; + + ModulesMap mModules; + uint32_t mTrustTestFailures; +}; + +} // namespace mozilla + +namespace IPC { + +template <> +struct ParamTraits<mozilla::ModuleVersion> { + typedef mozilla::ModuleVersion paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + aWriter->WriteUInt64(aParam.AsInteger()); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + uint64_t ver; + if (!aReader->ReadUInt64(&ver)) { + return false; + } + + *aResult = ver; + return true; + } +}; + +template <> +struct ParamTraits<mozilla::VendorInfo> { + typedef mozilla::VendorInfo paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + aWriter->WriteUInt32(static_cast<uint32_t>(aParam.mSource)); + WriteParam(aWriter, aParam.mVendor); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + uint32_t source; + if (!aReader->ReadUInt32(&source)) { + return false; + } + + aResult->mSource = static_cast<mozilla::VendorInfo::Source>(source); + + if (!ReadParam(aReader, &aResult->mVendor)) { + return false; + } + + return true; + } +}; + +template <> +struct ParamTraits<mozilla::ModuleRecord> { + typedef mozilla::ModuleRecord paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mResolvedNtName); + + nsAutoString resolvedDosName; + if (aParam.mResolvedDosName) { + mozilla::DebugOnly<nsresult> rv = + aParam.mResolvedDosName->GetPath(resolvedDosName); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + WriteParam(aWriter, resolvedDosName); + WriteParam(aWriter, aParam.mSanitizedDllName); + WriteParam(aWriter, aParam.mVersion); + WriteParam(aWriter, aParam.mVendorInfo); + aWriter->WriteUInt32(static_cast<uint32_t>(aParam.mTrustFlags)); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + if (!ReadParam(aReader, &aResult->mResolvedNtName)) { + return false; + } + + nsAutoString resolvedDosName; + if (!ReadParam(aReader, &resolvedDosName)) { + return false; + } + + if (resolvedDosName.IsEmpty()) { + aResult->mResolvedDosName = nullptr; + } else if (NS_FAILED(NS_NewLocalFile( + resolvedDosName, false, + getter_AddRefs(aResult->mResolvedDosName)))) { + return false; + } + + if (!ReadParam(aReader, &aResult->mSanitizedDllName)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mVersion)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mVendorInfo)) { + return false; + } + + uint32_t trustFlags; + if (!aReader->ReadUInt32(&trustFlags)) { + return false; + } + + aResult->mTrustFlags = static_cast<mozilla::ModuleTrustFlags>(trustFlags); + return true; + } +}; + +template <> +struct ParamTraits<mozilla::ModulesMap> { + typedef mozilla::ModulesMap paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + aWriter->WriteUInt32(aParam.Count()); + + for (const auto& entry : aParam) { + MOZ_RELEASE_ASSERT(entry.GetData()); + WriteParam(aWriter, entry.GetKey()); + WriteParam(aWriter, *(entry.GetData())); + } + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + uint32_t count; + if (!ReadParam(aReader, &count)) { + return false; + } + + for (uint32_t current = 0; current < count; ++current) { + nsAutoString key; + if (!ReadParam(aReader, &key) || key.IsEmpty()) { + return false; + } + + RefPtr<mozilla::ModuleRecord> rec(new mozilla::ModuleRecord()); + if (!ReadParam(aReader, rec.get())) { + return false; + } + + aResult->InsertOrUpdate(key, std::move(rec)); + } + + return true; + } +}; + +template <> +struct ParamTraits<mozilla::ModulePaths> { + typedef mozilla::ModulePaths paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + aParam.mModuleNtPaths.match( + [aWriter](const paramType::SetType& aSet) { WriteSet(aWriter, aSet); }, + [aWriter](const paramType::VecType& aVec) { + WriteVector(aWriter, aVec); + }); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + uint32_t len; + if (!aReader->ReadUInt32(&len)) { + return false; + } + + // As noted in the comments for ModulePaths, we only deserialize using the + // Vector representation. + auto& vec = aResult->mModuleNtPaths.as<paramType::VecType>(); + if (!vec.reserve(len)) { + return false; + } + + for (uint32_t idx = 0; idx < len; ++idx) { + nsString str; + if (!ReadParam(aReader, &str)) { + return false; + } + + if (!vec.emplaceBack(std::move(str))) { + return false; + } + } + + return true; + } + + private: + // NB: This function must write out the set in the same format as WriteVector + static void WriteSet(MessageWriter* aWriter, const paramType::SetType& aSet) { + aWriter->WriteUInt32(aSet.Count()); + for (const auto& key : aSet.Keys()) { + WriteParam(aWriter, key); + } + } + + // NB: This function must write out the vector in the same format as WriteSet + static void WriteVector(MessageWriter* aWriter, + const paramType::VecType& aVec) { + aWriter->WriteUInt32(aVec.length()); + for (auto const& item : aVec) { + WriteParam(aWriter, item); + } + } +}; + +template <> +struct ParamTraits<mozilla::UntrustedModulesData> { + typedef mozilla::UntrustedModulesData paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + aWriter->WriteUInt32(aParam.mProcessType); + aWriter->WriteULong(aParam.mPid); + WriteParam(aWriter, aParam.mElapsed); + WriteParam(aWriter, aParam.mModules); + + aWriter->WriteUInt32(aParam.mNumEvents); + for (auto event : aParam.mEvents) { + WriteEvent(aWriter, event->mEvent); + } + + WriteParam(aWriter, aParam.mStacks); + WriteParam(aWriter, aParam.mXULLoadDurationMS); + aWriter->WriteUInt32(aParam.mSanitizationFailures); + aWriter->WriteUInt32(aParam.mTrustTestFailures); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + uint32_t processType; + if (!aReader->ReadUInt32(&processType)) { + return false; + } + + aResult->mProcessType = static_cast<GeckoProcessType>(processType); + + if (!aReader->ReadULong(&aResult->mPid)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mElapsed)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mModules)) { + return false; + } + + // We read mEvents manually so that we can use ReadEvent defined below. + if (!ReadParam(aReader, &aResult->mNumEvents)) { + return false; + } + + for (uint32_t curEventIdx = 0; curEventIdx < aResult->mNumEvents; + ++curEventIdx) { + auto newEvent = + mozilla::MakeUnique<mozilla::ProcessedModuleLoadEventContainer>(); + if (!ReadEvent(aReader, &newEvent->mEvent, aResult->mModules)) { + return false; + } + aResult->mEvents.insertBack(newEvent.release()); + } + + if (!ReadParam(aReader, &aResult->mStacks)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mXULLoadDurationMS)) { + return false; + } + + if (!aReader->ReadUInt32(&aResult->mSanitizationFailures)) { + return false; + } + + if (!aReader->ReadUInt32(&aResult->mTrustTestFailures)) { + return false; + } + + return true; + } + + private: + // Because ProcessedModuleLoadEvent depends on a hash table from + // UntrustedModulesData, we do its serialization as part of this + // specialization. + static void WriteEvent(MessageWriter* aWriter, + const mozilla::ProcessedModuleLoadEvent& aParam) { + aWriter->WriteUInt64(aParam.mProcessUptimeMS); + WriteParam(aWriter, aParam.mLoadDurationMS); + aWriter->WriteULong(aParam.mThreadId); + WriteParam(aWriter, aParam.mThreadName); + WriteParam(aWriter, aParam.mRequestedDllName); + WriteParam(aWriter, aParam.mBaseAddress); + WriteParam(aWriter, aParam.mIsDependent); + WriteParam(aWriter, aParam.mLoadStatus); + + // We don't write the ModuleRecord directly; we write its key into the + // UntrustedModulesData::mModules hash table. + MOZ_ASSERT(aParam.mModule && !aParam.mModule->mResolvedNtName.IsEmpty()); + WriteParam(aWriter, aParam.mModule->mResolvedNtName); + } + + // Because ProcessedModuleLoadEvent depends on a hash table from + // UntrustedModulesData, we do its deserialization as part of this + // specialization. + static bool ReadEvent(MessageReader* aReader, + mozilla::ProcessedModuleLoadEvent* aResult, + const mozilla::ModulesMap& aModulesMap) { + if (!aReader->ReadUInt64(&aResult->mProcessUptimeMS)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mLoadDurationMS)) { + return false; + } + + if (!aReader->ReadULong(&aResult->mThreadId)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mThreadName)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mRequestedDllName)) { + return false; + } + + // When ProcessedModuleLoadEvent was constructed in a child process, we left + // mRequestedDllName unsanitized, so now is a good time to sanitize it. + aResult->SanitizeRequestedDllName(); + + if (!ReadParam(aReader, &aResult->mBaseAddress)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mIsDependent)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mLoadStatus)) { + return false; + } + + nsAutoString resolvedNtName; + if (!ReadParam(aReader, &resolvedNtName)) { + return false; + } + + // NB: While bad data integrity might for some reason result in a null + // mModule, we do not fail the deserialization; this is a data error, + // rather than an IPC error. The error is detected and dealt with in + // telemetry. + aResult->mModule = aModulesMap.Get(resolvedNtName); + + return true; + } +}; + +template <> +struct ParamTraits<mozilla::ModulesMapResult> { + typedef mozilla::ModulesMapResult paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mModules); + aWriter->WriteUInt32(aParam.mTrustTestFailures); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + if (!ReadParam(aReader, &aResult->mModules)) { + return false; + } + + if (!aReader->ReadUInt32(&aResult->mTrustTestFailures)) { + return false; + } + + return true; + } +}; + +} // namespace IPC + +#else // defined(XP_WIN) + +namespace mozilla { + +// For compiling IPDL on non-Windows platforms +using UntrustedModulesData = uint32_t; +using ModulePaths = uint32_t; +using ModulesMapResult = uint32_t; + +} // namespace mozilla + +#endif // defined(XP_WIN) + +#endif // mozilla_UntrustedModulesData_h diff --git a/toolkit/xre/dllservices/UntrustedModulesProcessor.cpp b/toolkit/xre/dllservices/UntrustedModulesProcessor.cpp new file mode 100644 index 0000000000..4deb920a52 --- /dev/null +++ b/toolkit/xre/dllservices/UntrustedModulesProcessor.cpp @@ -0,0 +1,1063 @@ +/* -*- 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 "UntrustedModulesProcessor.h" + +#include <windows.h> + +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/Likely.h" +#include "mozilla/net/SocketProcessChild.h" +#include "mozilla/net/SocketProcessParent.h" +#include "mozilla/ipc/UtilityProcessParent.h" +#include "mozilla/ipc/UtilityProcessChild.h" +#include "mozilla/RDDChild.h" +#include "mozilla/RDDParent.h" +#include "mozilla/RDDProcessManager.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Services.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Unused.h" +#include "ModuleEvaluator.h" +#include "nsCOMPtr.h" +#include "nsHashKeys.h" +#include "nsIObserverService.h" +#include "nsTHashtable.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" +#include "private/prpriv.h" // For PR_GetThreadID + +namespace mozilla { + +class MOZ_RAII BackgroundPriorityRegion final { + public: + BackgroundPriorityRegion() + : mIsBackground( + ::SetThreadPriority(::GetCurrentThread(), THREAD_PRIORITY_IDLE)) {} + + ~BackgroundPriorityRegion() { + if (!mIsBackground) { + return; + } + + Clear(::GetCurrentThread()); + } + + static void Clear(const nsAutoHandle& aThread) { + if (!aThread) { + return; + } + + Clear(aThread.get()); + } + + BackgroundPriorityRegion(const BackgroundPriorityRegion&) = delete; + BackgroundPriorityRegion(BackgroundPriorityRegion&&) = delete; + BackgroundPriorityRegion& operator=(const BackgroundPriorityRegion&) = delete; + BackgroundPriorityRegion& operator=(BackgroundPriorityRegion&&) = delete; + + private: + static void Clear(HANDLE aThread) { + DebugOnly<BOOL> ok = ::SetThreadPriority(aThread, THREAD_PRIORITY_NORMAL); + MOZ_ASSERT(ok); + } + + private: + const BOOL mIsBackground; +}; + +/* static */ +bool UntrustedModulesProcessor::IsSupportedProcessType() { + switch (XRE_GetProcessType()) { + case GeckoProcessType_Default: + case GeckoProcessType_Content: + case GeckoProcessType_Socket: + return Telemetry::CanRecordReleaseData(); + case GeckoProcessType_RDD: + case GeckoProcessType_Utility: + // For RDD and Utility process, we check the telemetry settings in + // RDDChild::Init() / UtilityProcessChild::Init() running in the browser + // process because CanRecordReleaseData() always returns false here. + return true; + default: + return false; + } +} + +/* static */ +RefPtr<UntrustedModulesProcessor> UntrustedModulesProcessor::Create( + bool aIsReadyForBackgroundProcessing) { + if (!IsSupportedProcessType()) { + return nullptr; + } + + RefPtr<UntrustedModulesProcessor> result( + new UntrustedModulesProcessor(aIsReadyForBackgroundProcessing)); + return result.forget(); +} + +NS_IMPL_ISUPPORTS(UntrustedModulesProcessor, nsIObserver, nsIThreadPoolListener) + +static const uint32_t kThreadTimeoutMS = 120000; // 2 minutes + +UntrustedModulesProcessor::UntrustedModulesProcessor( + bool aIsReadyForBackgroundProcessing) + : mThread(new LazyIdleThread(kThreadTimeoutMS, "Untrusted Modules", + LazyIdleThread::ManualShutdown)), + mThreadHandleMutex( + "mozilla::UntrustedModulesProcessor::mThreadHandleMutex"), + mUnprocessedMutex( + "mozilla::UntrustedModulesProcessor::mUnprocessedMutex"), + mModuleCacheMutex( + "mozilla::UntrustedModulesProcessor::mModuleCacheMutex"), + mStatus(aIsReadyForBackgroundProcessing ? Status::Allowed + : Status::StartingUp) { + AddObservers(); +} + +void UntrustedModulesProcessor::AddObservers() { + nsCOMPtr<nsIObserverService> obsServ(services::GetObserverService()); + obsServ->AddObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID, false); + obsServ->AddObserver(this, "xpcom-shutdown-threads", false); + obsServ->AddObserver(this, "unblock-untrusted-modules-thread", false); + if (XRE_IsContentProcess()) { + obsServ->AddObserver(this, "content-child-will-shutdown", false); + } + mThread->SetListener(this); +} + +bool UntrustedModulesProcessor::IsReadyForBackgroundProcessing() const { + return mStatus == Status::Allowed; +} + +void UntrustedModulesProcessor::Disable() { + // Ensure that mThread cannot run at low priority anymore + { + MutexAutoLock lock(mThreadHandleMutex); + BackgroundPriorityRegion::Clear(mThreadHandle); + } + + // No more background processing allowed beyond this point + if (mStatus.exchange(Status::ShuttingDown) != Status::Allowed) { + return; + } + + MutexAutoLock lock(mUnprocessedMutex); + CancelScheduledProcessing(lock); +} + +NS_IMETHODIMP UntrustedModulesProcessor::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID) || + !strcmp(aTopic, "content-child-will-shutdown")) { + Disable(); + return NS_OK; + } + + if (!strcmp(aTopic, "xpcom-shutdown-threads")) { + Disable(); + mThread->Shutdown(); + + RemoveObservers(); + + mThread = nullptr; + return NS_OK; + } + + if (!strcmp(aTopic, "unblock-untrusted-modules-thread")) { + nsCOMPtr<nsIObserverService> obs(services::GetObserverService()); + obs->RemoveObserver(this, "unblock-untrusted-modules-thread"); + + mStatus.compareExchange(Status::StartingUp, Status::Allowed); + + if (!IsReadyForBackgroundProcessing()) { + // If we're shutting down, stop here. + return NS_OK; + } + + if (XRE_IsParentProcess()) { + // Propagate notification to child processes + nsTArray<dom::ContentParent*> contentProcesses; + dom::ContentParent::GetAll(contentProcesses); + for (auto* proc : contentProcesses) { + Unused << proc->SendUnblockUntrustedModulesThread(); + } + if (auto* proc = net::SocketProcessParent::GetSingleton()) { + Unused << proc->SendUnblockUntrustedModulesThread(); + } + if (auto* rddMgr = RDDProcessManager::Get()) { + if (auto* proc = rddMgr->GetRDDChild()) { + Unused << proc->SendUnblockUntrustedModulesThread(); + } + } + } + + return NS_OK; + } + + MOZ_ASSERT_UNREACHABLE("Not reachable"); + + return NS_OK; +} + +NS_IMETHODIMP UntrustedModulesProcessor::OnThreadCreated() { + // Whenever a backing lazy thread is created, record a thread handle to it. + HANDLE threadHandle; + if (!::DuplicateHandle( + ::GetCurrentProcess(), ::GetCurrentThread(), ::GetCurrentProcess(), + &threadHandle, + THREAD_QUERY_LIMITED_INFORMATION | THREAD_SET_LIMITED_INFORMATION, + FALSE, 0)) { + MOZ_ASSERT_UNREACHABLE("DuplicateHandle failed on GetCurrentThread()?"); + threadHandle = nullptr; + } + MutexAutoLock lock(mThreadHandleMutex); + mThreadHandle.own(threadHandle); + return NS_OK; +} + +NS_IMETHODIMP UntrustedModulesProcessor::OnThreadShuttingDown() { + // When a lazy thread shuts down, clean up the thread handle reference unless + // it's already been replaced. + MutexAutoLock lock(mThreadHandleMutex); + if (mThreadHandle && ::GetCurrentThreadId() == ::GetThreadId(mThreadHandle)) { + mThreadHandle.reset(); + } + return NS_OK; +} + +void UntrustedModulesProcessor::RemoveObservers() { + nsCOMPtr<nsIObserverService> obsServ(services::GetObserverService()); + obsServ->RemoveObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID); + obsServ->RemoveObserver(this, "xpcom-shutdown-threads"); + obsServ->RemoveObserver(this, "unblock-untrusted-modules-thread"); + if (XRE_IsContentProcess()) { + obsServ->RemoveObserver(this, "content-child-will-shutdown"); + } + mThread->SetListener(nullptr); +} + +void UntrustedModulesProcessor::ScheduleNonEmptyQueueProcessing( + const MutexAutoLock& aProofOfLock) { + // In case something tried to load a DLL during shutdown + if (!mThread) { + return; + } + +#if defined(ENABLE_TESTS) + // Don't bother scheduling background processing in short-lived xpcshell + // processes; it makes the test suites take too long. + if (MOZ_UNLIKELY(mozilla::EnvHasValue("XPCSHELL_TEST_PROFILE_DIR"))) { + return; + } +#endif // defined(ENABLE_TESTS) + + if (mIdleRunnable) { + return; + } + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + // Schedule a runnable to trigger background processing once the main thread + // has gone idle. We do it this way to ensure that we don't start doing a + // bunch of processing during periods of heavy main thread activity. + nsCOMPtr<nsIRunnable> idleRunnable(NewCancelableRunnableMethod( + "UntrustedModulesProcessor::DispatchBackgroundProcessing", this, + &UntrustedModulesProcessor::DispatchBackgroundProcessing)); + + if (NS_FAILED(NS_DispatchToMainThreadQueue(do_AddRef(idleRunnable), + EventQueuePriority::Idle))) { + return; + } + + mIdleRunnable = std::move(idleRunnable); +} + +void UntrustedModulesProcessor::CancelScheduledProcessing( + const MutexAutoLock& aProofOfLock) { + if (!mIdleRunnable) { + return; + } + + nsCOMPtr<nsICancelableRunnable> cancelable(do_QueryInterface(mIdleRunnable)); + if (cancelable) { + // Stop the pending idle runnable from doing anything + cancelable->Cancel(); + } + + mIdleRunnable = nullptr; +} + +void UntrustedModulesProcessor::DispatchBackgroundProcessing() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + nsCOMPtr<nsIRunnable> runnable(NewRunnableMethod( + "UntrustedModulesProcessor::BackgroundProcessModuleLoadQueue", this, + &UntrustedModulesProcessor::BackgroundProcessModuleLoadQueue)); + + mThread->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); +} + +void UntrustedModulesProcessor::Enqueue( + glue::EnhancedModuleLoadInfo&& aModLoadInfo) { + if (mStatus == Status::ShuttingDown) { + return; + } + + { + MutexAutoLock lock(mThreadHandleMutex); + DWORD bgThreadId = ::GetThreadId(mThreadHandle); + if (aModLoadInfo.mNtLoadInfo.mThreadId == bgThreadId) { + // Exclude loads that were caused by our own background thread + return; + } + } + + MutexAutoLock lock(mUnprocessedMutex); + + mUnprocessedModuleLoads.insertBack( + new UnprocessedModuleLoadInfoContainer(std::move(aModLoadInfo))); + + ScheduleNonEmptyQueueProcessing(lock); +} + +void UntrustedModulesProcessor::Enqueue(ModuleLoadInfoVec&& aEvents) { + if (mStatus == Status::ShuttingDown) { + return; + } + + // We do not need to attempt to exclude our background thread in this case + // because |aEvents| was accumulated prior to |mThread|'s existence. + + MutexAutoLock lock(mUnprocessedMutex); + + for (auto& event : aEvents) { + mUnprocessedModuleLoads.insertBack( + new UnprocessedModuleLoadInfoContainer(std::move(event))); + } + + ScheduleNonEmptyQueueProcessing(lock); +} + +void UntrustedModulesProcessor::AssertRunningOnLazyIdleThread() { +#if defined(DEBUG) + MOZ_ASSERT(mThread->IsOnCurrentThread()); +#endif // defined(DEBUG) +} + +RefPtr<UntrustedModulesPromise> UntrustedModulesProcessor::GetProcessedData() { + MOZ_ASSERT(NS_IsMainThread()); + + // Clear any background priority in case background processing is running. + { + MutexAutoLock lock(mThreadHandleMutex); + BackgroundPriorityRegion::Clear(mThreadHandle); + } + + RefPtr<UntrustedModulesProcessor> self(this); + return InvokeAsync(mThread, __func__, [self = std::move(self)]() { + return self->GetProcessedDataInternal(); + }); +} + +RefPtr<ModulesTrustPromise> UntrustedModulesProcessor::GetModulesTrust( + ModulePaths&& aModPaths, bool aRunAtNormalPriority) { + MOZ_ASSERT(XRE_IsParentProcess() && NS_IsMainThread()); + + if (!IsReadyForBackgroundProcessing()) { + return ModulesTrustPromise::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + + RefPtr<UntrustedModulesProcessor> self(this); + auto run = [self = std::move(self), modPaths = std::move(aModPaths), + runNormal = aRunAtNormalPriority]() mutable { + return self->GetModulesTrustInternal(std::move(modPaths), runNormal); + }; + + if (aRunAtNormalPriority) { + // Clear any background priority in case background processing is running. + { + MutexAutoLock lock(mThreadHandleMutex); + BackgroundPriorityRegion::Clear(mThreadHandle); + } + + return InvokeAsync(mThread, __func__, std::move(run)); + } + + RefPtr<ModulesTrustPromise::Private> p( + new ModulesTrustPromise::Private(__func__)); + nsCOMPtr<nsISerialEventTarget> evtTarget(mThread); + const char* source = __func__; + + auto runWrap = [evtTarget = std::move(evtTarget), p, source, + run = std::move(run)]() mutable -> void { + InvokeAsync(evtTarget, source, std::move(run))->ChainTo(p.forget(), source); + }; + + nsCOMPtr<nsIRunnable> idleRunnable( + NS_NewRunnableFunction(source, std::move(runWrap))); + + nsresult rv = NS_DispatchToMainThreadQueue(idleRunnable.forget(), + EventQueuePriority::Idle); + if (NS_FAILED(rv)) { + p->Reject(rv, source); + } + + return p; +} + +RefPtr<UntrustedModulesPromise> +UntrustedModulesProcessor::GetProcessedDataInternal() { + AssertRunningOnLazyIdleThread(); + if (!XRE_IsParentProcess()) { + return GetProcessedDataInternalChildProcess(); + } + + ProcessModuleLoadQueue(); + + return GetAllProcessedData(__func__); +} + +RefPtr<UntrustedModulesPromise> UntrustedModulesProcessor::GetAllProcessedData( + const char* aSource) { + AssertRunningOnLazyIdleThread(); + + UntrustedModulesData result; + + if (!mProcessedModuleLoads) { + return UntrustedModulesPromise::CreateAndResolve(Nothing(), aSource); + } + + result.Swap(mProcessedModuleLoads); + + result.mElapsed = TimeStamp::Now() - TimeStamp::ProcessCreation(); + + return UntrustedModulesPromise::CreateAndResolve( + Some(UntrustedModulesData(std::move(result))), aSource); +} + +RefPtr<UntrustedModulesPromise> +UntrustedModulesProcessor::GetProcessedDataInternalChildProcess() { + AssertRunningOnLazyIdleThread(); + MOZ_ASSERT(!XRE_IsParentProcess()); + + RefPtr<GetModulesTrustPromise> whenProcessed( + ProcessModuleLoadQueueChildProcess(Priority::Default)); + + RefPtr<UntrustedModulesProcessor> self(this); + RefPtr<UntrustedModulesPromise::Private> p( + new UntrustedModulesPromise::Private(__func__)); + nsCOMPtr<nsISerialEventTarget> evtTarget(mThread); + + const char* source = __func__; + auto completionRoutine = [evtTarget = std::move(evtTarget), p, + self = std::move(self), source, + whenProcessed = std::move(whenProcessed)]() { + MOZ_ASSERT(NS_IsMainThread()); + if (!self->IsReadyForBackgroundProcessing()) { + // We can't do any more work, just reject all the things + whenProcessed->Then( + GetMainThreadSerialEventTarget(), source, + [p, source](Maybe<ModulesMapResultWithLoads>&& aResult) { + p->Reject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN, source); + }, + [p, source](nsresult aRv) { p->Reject(aRv, source); }); + return; + } + + whenProcessed->Then( + evtTarget, source, + [p, self = std::move(self), + source](Maybe<ModulesMapResultWithLoads>&& aResult) mutable { + if (aResult.isSome()) { + self->CompleteProcessing(std::move(aResult.ref())); + } + self->GetAllProcessedData(source)->ChainTo(p.forget(), source); + }, + [p, source](nsresult aRv) { p->Reject(aRv, source); }); + }; + + // We always send |completionRoutine| on a trip through the main thread + // due to some subtlety with |mThread| being a LazyIdleThread: we can only + // Dispatch or Then to |mThread| from its creating thread, which is the + // main thread. Hopefully we can get rid of this in the future and just + // invoke whenProcessed->Then() directly. + nsresult rv = NS_DispatchToMainThread( + NS_NewRunnableFunction(__func__, std::move(completionRoutine))); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (NS_FAILED(rv)) { + p->Reject(rv, __func__); + } + + return p; +} + +void UntrustedModulesProcessor::BackgroundProcessModuleLoadQueue() { + if (!IsReadyForBackgroundProcessing()) { + return; + } + + BackgroundPriorityRegion bgRgn; + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + ProcessModuleLoadQueue(); +} + +RefPtr<ModuleRecord> UntrustedModulesProcessor::GetOrAddModuleRecord( + const ModuleEvaluator& aModEval, const nsAString& aResolvedNtPath) { + MOZ_ASSERT(XRE_IsParentProcess()); + + MutexAutoLock lock(mModuleCacheMutex); + return mGlobalModuleCache.WithEntryHandle( + aResolvedNtPath, [&](auto&& addPtr) -> RefPtr<ModuleRecord> { + if (addPtr) { + return addPtr.Data(); + } + + RefPtr<ModuleRecord> newMod(new ModuleRecord(aResolvedNtPath)); + if (!(*newMod)) { + return nullptr; + } + + Maybe<ModuleTrustFlags> maybeTrust = aModEval.GetTrust(*newMod); + if (maybeTrust.isNothing()) { + return nullptr; + } + + newMod->mTrustFlags = maybeTrust.value(); + + return addPtr.Insert(std::move(newMod)); + }); +} + +RefPtr<ModuleRecord> UntrustedModulesProcessor::GetModuleRecord( + const ModulesMap& aModules, + const glue::EnhancedModuleLoadInfo& aModuleLoadInfo) { + MOZ_ASSERT(!XRE_IsParentProcess()); + + return aModules.Get(aModuleLoadInfo.mNtLoadInfo.mSectionName.AsString()); +} + +void UntrustedModulesProcessor::BackgroundProcessModuleLoadQueueChildProcess() { + RefPtr<GetModulesTrustPromise> whenProcessed( + ProcessModuleLoadQueueChildProcess(Priority::Background)); + + RefPtr<UntrustedModulesProcessor> self(this); + nsCOMPtr<nsISerialEventTarget> evtTarget(mThread); + + const char* source = __func__; + auto completionRoutine = [evtTarget = std::move(evtTarget), + self = std::move(self), source, + whenProcessed = std::move(whenProcessed)]() { + MOZ_ASSERT(NS_IsMainThread()); + if (!self->IsReadyForBackgroundProcessing()) { + // We can't do any more work, just no-op + whenProcessed->Then( + GetMainThreadSerialEventTarget(), source, + [](Maybe<ModulesMapResultWithLoads>&& aResult) {}, + [](nsresult aRv) {}); + return; + } + + whenProcessed->Then( + evtTarget, source, + [self = std::move(self)](Maybe<ModulesMapResultWithLoads>&& aResult) { + if (aResult.isNothing() || !self->IsReadyForBackgroundProcessing()) { + // Nothing to do + return; + } + + BackgroundPriorityRegion bgRgn; + self->CompleteProcessing(std::move(aResult.ref())); + }, + [](nsresult aRv) {}); + }; + + // We always send |completionRoutine| on a trip through the main thread + // due to some subtlety with |mThread| being a LazyIdleThread: we can only + // Dispatch or Then to |mThread| from its creating thread, which is the + // main thread. Hopefully we can get rid of this in the future and just + // invoke whenProcessed->Then() directly. + DebugOnly<nsresult> rv = NS_DispatchToMainThread( + NS_NewRunnableFunction(__func__, std::move(completionRoutine))); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +UnprocessedModuleLoads UntrustedModulesProcessor::ExtractLoadingEventsToProcess( + size_t aMaxLength) { + UnprocessedModuleLoads loadsToProcess; + + MutexAutoLock lock(mUnprocessedMutex); + CancelScheduledProcessing(lock); + + loadsToProcess.splice(0, mUnprocessedModuleLoads, 0, aMaxLength); + return loadsToProcess; +} + +// This function contains multiple IsReadyForBackgroundProcessing() checks so +// that we can quickly bail out at the first sign of shutdown. This may be +// important when the current thread is running under background priority. +void UntrustedModulesProcessor::ProcessModuleLoadQueue() { + AssertRunningOnLazyIdleThread(); + if (!XRE_IsParentProcess()) { + BackgroundProcessModuleLoadQueueChildProcess(); + return; + } + + UnprocessedModuleLoads loadsToProcess = + ExtractLoadingEventsToProcess(UntrustedModulesData::kMaxEvents); + if (!IsReadyForBackgroundProcessing() || loadsToProcess.isEmpty()) { + return; + } + + ModuleEvaluator modEval; + MOZ_ASSERT(!!modEval); + if (!modEval) { + return; + } + + Telemetry::BatchProcessedStackGenerator stackProcessor; + Maybe<double> maybeXulLoadDuration; + Vector<Telemetry::ProcessedStack> processedStacks; + UntrustedModuleLoadingEvents processedEvents; + uint32_t sanitizationFailures = 0; + uint32_t trustTestFailures = 0; + + for (UnprocessedModuleLoadInfoContainer* container : loadsToProcess) { + glue::EnhancedModuleLoadInfo& entry = container->mInfo; + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + RefPtr<ModuleRecord> module(GetOrAddModuleRecord( + modEval, entry.mNtLoadInfo.mSectionName.AsString())); + if (!module) { + // We failed to obtain trust information about the module. + // Don't include test failures in the ping to avoid flooding it. + ++trustTestFailures; + continue; + } + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + glue::EnhancedModuleLoadInfo::BacktraceType backtrace = + std::move(entry.mNtLoadInfo.mBacktrace); + ProcessedModuleLoadEvent event(std::move(entry), std::move(module)); + + if (!event) { + // We don't have a sanitized DLL path, so we cannot include this event + // for privacy reasons. + ++sanitizationFailures; + continue; + } + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + if (event.IsTrusted()) { + if (event.IsXULLoad()) { + maybeXulLoadDuration = event.mLoadDurationMS; + } + + // Trusted modules are not included in the ping + continue; + } + + mProcessedModuleLoads.mModules.LookupOrInsert( + event.mModule->mResolvedNtName, event.mModule); + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + Telemetry::ProcessedStack processedStack = + stackProcessor.GetStackAndModules(backtrace); + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + Unused << processedStacks.emplaceBack(std::move(processedStack)); + processedEvents.insertBack( + new ProcessedModuleLoadEventContainer(std::move(event))); + } + + if (processedStacks.empty() && processedEvents.isEmpty() && + !sanitizationFailures && !trustTestFailures) { + // Nothing to save + return; + } + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + // Modules have been added to mProcessedModuleLoads.mModules + // in the loop above. Passing an empty ModulesMap to AddNewLoads. + mProcessedModuleLoads.AddNewLoads(ModulesMap{}, std::move(processedEvents), + std::move(processedStacks)); + if (maybeXulLoadDuration) { + MOZ_ASSERT(!mProcessedModuleLoads.mXULLoadDurationMS); + mProcessedModuleLoads.mXULLoadDurationMS = maybeXulLoadDuration; + } + + mProcessedModuleLoads.mSanitizationFailures += sanitizationFailures; + mProcessedModuleLoads.mTrustTestFailures += trustTestFailures; +} + +template <typename ActorT> +static RefPtr<GetModulesTrustIpcPromise> SendGetModulesTrust( + ActorT* aActor, ModulePaths&& aModPaths, bool aRunAtNormalPriority) { + MOZ_ASSERT(NS_IsMainThread()); + return aActor->SendGetModulesTrust(std::move(aModPaths), + aRunAtNormalPriority); +} + +RefPtr<GetModulesTrustIpcPromise> +UntrustedModulesProcessor::SendGetModulesTrust(ModulePaths&& aModules, + Priority aPriority) { + MOZ_ASSERT(NS_IsMainThread()); + bool runNormal = aPriority == Priority::Default; + + switch (XRE_GetProcessType()) { + case GeckoProcessType_Content: { + return ::mozilla::SendGetModulesTrust(dom::ContentChild::GetSingleton(), + std::move(aModules), runNormal); + } + case GeckoProcessType_RDD: { + return ::mozilla::SendGetModulesTrust(RDDParent::GetSingleton(), + std::move(aModules), runNormal); + } + case GeckoProcessType_Socket: { + return ::mozilla::SendGetModulesTrust( + net::SocketProcessChild::GetSingleton(), std::move(aModules), + runNormal); + } + case GeckoProcessType_Utility: { + return ::mozilla::SendGetModulesTrust( + ipc::UtilityProcessChild::GetSingleton().get(), std::move(aModules), + runNormal); + } + default: { + MOZ_ASSERT_UNREACHABLE("Unsupported process type"); + return GetModulesTrustIpcPromise::CreateAndReject( + ipc::ResponseRejectReason::SendError, __func__); + } + } +} + +/** + * This method works very similarly to ProcessModuleLoadQueue, with the + * exception that a sandboxed child process does not have sufficient rights to + * be able to evaluate a module's trustworthiness. Instead, we accumulate the + * resolved paths for all of the modules in this batch and send them to the + * parent to determine trustworthiness. + * + * The parent process returns a list of untrusted modules and invokes + * CompleteProcessing to handle the remainder of the process. + * + * By doing it this way, we minimize the amount of data that needs to be sent + * over IPC and avoid the need to process every load's metadata only + * to throw most of it away (since most modules will be trusted). + */ +RefPtr<UntrustedModulesProcessor::GetModulesTrustPromise> +UntrustedModulesProcessor::ProcessModuleLoadQueueChildProcess( + UntrustedModulesProcessor::Priority aPriority) { + AssertRunningOnLazyIdleThread(); + MOZ_ASSERT(!XRE_IsParentProcess()); + + UnprocessedModuleLoads loadsToProcess = + ExtractLoadingEventsToProcess(UntrustedModulesData::kMaxEvents); + if (loadsToProcess.isEmpty()) { + // Nothing to process + return GetModulesTrustPromise::CreateAndResolve(Nothing(), __func__); + } + + if (!IsReadyForBackgroundProcessing()) { + return GetModulesTrustPromise::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + + nsTHashtable<nsStringCaseInsensitiveHashKey> moduleNtPathSet; + + // Build a set of modules to be processed by the parent + for (UnprocessedModuleLoadInfoContainer* container : loadsToProcess) { + glue::EnhancedModuleLoadInfo& entry = container->mInfo; + + if (!IsReadyForBackgroundProcessing()) { + return GetModulesTrustPromise::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + + moduleNtPathSet.PutEntry(entry.mNtLoadInfo.mSectionName.AsString()); + } + + if (!IsReadyForBackgroundProcessing()) { + return GetModulesTrustPromise::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + + MOZ_ASSERT(!moduleNtPathSet.IsEmpty()); + if (moduleNtPathSet.IsEmpty()) { + // Nothing to process + return GetModulesTrustPromise::CreateAndResolve(Nothing(), __func__); + } + + ModulePaths moduleNtPaths(std::move(moduleNtPathSet)); + + if (!IsReadyForBackgroundProcessing()) { + return GetModulesTrustPromise::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + + RefPtr<UntrustedModulesProcessor> self(this); + + auto invoker = [self = std::move(self), + moduleNtPaths = std::move(moduleNtPaths), + priority = aPriority]() mutable { + return self->SendGetModulesTrust(std::move(moduleNtPaths), priority); + }; + + RefPtr<GetModulesTrustPromise::Private> p( + new GetModulesTrustPromise::Private(__func__)); + + if (!IsReadyForBackgroundProcessing()) { + p->Reject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + return p; + } + + // Send the IPC request via the main thread + InvokeAsync(GetMainThreadSerialEventTarget(), __func__, std::move(invoker)) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [p, loads = std::move(loadsToProcess)]( + Maybe<ModulesMapResult>&& aResult) mutable { + ModulesMapResultWithLoads result(std::move(aResult), + std::move(loads)); + p->Resolve(Some(ModulesMapResultWithLoads(std::move(result))), + __func__); + }, + [p](ipc::ResponseRejectReason aReason) { + p->Reject(NS_ERROR_FAILURE, __func__); + }); + + return p; +} + +void UntrustedModulesProcessor::CompleteProcessing( + UntrustedModulesProcessor::ModulesMapResultWithLoads&& aModulesAndLoads) { + MOZ_ASSERT(!XRE_IsParentProcess()); + AssertRunningOnLazyIdleThread(); + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + if (aModulesAndLoads.mModMapResult.isNothing()) { + // No untrusted modules in this batch, nothing to save. + return; + } + + // This map only contains information about modules deemed to be untrusted, + // plus xul.dll. Any module referenced by load requests that is *not* in the + // map is deemed to be trusted. + ModulesMap& modules = aModulesAndLoads.mModMapResult.ref().mModules; + const uint32_t& trustTestFailures = + aModulesAndLoads.mModMapResult.ref().mTrustTestFailures; + UnprocessedModuleLoads& loads = aModulesAndLoads.mLoads; + + if (modules.IsEmpty() && !trustTestFailures) { + // No data, nothing to save. + return; + } + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + Telemetry::BatchProcessedStackGenerator stackProcessor; + + Maybe<double> maybeXulLoadDuration; + Vector<Telemetry::ProcessedStack> processedStacks; + UntrustedModuleLoadingEvents processedEvents; + uint32_t sanitizationFailures = 0; + + if (!modules.IsEmpty()) { + for (UnprocessedModuleLoadInfoContainer* container : loads) { + glue::EnhancedModuleLoadInfo& item = container->mInfo; + if (!IsReadyForBackgroundProcessing()) { + return; + } + + RefPtr<ModuleRecord> module(GetModuleRecord(modules, item)); + if (!module) { + // If module is null then |item| is trusted + continue; + } + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + glue::EnhancedModuleLoadInfo::BacktraceType backtrace = + std::move(item.mNtLoadInfo.mBacktrace); + ProcessedModuleLoadEvent event(std::move(item), std::move(module)); + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + if (!event) { + // We don't have a sanitized DLL path, so we cannot include this event + // for privacy reasons. + ++sanitizationFailures; + continue; + } + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + if (event.IsXULLoad()) { + maybeXulLoadDuration = event.mLoadDurationMS; + // We saved the XUL load duration, but it is still trusted, so we + // continue. + continue; + } + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + Telemetry::ProcessedStack processedStack = + stackProcessor.GetStackAndModules(backtrace); + + Unused << processedStacks.emplaceBack(std::move(processedStack)); + processedEvents.insertBack( + new ProcessedModuleLoadEventContainer(std::move(event))); + } + } + + if (processedStacks.empty() && processedEvents.isEmpty() && + !sanitizationFailures && !trustTestFailures) { + // Nothing to save + return; + } + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + mProcessedModuleLoads.AddNewLoads(modules, std::move(processedEvents), + std::move(processedStacks)); + if (maybeXulLoadDuration) { + MOZ_ASSERT(!mProcessedModuleLoads.mXULLoadDurationMS); + mProcessedModuleLoads.mXULLoadDurationMS = maybeXulLoadDuration; + } + + mProcessedModuleLoads.mSanitizationFailures += sanitizationFailures; + mProcessedModuleLoads.mTrustTestFailures += trustTestFailures; +} + +// The thread priority of this job should match the priority that the child +// process is running with, as specified by |aRunAtNormalPriority|. +RefPtr<ModulesTrustPromise> UntrustedModulesProcessor::GetModulesTrustInternal( + ModulePaths&& aModPaths, bool aRunAtNormalPriority) { + MOZ_ASSERT(XRE_IsParentProcess()); + AssertRunningOnLazyIdleThread(); + + if (!IsReadyForBackgroundProcessing()) { + return ModulesTrustPromise::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + + if (aRunAtNormalPriority) { + return GetModulesTrustInternal(std::move(aModPaths)); + } + + BackgroundPriorityRegion bgRgn; + return GetModulesTrustInternal(std::move(aModPaths)); +} + +// For each module in |aModPaths|, evaluate its trustworthiness and only send +// ModuleRecords for untrusted modules back to the child process. We also save +// XUL's ModuleRecord so that the child process may report XUL's load time. +RefPtr<ModulesTrustPromise> UntrustedModulesProcessor::GetModulesTrustInternal( + ModulePaths&& aModPaths) { + MOZ_ASSERT(XRE_IsParentProcess()); + AssertRunningOnLazyIdleThread(); + + ModulesMapResult result; + + ModulesMap& modMap = result.mModules; + uint32_t& trustTestFailures = result.mTrustTestFailures; + + ModuleEvaluator modEval; + MOZ_ASSERT(!!modEval); + if (!modEval) { + return ModulesTrustPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + for (auto& resolvedNtPath : + aModPaths.mModuleNtPaths.as<ModulePaths::VecType>()) { + if (!IsReadyForBackgroundProcessing()) { + return ModulesTrustPromise::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + + MOZ_ASSERT(!resolvedNtPath.IsEmpty()); + if (resolvedNtPath.IsEmpty()) { + continue; + } + + RefPtr<ModuleRecord> module(GetOrAddModuleRecord(modEval, resolvedNtPath)); + if (!module) { + // We failed to obtain trust information. + ++trustTestFailures; + continue; + } + + if (!IsReadyForBackgroundProcessing()) { + return ModulesTrustPromise::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + + if (module->IsTrusted() && !module->IsXUL()) { + // If the module is trusted we exclude it from results, unless it's XUL. + // (We save XUL so that the child process may report XUL's load time) + continue; + } + + if (!IsReadyForBackgroundProcessing()) { + return ModulesTrustPromise::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + + modMap.InsertOrUpdate(resolvedNtPath, std::move(module)); + } + + return ModulesTrustPromise::CreateAndResolve(std::move(result), __func__); +} + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/UntrustedModulesProcessor.h b/toolkit/xre/dllservices/UntrustedModulesProcessor.h new file mode 100644 index 0000000000..b91d88961a --- /dev/null +++ b/toolkit/xre/dllservices/UntrustedModulesProcessor.h @@ -0,0 +1,185 @@ +/* -*- 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/. */ + +#ifndef mozilla_UntrustedModulesProcessor_h +#define mozilla_UntrustedModulesProcessor_h + +#include "mozilla/Atomics.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/glue/WindowsDllServices.h" +#include "mozilla/LazyIdleThread.h" +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UntrustedModulesData.h" +#include "mozilla/Vector.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "nsCOMPtr.h" +#include "nsIObserver.h" +#include "nsIRunnable.h" +#include "nsISupportsImpl.h" +#include "nsString.h" + +namespace mozilla { + +class ModuleEvaluator; + +using UntrustedModulesPromise = + MozPromise<Maybe<UntrustedModulesData>, nsresult, true>; + +using ModulesTrustPromise = MozPromise<ModulesMapResult, nsresult, true>; + +using GetModulesTrustIpcPromise = + MozPromise<Maybe<ModulesMapResult>, ipc::ResponseRejectReason, true>; + +struct UnprocessedModuleLoadInfoContainer final + : public LinkedListElement<UnprocessedModuleLoadInfoContainer> { + glue::EnhancedModuleLoadInfo mInfo; + + template <typename T> + explicit UnprocessedModuleLoadInfoContainer(T&& aInfo) + : mInfo(std::move(aInfo)) {} + + UnprocessedModuleLoadInfoContainer( + const UnprocessedModuleLoadInfoContainer&) = delete; + UnprocessedModuleLoadInfoContainer& operator=( + const UnprocessedModuleLoadInfoContainer&) = delete; +}; +using UnprocessedModuleLoads = + AutoCleanLinkedList<UnprocessedModuleLoadInfoContainer>; + +class UntrustedModulesProcessor final : public nsIObserver, + public nsIThreadPoolListener { + public: + static RefPtr<UntrustedModulesProcessor> Create( + bool aIsReadyForBackgroundProcessing); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSITHREADPOOLLISTENER + + // Called to check if the parent process is ready when a child process + // is spanwed + bool IsReadyForBackgroundProcessing() const; + + // Called by DLL Services to explicitly begin shutting down + void Disable(); + + // Called by DLL Services to submit module load data to the processor + void Enqueue(glue::EnhancedModuleLoadInfo&& aModLoadInfo); + void Enqueue(ModuleLoadInfoVec&& aEvents); + + // Called by telemetry to retrieve the processed data + RefPtr<UntrustedModulesPromise> GetProcessedData(); + + // Called by IPC actors in the parent process to evaluate module trust + // on behalf of child processes + RefPtr<ModulesTrustPromise> GetModulesTrust(ModulePaths&& aModPaths, + bool aRunAtNormalPriority); + + UntrustedModulesProcessor(const UntrustedModulesProcessor&) = delete; + UntrustedModulesProcessor(UntrustedModulesProcessor&&) = delete; + UntrustedModulesProcessor& operator=(const UntrustedModulesProcessor&) = + delete; + UntrustedModulesProcessor& operator=(UntrustedModulesProcessor&&) = delete; + + private: + ~UntrustedModulesProcessor() = default; + explicit UntrustedModulesProcessor(bool aIsReadyForBackgroundProcessing); + + static bool IsSupportedProcessType(); + + void AddObservers(); + void RemoveObservers(); + + void ScheduleNonEmptyQueueProcessing(const MutexAutoLock& aProofOfLock) + MOZ_REQUIRES(mUnprocessedMutex); + void CancelScheduledProcessing(const MutexAutoLock& aProofOfLock) + MOZ_REQUIRES(mUnprocessedMutex); + void DispatchBackgroundProcessing(); + + void BackgroundProcessModuleLoadQueue(); + void ProcessModuleLoadQueue(); + + // Extract the loading events from mUnprocessedModuleLoads to process and + // move to mProcessedModuleLoads. It's guaranteed that the total length of + // mProcessedModuleLoads will not exceed |aMaxLength|. + UnprocessedModuleLoads ExtractLoadingEventsToProcess(size_t aMaxLength); + + class ModulesMapResultWithLoads final { + public: + ModulesMapResultWithLoads(Maybe<ModulesMapResult>&& aModMapResult, + UnprocessedModuleLoads&& aLoads) + : mModMapResult(std::move(aModMapResult)), mLoads(std::move(aLoads)) {} + Maybe<ModulesMapResult> mModMapResult; + UnprocessedModuleLoads mLoads; + }; + + using GetModulesTrustPromise = + MozPromise<Maybe<ModulesMapResultWithLoads>, nsresult, true>; + + enum class Priority { Default, Background }; + + RefPtr<GetModulesTrustPromise> ProcessModuleLoadQueueChildProcess( + Priority aPriority); + void BackgroundProcessModuleLoadQueueChildProcess(); + + void AssertRunningOnLazyIdleThread(); + + RefPtr<UntrustedModulesPromise> GetProcessedDataInternal(); + RefPtr<UntrustedModulesPromise> GetProcessedDataInternalChildProcess(); + + RefPtr<ModulesTrustPromise> GetModulesTrustInternal( + ModulePaths&& aModPaths, bool aRunAtNormalPriority); + RefPtr<ModulesTrustPromise> GetModulesTrustInternal(ModulePaths&& aModPaths); + + // This function is only called by the parent process + RefPtr<ModuleRecord> GetOrAddModuleRecord(const ModuleEvaluator& aModEval, + const nsAString& aResolvedNtPath); + + // Only called by child processes + RefPtr<ModuleRecord> GetModuleRecord( + const ModulesMap& aModules, + const glue::EnhancedModuleLoadInfo& aModuleLoadInfo); + + RefPtr<GetModulesTrustIpcPromise> SendGetModulesTrust(ModulePaths&& aModules, + Priority aPriority); + + void CompleteProcessing(ModulesMapResultWithLoads&& aModulesAndLoads); + RefPtr<UntrustedModulesPromise> GetAllProcessedData(const char* aSource); + + private: + RefPtr<LazyIdleThread> mThread; + + Mutex mThreadHandleMutex; + Mutex mUnprocessedMutex; + Mutex mModuleCacheMutex; + + // Windows HANDLE for the currently active mThread, if active. + nsAutoHandle mThreadHandle MOZ_GUARDED_BY(mThreadHandleMutex); + + // The members in this group are protected by mUnprocessedMutex + UnprocessedModuleLoads mUnprocessedModuleLoads + MOZ_GUARDED_BY(mUnprocessedMutex); + nsCOMPtr<nsIRunnable> mIdleRunnable MOZ_GUARDED_BY(mUnprocessedMutex); + + // This member must only be touched on mThread + UntrustedModulesData mProcessedModuleLoads; + + enum class Status { StartingUp, Allowed, ShuttingDown }; + + // This member may be touched by any thread + Atomic<Status> mStatus; + + // Cache all module records, including ones trusted and ones loaded in + // child processes, in the browser process to avoid evaluating the same + // module multiple times + ModulesMap mGlobalModuleCache MOZ_GUARDED_BY(mModuleCacheMutex); +}; + +} // namespace mozilla + +#endif // mozilla_UntrustedModulesProcessor_h diff --git a/toolkit/xre/dllservices/WinDllServices.cpp b/toolkit/xre/dllservices/WinDllServices.cpp new file mode 100644 index 0000000000..b98a15252b --- /dev/null +++ b/toolkit/xre/dllservices/WinDllServices.cpp @@ -0,0 +1,142 @@ +/* -*- 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/WinDllServices.h" + +#include <windows.h> +#include <psapi.h> + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/Services.h" +#include "mozilla/StaticLocalPtr.h" +#include "mozilla/UntrustedModulesProcessor.h" +#include "nsCOMPtr.h" +#include "nsIObserverService.h" +#include "nsString.h" +#include "nsXULAppAPI.h" + +namespace mozilla { + +const char* DllServices::kTopicDllLoadedMainThread = "dll-loaded-main-thread"; +const char* DllServices::kTopicDllLoadedNonMainThread = + "dll-loaded-non-main-thread"; + +/* static */ +DllServices* DllServices::Get() { + static StaticLocalRefPtr<DllServices> sInstance( + []() -> already_AddRefed<DllServices> { + RefPtr<DllServices> dllSvc(new DllServices()); + // Full DLL services require XPCOM, which GMP doesn't have + if (XRE_IsGMPluginProcess()) { + dllSvc->EnableBasic(); + } else { + dllSvc->EnableFull(); + } + + auto setClearOnShutdown = [ptr = &sInstance]() -> void { + ClearOnShutdown(ptr); + }; + + if (NS_IsMainThread()) { + setClearOnShutdown(); + return dllSvc.forget(); + } + + SchedulerGroup::Dispatch( + TaskCategory::Other, + NS_NewRunnableFunction("mozilla::DllServices::Get", + std::move(setClearOnShutdown))); + + return dllSvc.forget(); + }()); + + return sInstance; +} + +DllServices::~DllServices() { DisableFull(); } + +void DllServices::StartUntrustedModulesProcessor(bool aIsStartingUp) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mUntrustedModulesProcessor); + mUntrustedModulesProcessor = UntrustedModulesProcessor::Create(aIsStartingUp); +} + +bool DllServices::IsReadyForBackgroundProcessing() const { + return mUntrustedModulesProcessor && + mUntrustedModulesProcessor->IsReadyForBackgroundProcessing(); +} + +RefPtr<UntrustedModulesPromise> DllServices::GetUntrustedModulesData() { + if (!mUntrustedModulesProcessor) { + return UntrustedModulesPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, + __func__); + } + + return mUntrustedModulesProcessor->GetProcessedData(); +} + +void DllServices::DisableFull() { + if (XRE_IsGMPluginProcess()) { + return; + } + + if (mUntrustedModulesProcessor) { + mUntrustedModulesProcessor->Disable(); + } + + glue::DllServices::DisableFull(); +} + +RefPtr<ModulesTrustPromise> DllServices::GetModulesTrust( + ModulePaths&& aModPaths, bool aRunAtNormalPriority) { + if (!mUntrustedModulesProcessor) { + return ModulesTrustPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, + __func__); + } + + return mUntrustedModulesProcessor->GetModulesTrust(std::move(aModPaths), + aRunAtNormalPriority); +} + +void DllServices::NotifyDllLoad(glue::EnhancedModuleLoadInfo&& aModLoadInfo) { + MOZ_ASSERT(NS_IsMainThread()); + + const char* topic; + + if (aModLoadInfo.mNtLoadInfo.mThreadId == ::GetCurrentThreadId()) { + topic = kTopicDllLoadedMainThread; + } else { + topic = kTopicDllLoadedNonMainThread; + } + + // We save the path to a nsAutoString because once we have submitted + // aModLoadInfo for processing there is no guarantee that the original + // buffer will continue to be valid. + nsAutoString dllFilePath(aModLoadInfo.GetSectionName()); + + if (mUntrustedModulesProcessor) { + mUntrustedModulesProcessor->Enqueue(std::move(aModLoadInfo)); + } + + nsCOMPtr<nsIObserverService> obsServ(mozilla::services::GetObserverService()); + if (!obsServ) { + return; + } + + obsServ->NotifyObservers(nullptr, topic, dllFilePath.get()); +} + +void DllServices::NotifyModuleLoadBacklog(ModuleLoadInfoVec&& aEvents) { + MOZ_ASSERT(NS_IsMainThread()); + if (!mUntrustedModulesProcessor) { + return; + } + + mUntrustedModulesProcessor->Enqueue(std::move(aEvents)); +} + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/WinDllServices.h b/toolkit/xre/dllservices/WinDllServices.h new file mode 100644 index 0000000000..a5d4f9b363 --- /dev/null +++ b/toolkit/xre/dllservices/WinDllServices.h @@ -0,0 +1,57 @@ +/* -*- 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/. */ + +#ifndef mozilla_WinDllServices_h +#define mozilla_WinDllServices_h + +#include "mozilla/glue/WindowsDllServices.h" +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" +#include "mozilla/RefPtr.h" + +namespace mozilla { + +class UntrustedModulesData; +class UntrustedModulesProcessor; + +using UntrustedModulesPromise = + MozPromise<Maybe<UntrustedModulesData>, nsresult, true>; + +struct ModulePaths; +class ModulesMapResult; + +using ModulesTrustPromise = MozPromise<ModulesMapResult, nsresult, true>; + +class DllServices final : public glue::DllServices { + public: + static DllServices* Get(); + + virtual void DisableFull() override; + + static const char* kTopicDllLoadedMainThread; + static const char* kTopicDllLoadedNonMainThread; + + void StartUntrustedModulesProcessor(bool aIsReadyForBackgroundProcessing); + bool IsReadyForBackgroundProcessing() const; + + RefPtr<UntrustedModulesPromise> GetUntrustedModulesData(); + + RefPtr<ModulesTrustPromise> GetModulesTrust(ModulePaths&& aModPaths, + bool aRunAtNormalPriority); + + private: + DllServices() = default; + ~DllServices(); + + void NotifyDllLoad(glue::EnhancedModuleLoadInfo&& aModLoadInfo) override; + void NotifyModuleLoadBacklog(ModuleLoadInfoVec&& aEvents) override; + + RefPtr<UntrustedModulesProcessor> mUntrustedModulesProcessor; +}; + +} // namespace mozilla + +#endif // mozilla_WinDllServices_h diff --git a/toolkit/xre/dllservices/moz.build b/toolkit/xre/dllservices/moz.build new file mode 100644 index 0000000000..9fb82f6c23 --- /dev/null +++ b/toolkit/xre/dllservices/moz.build @@ -0,0 +1,43 @@ +# -*- 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DLL Services") + +Library("dllservices") + +FINAL_LIBRARY = "xul" + +EXPORTS.mozilla += [ + "DynamicBlocklist.h", + "ModuleVersionInfo.h", + "UntrustedModulesData.h", + "UntrustedModulesProcessor.h", + "WinDllServices.h", +] + +DIRS += [ + "mozglue", +] + +UNIFIED_SOURCES += [ + "ModuleEvaluator.cpp", + "ModuleVersionInfo.cpp", + "UntrustedModulesData.cpp", + "UntrustedModulesProcessor.cpp", + "WinDllServices.cpp", +] + +if CONFIG["MOZ_LAUNCHER_PROCESS"]: + UNIFIED_SOURCES += [ + "DynamicBlocklistWriter.cpp", + ] + +TEST_DIRS += [ + "tests", +] + +include("/ipc/chromium/chromium-config.mozbuild") diff --git a/toolkit/xre/dllservices/mozglue/Authenticode.cpp b/toolkit/xre/dllservices/mozglue/Authenticode.cpp new file mode 100644 index 0000000000..ee702b2ac6 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/Authenticode.cpp @@ -0,0 +1,439 @@ +/* -*- 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/. */ + +// We need Windows 8 functions and structures to be able to verify SHA-256. +#if defined(_WIN32_WINNT) +# undef _WIN32_WINNT +# define _WIN32_WINNT _WIN32_WINNT_WIN8 +#endif // defined(_WIN32_WINNT) +#if defined(NTDDI_VERSION) +# undef NTDDI_VERSION +# define NTDDI_VERSION NTDDI_WIN8 +#endif // defined(NTDDI_VERSION) + +#include "Authenticode.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/DynamicallyLinkedFunctionPtr.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WindowsVersion.h" +#include "nsWindowsHelpers.h" + +#include <windows.h> +#include <softpub.h> +#include <wincrypt.h> +#include <wintrust.h> +#include <mscat.h> + +#include <string.h> + +namespace { + +struct CertStoreDeleter { + typedef HCERTSTORE pointer; + void operator()(pointer aStore) { ::CertCloseStore(aStore, 0); } +}; + +struct CryptMsgDeleter { + typedef HCRYPTMSG pointer; + void operator()(pointer aMsg) { ::CryptMsgClose(aMsg); } +}; + +struct CertContextDeleter { + void operator()(PCCERT_CONTEXT aCertContext) { + ::CertFreeCertificateContext(aCertContext); + } +}; + +struct CATAdminContextDeleter { + typedef HCATADMIN pointer; + void operator()(pointer aCtx) { + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::CryptCATAdminReleaseContext)> + pCryptCATAdminReleaseContext(L"wintrust.dll", + "CryptCATAdminReleaseContext"); + + MOZ_ASSERT(!!pCryptCATAdminReleaseContext); + if (!pCryptCATAdminReleaseContext) { + return; + } + + pCryptCATAdminReleaseContext(aCtx, 0); + } +}; + +typedef mozilla::UniquePtr<HCERTSTORE, CertStoreDeleter> CertStoreUniquePtr; +typedef mozilla::UniquePtr<HCRYPTMSG, CryptMsgDeleter> CryptMsgUniquePtr; +typedef mozilla::UniquePtr<const CERT_CONTEXT, CertContextDeleter> + CertContextUniquePtr; +typedef mozilla::UniquePtr<HCATADMIN, CATAdminContextDeleter> + CATAdminContextUniquePtr; + +static const DWORD kEncodingTypes = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING; + +class SignedBinary final { + public: + SignedBinary(const wchar_t* aFilePath, mozilla::AuthenticodeFlags aFlags); + + explicit operator bool() const { return mCertStore && mCryptMsg && mCertCtx; } + + mozilla::UniquePtr<wchar_t[]> GetOrgName(); + + SignedBinary(const SignedBinary&) = delete; + SignedBinary(SignedBinary&&) = delete; + SignedBinary& operator=(const SignedBinary&) = delete; + SignedBinary& operator=(SignedBinary&&) = delete; + + private: + bool VerifySignature(const wchar_t* aFilePath); + bool QueryObject(const wchar_t* aFilePath); + static bool VerifySignatureInternal(WINTRUST_DATA& aTrustData); + + private: + enum class TrustSource { eNone, eEmbedded, eCatalog }; + + private: + const mozilla::AuthenticodeFlags mFlags; + TrustSource mTrustSource; + CertStoreUniquePtr mCertStore; + CryptMsgUniquePtr mCryptMsg; + CertContextUniquePtr mCertCtx; +}; + +SignedBinary::SignedBinary(const wchar_t* aFilePath, + mozilla::AuthenticodeFlags aFlags) + : mFlags(aFlags), mTrustSource(TrustSource::eNone) { + if (!VerifySignature(aFilePath)) { + return; + } + + DWORD certInfoLen = 0; + BOOL ok = CryptMsgGetParam(mCryptMsg.get(), CMSG_SIGNER_CERT_INFO_PARAM, 0, + nullptr, &certInfoLen); + if (!ok) { + return; + } + + auto certInfoBuf = mozilla::MakeUnique<char[]>(certInfoLen); + + ok = CryptMsgGetParam(mCryptMsg.get(), CMSG_SIGNER_CERT_INFO_PARAM, 0, + certInfoBuf.get(), &certInfoLen); + if (!ok) { + return; + } + + auto certInfo = reinterpret_cast<CERT_INFO*>(certInfoBuf.get()); + + PCCERT_CONTEXT certCtx = + CertFindCertificateInStore(mCertStore.get(), kEncodingTypes, 0, + CERT_FIND_SUBJECT_CERT, certInfo, nullptr); + if (!certCtx) { + return; + } + + mCertCtx.reset(certCtx); +} + +bool SignedBinary::QueryObject(const wchar_t* aFilePath) { + DWORD encodingType, contentType, formatType; + HCERTSTORE rawCertStore; + HCRYPTMSG rawCryptMsg; + BOOL result = ::CryptQueryObject(CERT_QUERY_OBJECT_FILE, aFilePath, + CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED, + CERT_QUERY_FORMAT_FLAG_BINARY, 0, + &encodingType, &contentType, &formatType, + &rawCertStore, &rawCryptMsg, nullptr); + if (!result) { + return false; + } + + mCertStore.reset(rawCertStore); + mCryptMsg.reset(rawCryptMsg); + + return true; +} + +/** + * @param aTrustData must be a WINTRUST_DATA structure that has been zeroed out + * and then populated at least with its |cbStruct|, + * |dwUnionChoice|, and appropriate union field. This function + * will then populate the remaining fields as appropriate. + */ +/* static */ +bool SignedBinary::VerifySignatureInternal(WINTRUST_DATA& aTrustData) { + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::WinVerifyTrust)> + pWinVerifyTrust(L"wintrust.dll", "WinVerifyTrust"); + if (!pWinVerifyTrust) { + return false; + } + + aTrustData.dwUIChoice = WTD_UI_NONE; + aTrustData.fdwRevocationChecks = WTD_REVOKE_NONE; + aTrustData.dwStateAction = WTD_STATEACTION_VERIFY; + aTrustData.dwProvFlags = WTD_CACHE_ONLY_URL_RETRIEVAL; + + const HWND hwnd = (HWND)INVALID_HANDLE_VALUE; + GUID policyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2; + LONG result = pWinVerifyTrust(hwnd, &policyGUID, &aTrustData); + + aTrustData.dwStateAction = WTD_STATEACTION_CLOSE; + pWinVerifyTrust(hwnd, &policyGUID, &aTrustData); + + return result == ERROR_SUCCESS; +} + +bool SignedBinary::VerifySignature(const wchar_t* aFilePath) { + // First, try the binary itself + if (QueryObject(aFilePath)) { + mTrustSource = TrustSource::eEmbedded; + if (mFlags & mozilla::AuthenticodeFlags::SkipTrustVerification) { + return true; + } + + WINTRUST_FILE_INFO fileInfo = {sizeof(fileInfo)}; + fileInfo.pcwszFilePath = aFilePath; + + WINTRUST_DATA trustData = {sizeof(trustData)}; + trustData.dwUnionChoice = WTD_CHOICE_FILE; + trustData.pFile = &fileInfo; + + return VerifySignatureInternal(trustData); + } + + // We didn't find anything in the binary, so now try a catalog file. + + // First, we open a catalog admin context. + HCATADMIN rawCatAdmin; + + // Windows 7 also exports the CryptCATAdminAcquireContext2 API, but it does + // *not* sign its binaries with SHA-256, so we use the old API in that case. + if (mozilla::IsWin8OrLater()) { + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::CryptCATAdminAcquireContext2)> + pCryptCATAdminAcquireContext2(L"wintrust.dll", + "CryptCATAdminAcquireContext2"); + if (!pCryptCATAdminAcquireContext2) { + return false; + } + + CERT_STRONG_SIGN_PARA policy = {sizeof(policy)}; + policy.dwInfoChoice = CERT_STRONG_SIGN_OID_INFO_CHOICE; + policy.pszOID = const_cast<char*>( + szOID_CERT_STRONG_SIGN_OS_CURRENT); // -Wwritable-strings + + if (!pCryptCATAdminAcquireContext2(&rawCatAdmin, nullptr, + BCRYPT_SHA256_ALGORITHM, &policy, 0)) { + return false; + } + } else { + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::CryptCATAdminAcquireContext)> + pCryptCATAdminAcquireContext(L"wintrust.dll", + "CryptCATAdminAcquireContext"); + + if (!pCryptCATAdminAcquireContext || + !pCryptCATAdminAcquireContext(&rawCatAdmin, nullptr, 0)) { + return false; + } + } + + CATAdminContextUniquePtr catAdmin(rawCatAdmin); + + // Now we need to hash the file at aFilePath. + // Since we're hashing this file, let's open it with a sequential scan hint. + HANDLE rawFile = + ::CreateFileW(aFilePath, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, nullptr); + if (rawFile == INVALID_HANDLE_VALUE) { + return false; + } + + nsAutoHandle file(rawFile); + DWORD hashLen = 0; + mozilla::UniquePtr<BYTE[]> hashBuf; + + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::CryptCATAdminCalcHashFromFileHandle2)> + pCryptCATAdminCalcHashFromFileHandle2( + L"wintrust.dll", "CryptCATAdminCalcHashFromFileHandle2"); + if (pCryptCATAdminCalcHashFromFileHandle2) { + if (!pCryptCATAdminCalcHashFromFileHandle2(rawCatAdmin, rawFile, &hashLen, + nullptr, 0) && + ::GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + return false; + } + + hashBuf = mozilla::MakeUnique<BYTE[]>(hashLen); + + if (!pCryptCATAdminCalcHashFromFileHandle2(rawCatAdmin, rawFile, &hashLen, + hashBuf.get(), 0)) { + return false; + } + } else { + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::CryptCATAdminCalcHashFromFileHandle)> + pCryptCATAdminCalcHashFromFileHandle( + L"wintrust.dll", "CryptCATAdminCalcHashFromFileHandle"); + + if (!pCryptCATAdminCalcHashFromFileHandle) { + return false; + } + + if (!pCryptCATAdminCalcHashFromFileHandle(rawFile, &hashLen, nullptr, 0) && + ::GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + return false; + } + + hashBuf = mozilla::MakeUnique<BYTE[]>(hashLen); + + if (!pCryptCATAdminCalcHashFromFileHandle(rawFile, &hashLen, hashBuf.get(), + 0)) { + return false; + } + } + + // Now that we've hashed the file, query the catalog system to see if any + // catalogs reference a binary with our hash. + + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::CryptCATAdminEnumCatalogFromHash)> + pCryptCATAdminEnumCatalogFromHash(L"wintrust.dll", + "CryptCATAdminEnumCatalogFromHash"); + if (!pCryptCATAdminEnumCatalogFromHash) { + return false; + } + + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::CryptCATAdminReleaseCatalogContext)> + pCryptCATAdminReleaseCatalogContext(L"wintrust.dll", + "CryptCATAdminReleaseCatalogContext"); + if (!pCryptCATAdminReleaseCatalogContext) { + return false; + } + + HCATINFO catInfoHdl = pCryptCATAdminEnumCatalogFromHash( + rawCatAdmin, hashBuf.get(), hashLen, 0, nullptr); + if (!catInfoHdl) { + return false; + } + + // We can't use UniquePtr for this because the deleter function requires two + // parameters. + auto cleanCatInfoHdl = + mozilla::MakeScopeExit([rawCatAdmin, catInfoHdl]() -> void { + pCryptCATAdminReleaseCatalogContext(rawCatAdmin, catInfoHdl, 0); + }); + + // We found a catalog! Now query for the path to the catalog file. + + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::CryptCATCatalogInfoFromContext)> + pCryptCATCatalogInfoFromContext(L"wintrust.dll", + "CryptCATCatalogInfoFromContext"); + if (!pCryptCATCatalogInfoFromContext) { + return false; + } + + CATALOG_INFO_ catInfo = {sizeof(catInfo)}; + if (!pCryptCATCatalogInfoFromContext(catInfoHdl, &catInfo, 0)) { + return false; + } + + if (!QueryObject(catInfo.wszCatalogFile)) { + return false; + } + + mTrustSource = TrustSource::eCatalog; + + if (mFlags & mozilla::AuthenticodeFlags::SkipTrustVerification) { + return true; + } + + // WINTRUST_CATALOG_INFO::pcwszMemberTag is commonly set to the string + // representation of the file hash, so we build that here. + + DWORD strHashBufLen = (hashLen * 2) + 1; + auto strHashBuf = mozilla::MakeUnique<wchar_t[]>(strHashBufLen); + if (!::CryptBinaryToStringW(hashBuf.get(), hashLen, + CRYPT_STRING_HEXRAW | CRYPT_STRING_NOCRLF, + strHashBuf.get(), &strHashBufLen)) { + return false; + } + + // Ensure that the tag is uppercase for WinVerifyTrust + // NB: CryptBinaryToStringW overwrites strHashBufLen with the length excluding + // the null terminator, so we need to add it back for this call. + if (_wcsupr_s(strHashBuf.get(), strHashBufLen + 1)) { + return false; + } + + // Now, given the path to the catalog, and the path to the member (ie, the + // binary whose hash we are validating), we may now validate. If the + // validation is successful, we then QueryObject on the *catalog file* + // instead of the binary. + + WINTRUST_CATALOG_INFO wtCatInfo = {sizeof(wtCatInfo)}; + wtCatInfo.pcwszCatalogFilePath = catInfo.wszCatalogFile; + wtCatInfo.pcwszMemberTag = strHashBuf.get(); + wtCatInfo.pcwszMemberFilePath = aFilePath; + wtCatInfo.hMemberFile = rawFile; + if (mozilla::IsWin8OrLater()) { + wtCatInfo.hCatAdmin = rawCatAdmin; + } + + WINTRUST_DATA trustData = {sizeof(trustData)}; + trustData.dwUnionChoice = WTD_CHOICE_CATALOG; + trustData.pCatalog = &wtCatInfo; + + return VerifySignatureInternal(trustData); +} + +mozilla::UniquePtr<wchar_t[]> SignedBinary::GetOrgName() { + DWORD charCount = CertGetNameStringW( + mCertCtx.get(), CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, nullptr, nullptr, 0); + if (charCount <= 1) { + // Not found + return nullptr; + } + + auto result = mozilla::MakeUnique<wchar_t[]>(charCount); + charCount = CertGetNameStringW(mCertCtx.get(), CERT_NAME_SIMPLE_DISPLAY_TYPE, + 0, nullptr, result.get(), charCount); + MOZ_ASSERT(charCount > 1); + + return result; +} + +} // anonymous namespace + +namespace mozilla { + +class AuthenticodeImpl : public Authenticode { + public: + virtual UniquePtr<wchar_t[]> GetBinaryOrgName( + const wchar_t* aFilePath, + AuthenticodeFlags aFlags = AuthenticodeFlags::Default) override; +}; + +UniquePtr<wchar_t[]> AuthenticodeImpl::GetBinaryOrgName( + const wchar_t* aFilePath, AuthenticodeFlags aFlags) { + SignedBinary bin(aFilePath, aFlags); + if (!bin) { + return nullptr; + } + + return bin.GetOrgName(); +} + +static AuthenticodeImpl sAuthenticodeImpl; + +Authenticode* GetAuthenticode() { return &sAuthenticodeImpl; } + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/mozglue/Authenticode.h b/toolkit/xre/dllservices/mozglue/Authenticode.h new file mode 100644 index 0000000000..182512da2c --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/Authenticode.h @@ -0,0 +1,32 @@ +/* -*- 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/. */ + +#ifndef mozilla_Authenticode_h +#define mozilla_Authenticode_h + +#include "mozilla/Maybe.h" +#include "mozilla/TypedEnumBits.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { + +enum class AuthenticodeFlags : uint32_t { + Default = 0, + SkipTrustVerification = 1, +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(AuthenticodeFlags) + +class Authenticode { + public: + virtual UniquePtr<wchar_t[]> GetBinaryOrgName( + const wchar_t* aFilePath, + AuthenticodeFlags aFlags = AuthenticodeFlags::Default) = 0; +}; + +} // namespace mozilla + +#endif // mozilla_Authenticode_h diff --git a/toolkit/xre/dllservices/mozglue/CacheNtDllThunk.cpp b/toolkit/xre/dllservices/mozglue/CacheNtDllThunk.cpp new file mode 100644 index 0000000000..f3eef13504 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/CacheNtDllThunk.cpp @@ -0,0 +1,60 @@ +/* -*- 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 "CacheNtDllThunk.h" + +#include "mozilla/Maybe.h" +#include "mozilla/Span.h" +#include "mozilla/StaticPtr.h" + +namespace mozilla { + +static StaticAutoPtr<Buffer<IMAGE_THUNK_DATA>> sCachedNtDllThunk; + +// This static method initializes sCachedNtDllThunk. Because it's called in +// XREMain::XRE_main, which happens long before WindowsProcessLauncher's ctor +// accesses sCachedNtDllThunk, there is no race on sCachedNtDllThunk, thus +// no mutex is needed. +static void CacheNtDllThunk() { + if (sCachedNtDllThunk) { + return; + } + + do { + nt::PEHeaders ourExeImage(::GetModuleHandleW(nullptr)); + if (!ourExeImage) { + break; + } + + nt::PEHeaders ntdllImage(::GetModuleHandleW(L"ntdll.dll")); + if (!ntdllImage) { + break; + } + + Maybe<Range<const uint8_t>> ntdllBoundaries = ntdllImage.GetBounds(); + if (!ntdllBoundaries) { + break; + } + + Maybe<Span<IMAGE_THUNK_DATA>> maybeNtDllThunks = + ourExeImage.GetIATThunksForModule("ntdll.dll", ntdllBoundaries.ptr()); + if (maybeNtDllThunks.isNothing()) { + break; + } + + sCachedNtDllThunk = new Buffer<IMAGE_THUNK_DATA>(maybeNtDllThunks.value()); + return; + } while (false); + + // Failed to cache IAT. Initializing the variable with nullptr. + sCachedNtDllThunk = new Buffer<IMAGE_THUNK_DATA>(); +} + +static Buffer<IMAGE_THUNK_DATA>* GetCachedNtDllThunk() { + return sCachedNtDllThunk; +} + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/mozglue/CacheNtDllThunk.h b/toolkit/xre/dllservices/mozglue/CacheNtDllThunk.h new file mode 100644 index 0000000000..403e001afc --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/CacheNtDllThunk.h @@ -0,0 +1,21 @@ +/* -*- 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/. */ + +#ifndef mozilla_CacheNtDllThunk_h +#define mozilla_CacheNtDllThunk_h + +#include "mozilla/Types.h" +#include "mozilla/Buffer.h" +#include "mozilla/NativeNt.h" + +namespace mozilla { + +MFBT_API void CacheNtDllThunk(); +MFBT_API Buffer<IMAGE_THUNK_DATA>* GetCachedNtDllThunk(); + +}; // namespace mozilla + +#endif // mozilla_CacheNtDllThunk_h diff --git a/toolkit/xre/dllservices/mozglue/LoaderAPIInterfaces.h b/toolkit/xre/dllservices/mozglue/LoaderAPIInterfaces.h new file mode 100644 index 0000000000..67871b0c7a --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/LoaderAPIInterfaces.h @@ -0,0 +1,126 @@ +/* -*- 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/. */ + +#ifndef mozilla_LoaderAPIInterfaces_h +#define mozilla_LoaderAPIInterfaces_h + +#include "nscore.h" +#include "mozilla/glue/SharedSection.h" +#include "mozilla/ModuleLoadInfo.h" +#include "mozilla/ProcessType.h" + +namespace mozilla { +namespace nt { + +class NS_NO_VTABLE LoaderObserver { + public: + /** + * Notification that a DLL load has begun. + * + * @param aContext Outparam that allows this observer to store any context + * information pertaining to the current load. + * @param aRequestedDllName The DLL name requested by whatever invoked the + * loader. This name may not match the effective + * name of the DLL once the loader has completed + * its path search. + */ + virtual void OnBeginDllLoad(void** aContext, + PCUNICODE_STRING aRequestedDllName) = 0; + + /** + * Query the observer to determine whether the DLL named |aLSPLeafName| needs + * to be substituted with another module, and substitute the module handle + * when necessary. + * + * @return true when substitution occurs, otherwise false + */ + virtual bool SubstituteForLSP(PCUNICODE_STRING aLSPLeafName, + PHANDLE aOutHandle) = 0; + + /** + * Notification that a DLL load has ended. + * + * @param aContext The context that was set by the corresponding call to + * OnBeginDllLoad + * @param aNtStatus The NTSTATUS returned by LdrLoadDll + * @param aModuleLoadInfo Telemetry information that was gathered about the + * load. + */ + virtual void OnEndDllLoad(void* aContext, NTSTATUS aNtStatus, + ModuleLoadInfo&& aModuleLoadInfo) = 0; + + /** + * Called to inform the observer that it is no longer active and, if + * necessary, call aNext->OnForward() with any accumulated telemetry + * information. + */ + virtual void Forward(LoaderObserver* aNext) = 0; + + /** + * Receives a vector of module load telemetry from a previous LoaderObserver. + */ + virtual void OnForward(ModuleLoadInfoVec&& aInfo) = 0; +}; + +class NS_NO_VTABLE LoaderAPI { + public: + /** + * Construct a new ModuleLoadInfo structure and notify the LoaderObserver + * that a library load is beginning. + */ + virtual ModuleLoadInfo ConstructAndNotifyBeginDllLoad( + void** aContext, PCUNICODE_STRING aRequestedDllName) = 0; + + /** + * Query to determine whether the DLL named |aLSPLeafName| needs to be + * substituted with another module, and substitute the module handle when + * necessary. + * + * @return true when substitution occurs, otherwise false + */ + virtual bool SubstituteForLSP(PCUNICODE_STRING aLSPLeafName, + PHANDLE aOutHandle) = 0; + + /** + * Notification that a DLL load has ended. + */ + virtual void NotifyEndDllLoad(void* aContext, NTSTATUS aLoadNtStatus, + ModuleLoadInfo&& aModuleLoadInfo) = 0; + + /** + * Given the address of a mapped section, obtain the name of the file that is + * backing it. + */ + virtual AllocatedUnicodeString GetSectionName(void* aSectionAddr) = 0; + + using InitDllBlocklistOOPFnPtr = LauncherVoidResultWithLineInfo (*)( + const wchar_t*, HANDLE, const IMAGE_THUNK_DATA*, const GeckoProcessType); + using HandleLauncherErrorFnPtr = void (*)(const LauncherError&, const char*); + + /** + * Return a pointer to winlauncher's function. + * Used by sandboxBroker::LaunchApp. + */ + virtual InitDllBlocklistOOPFnPtr GetDllBlocklistInitFn() = 0; + virtual HandleLauncherErrorFnPtr GetHandleLauncherErrorFn() = 0; + virtual SharedSection* GetSharedSection() = 0; +}; + +struct WinLauncherServices final { + nt::LoaderAPI::InitDllBlocklistOOPFnPtr mInitDllBlocklistOOP; + nt::LoaderAPI::HandleLauncherErrorFnPtr mHandleLauncherError; + SharedSection* mSharedSection; + + WinLauncherServices() + : mInitDllBlocklistOOP(nullptr), + mHandleLauncherError(nullptr), + mSharedSection(nullptr) {} +}; + +} // namespace nt +} // namespace mozilla + +#endif // mozilla_LoaderAPIInterfaces_h diff --git a/toolkit/xre/dllservices/mozglue/LoaderObserver.cpp b/toolkit/xre/dllservices/mozglue/LoaderObserver.cpp new file mode 100644 index 0000000000..d2014365c4 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/LoaderObserver.cpp @@ -0,0 +1,149 @@ +/* -*- 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 "LoaderObserver.h" + +#include "mozilla/AutoProfilerLabel.h" +#include "mozilla/BaseProfilerMarkers.h" +#include "mozilla/glue/WindowsUnicode.h" +#include "mozilla/StackWalk_windows.h" +#include "mozilla/TimeStamp.h" + +namespace mozilla { + +extern glue::Win32SRWLock gDllServicesLock; +extern glue::detail::DllServicesBase* gDllServices; + +namespace glue { + +void LoaderObserver::OnBeginDllLoad(void** aContext, + PCUNICODE_STRING aRequestedDllName) { + MOZ_ASSERT(aContext); + if (IsProfilerPresent()) { + UniquePtr<char[]> utf8RequestedDllName(WideToUTF8(aRequestedDllName)); + BASE_PROFILER_MARKER_TEXT( + "DllLoad", OTHER, MarkerTiming::IntervalStart(), + mozilla::ProfilerString8View::WrapNullTerminatedString( + utf8RequestedDllName.get())); + *aContext = utf8RequestedDllName.release(); + } + +#ifdef _M_AMD64 + // Prevent the stack walker from suspending this thread when LdrLoadDll + // holds the RtlLookupFunctionEntry lock. + SuppressStackWalking(); +#endif +} + +bool LoaderObserver::SubstituteForLSP(PCUNICODE_STRING aLSPLeafName, + PHANDLE aOutHandle) { + // Currently unsupported + return false; +} + +void LoaderObserver::OnEndDllLoad(void* aContext, NTSTATUS aNtStatus, + ModuleLoadInfo&& aModuleLoadInfo) { +#ifdef _M_AMD64 + DesuppressStackWalking(); +#endif + + if (aContext) { + UniquePtr<char[]> utf8RequestedDllName{static_cast<char*>(aContext)}; + BASE_PROFILER_MARKER_TEXT( + "DllLoad", OTHER, MarkerTiming::IntervalEnd(), + mozilla::ProfilerString8View::WrapNullTerminatedString( + utf8RequestedDllName.get())); + } + + // We want to record a denied DLL load regardless of |aNtStatus| because + // |aNtStatus| is set to access-denied when DLL load was blocked. + if ((!NT_SUCCESS(aNtStatus) && !aModuleLoadInfo.WasDenied()) || + !aModuleLoadInfo.WasMapped()) { + return; + } + + { // Scope for lock + AutoSharedLock lock(gDllServicesLock); + if (gDllServices) { + gDllServices->DispatchDllLoadNotification(std::move(aModuleLoadInfo)); + return; + } + } + + // No dll services, save for later + AutoExclusiveLock lock(mLock); + if (!mEnabled) { + return; + } + + if (!mModuleLoads) { + mModuleLoads = new ModuleLoadInfoVec(); + } + + Unused << mModuleLoads->emplaceBack( + std::forward<ModuleLoadInfo>(aModuleLoadInfo)); +} + +void LoaderObserver::Forward(nt::LoaderObserver* aNext) { + MOZ_ASSERT_UNREACHABLE( + "This implementation does not forward to any more " + "nt::LoaderObserver objects"); +} + +void LoaderObserver::Forward(detail::DllServicesBase* aNext) { + MOZ_ASSERT(aNext); + if (!aNext) { + return; + } + + ModuleLoadInfoVec* moduleLoads = nullptr; + + { // Scope for lock + AutoExclusiveLock lock(mLock); + moduleLoads = mModuleLoads; + mModuleLoads = nullptr; + } + + if (!moduleLoads) { + return; + } + + aNext->DispatchModuleLoadBacklogNotification(std::move(*moduleLoads)); + delete moduleLoads; +} + +void LoaderObserver::Disable() { + ModuleLoadInfoVec* moduleLoads = nullptr; + + { // Scope for lock + AutoExclusiveLock lock(mLock); + moduleLoads = mModuleLoads; + mModuleLoads = nullptr; + mEnabled = false; + } + + delete moduleLoads; +} + +void LoaderObserver::OnForward(ModuleLoadInfoVec&& aInfo) { + AutoExclusiveLock lock(mLock); + if (!mModuleLoads) { + mModuleLoads = new ModuleLoadInfoVec(); + } + + MOZ_ASSERT(mModuleLoads->empty()); + if (mModuleLoads->empty()) { + *mModuleLoads = std::move(aInfo); + } else { + // This should not happen, but we can handle it + for (auto&& item : aInfo) { + Unused << mModuleLoads->append(std::move(item)); + } + } +} + +} // namespace glue +} // namespace mozilla diff --git a/toolkit/xre/dllservices/mozglue/LoaderObserver.h b/toolkit/xre/dllservices/mozglue/LoaderObserver.h new file mode 100644 index 0000000000..39b13035a3 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/LoaderObserver.h @@ -0,0 +1,45 @@ +/* -*- 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/. */ + +#ifndef mozilla_glue_LoaderObserver_h +#define mozilla_glue_LoaderObserver_h + +#include "mozilla/Attributes.h" +#include "mozilla/LoaderAPIInterfaces.h" +#include "mozilla/glue/WindowsDllServices.h" +#include "mozilla/glue/WinUtils.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { +namespace glue { + +class MOZ_ONLY_USED_TO_AVOID_STATIC_CONSTRUCTORS LoaderObserver final + : public nt::LoaderObserver { + public: + constexpr LoaderObserver() : mModuleLoads(nullptr), mEnabled(true) {} + + void OnBeginDllLoad(void** aContext, + PCUNICODE_STRING aPreliminaryDllName) final; + bool SubstituteForLSP(PCUNICODE_STRING aLspLeafName, + PHANDLE aOutHandle) final; + void OnEndDllLoad(void* aContext, NTSTATUS aNtStatus, + ModuleLoadInfo&& aModuleLoadInfo) final; + void Forward(nt::LoaderObserver* aNext) final; + void OnForward(ModuleLoadInfoVec&& aInfo) final; + + void Forward(mozilla::glue::detail::DllServicesBase* aSvc); + void Disable(); + + private: + Win32SRWLock mLock; + ModuleLoadInfoVec* mModuleLoads; + bool mEnabled; +}; + +} // namespace glue +} // namespace mozilla + +#endif // mozilla_glue_LoaderObserver_h diff --git a/toolkit/xre/dllservices/mozglue/ModuleLoadFrame.cpp b/toolkit/xre/dllservices/mozglue/ModuleLoadFrame.cpp new file mode 100644 index 0000000000..a49505adbc --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/ModuleLoadFrame.cpp @@ -0,0 +1,104 @@ +/* -*- 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 "ModuleLoadFrame.h" +#include "mozilla/NativeNt.h" +#include "mozilla/UniquePtr.h" +#include "NtLoaderAPI.h" + +#include <string.h> + +#include "WindowsFallbackLoaderAPI.h" + +static bool IsNullTerminated(PCUNICODE_STRING aStr) { + return aStr && (aStr->MaximumLength >= (aStr->Length + sizeof(WCHAR))) && + aStr->Buffer && aStr->Buffer[aStr->Length / sizeof(WCHAR)] == 0; +} + +static mozilla::FallbackLoaderAPI gFallbackLoaderAPI; + +namespace mozilla { +namespace glue { + +nt::LoaderAPI* ModuleLoadFrame::sLoaderAPI; + +using GetNtLoaderAPIFn = decltype(&mozilla::GetNtLoaderAPI); + +/* static */ +void ModuleLoadFrame::StaticInit(nt::LoaderObserver* aNewObserver, + nt::WinLauncherServices* aOutWinLauncher) { + const auto pGetNtLoaderAPI = reinterpret_cast<GetNtLoaderAPIFn>( + ::GetProcAddress(::GetModuleHandleW(nullptr), "GetNtLoaderAPI")); + if (!pGetNtLoaderAPI) { + // This case occurs in processes other than firefox.exe that do not contain + // the launcher process blocklist. + gFallbackLoaderAPI.SetObserver(aNewObserver); + sLoaderAPI = &gFallbackLoaderAPI; + + if (aOutWinLauncher) { + aOutWinLauncher->mHandleLauncherError = [](const mozilla::LauncherError&, + const char*) {}; + // We intentionally leave mInitDllBlocklistOOP null to make sure calling + // mInitDllBlocklistOOP in non-Firefox hits MOZ_RELEASE_ASSERT. + } + return; + } + + sLoaderAPI = pGetNtLoaderAPI(aNewObserver); + MOZ_ASSERT(sLoaderAPI); + + if (aOutWinLauncher) { + aOutWinLauncher->mInitDllBlocklistOOP = sLoaderAPI->GetDllBlocklistInitFn(); + aOutWinLauncher->mHandleLauncherError = + sLoaderAPI->GetHandleLauncherErrorFn(); + aOutWinLauncher->mSharedSection = sLoaderAPI->GetSharedSection(); + } +} + +ModuleLoadFrame::ModuleLoadFrame(PCUNICODE_STRING aRequestedDllName) + : mAlreadyLoaded(false), + mContext(nullptr), + mDllLoadStatus(STATUS_UNSUCCESSFUL), + mLoadInfo(sLoaderAPI->ConstructAndNotifyBeginDllLoad(&mContext, + aRequestedDllName)) { + if (!aRequestedDllName) { + return; + } + + UniquePtr<WCHAR[]> nameBuf; + const WCHAR* name = nullptr; + + if (IsNullTerminated(aRequestedDllName)) { + name = aRequestedDllName->Buffer; + } else { + USHORT charLenExclNul = aRequestedDllName->Length / sizeof(WCHAR); + USHORT charLenInclNul = charLenExclNul + 1; + nameBuf = MakeUnique<WCHAR[]>(charLenInclNul); + if (!wcsncpy_s(nameBuf.get(), charLenInclNul, aRequestedDllName->Buffer, + charLenExclNul)) { + name = nameBuf.get(); + } + } + + mAlreadyLoaded = name && !!::GetModuleHandleW(name); +} + +ModuleLoadFrame::~ModuleLoadFrame() { + sLoaderAPI->NotifyEndDllLoad(mContext, mDllLoadStatus, std::move(mLoadInfo)); +} + +void ModuleLoadFrame::SetLoadStatus(NTSTATUS aNtStatus, HANDLE aHandle) { + mDllLoadStatus = aNtStatus; + void* baseAddr = mozilla::nt::PEHeaders::HModuleToBaseAddr<void*>( + reinterpret_cast<HMODULE>(aHandle)); + mLoadInfo.mBaseAddr = baseAddr; + if (!mAlreadyLoaded) { + mLoadInfo.mSectionName = sLoaderAPI->GetSectionName(baseAddr); + } +} + +} // namespace glue +} // namespace mozilla diff --git a/toolkit/xre/dllservices/mozglue/ModuleLoadFrame.h b/toolkit/xre/dllservices/mozglue/ModuleLoadFrame.h new file mode 100644 index 0000000000..d47e42ab0c --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/ModuleLoadFrame.h @@ -0,0 +1,44 @@ +/* -*- 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/. */ + +#ifndef mozilla_glue_ModuleLoadFrame_h +#define mozilla_glue_ModuleLoadFrame_h + +#include "mozilla/Attributes.h" +#include "mozilla/LoaderAPIInterfaces.h" + +namespace mozilla { +namespace glue { + +class MOZ_RAII ModuleLoadFrame final { + public: + explicit ModuleLoadFrame(PCUNICODE_STRING aRequestedDllName); + ~ModuleLoadFrame(); + + void SetLoadStatus(NTSTATUS aNtStatus, HANDLE aHandle); + + ModuleLoadFrame(const ModuleLoadFrame&) = delete; + ModuleLoadFrame(ModuleLoadFrame&&) = delete; + ModuleLoadFrame& operator=(const ModuleLoadFrame&) = delete; + ModuleLoadFrame& operator=(ModuleLoadFrame&&) = delete; + + static void StaticInit(nt::LoaderObserver* aNewObserver, + nt::WinLauncherServices* aOutWinLauncher); + + private: + bool mAlreadyLoaded; + void* mContext; + NTSTATUS mDllLoadStatus; + ModuleLoadInfo mLoadInfo; + + private: + static nt::LoaderAPI* sLoaderAPI; +}; + +} // namespace glue +} // namespace mozilla + +#endif // mozilla_glue_ModuleLoadFrame_h diff --git a/toolkit/xre/dllservices/mozglue/ModuleLoadInfo.h b/toolkit/xre/dllservices/mozglue/ModuleLoadInfo.h new file mode 100644 index 0000000000..683586a5b0 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/ModuleLoadInfo.h @@ -0,0 +1,179 @@ +/* -*- 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/. */ + +#ifndef mozilla_ModuleLoadInfo_h +#define mozilla_ModuleLoadInfo_h + +#include "mozilla/NativeNt.h" +#include "mozilla/Vector.h" +#include "mozilla/Unused.h" + +namespace mozilla { + +struct ModuleLoadInfo final { + enum class Status : uint32_t { + Loaded = 0, + Blocked, + Redirected, + }; + + // We do not provide these methods inside Gecko proper. +#if !defined(MOZILLA_INTERNAL_API) + + /** + * This constructor is for use by the LdrLoadDll hook. + */ + explicit ModuleLoadInfo(PCUNICODE_STRING aRequestedDllName) + : mLoadTimeInfo(), + mThreadId(nt::RtlGetCurrentThreadId()), + mRequestedDllName(aRequestedDllName), + mBaseAddr(nullptr), + mStatus(Status::Loaded), + mIsDependent(false) { +# if defined(IMPL_MFBT) + ::QueryPerformanceCounter(&mBeginTimestamp); +# else + ::RtlQueryPerformanceCounter(&mBeginTimestamp); +# endif // defined(IMPL_MFBT) + } + + /** + * This constructor is used by the NtMapViewOfSection hook IF AND ONLY IF + * the LdrLoadDll hook did not already construct a ModuleLoadInfo for the + * current DLL load. This may occur while the loader is loading dependencies + * of another library. + */ + ModuleLoadInfo(nt::AllocatedUnicodeString&& aSectionName, + const void* aBaseAddr, Status aLoadStatus, bool aIsDependent) + : mLoadTimeInfo(), + mThreadId(nt::RtlGetCurrentThreadId()), + mSectionName(std::move(aSectionName)), + mBaseAddr(aBaseAddr), + mStatus(aLoadStatus), + mIsDependent(aIsDependent) { +# if defined(IMPL_MFBT) + ::QueryPerformanceCounter(&mBeginTimestamp); +# else + ::RtlQueryPerformanceCounter(&mBeginTimestamp); +# endif // defined(IMPL_MFBT) + } + + /** + * Marks the time that LdrLoadDll began loading this library. + */ + void SetBeginLoadTimeStamp() { +# if defined(IMPL_MFBT) + ::QueryPerformanceCounter(&mLoadTimeInfo); +# else + ::RtlQueryPerformanceCounter(&mLoadTimeInfo); +# endif // defined(IMPL_MFBT) + } + + /** + * Marks the time that LdrLoadDll finished loading this library. + */ + void SetEndLoadTimeStamp() { + LARGE_INTEGER endTimeStamp; +# if defined(IMPL_MFBT) + ::QueryPerformanceCounter(&endTimeStamp); +# else + ::RtlQueryPerformanceCounter(&endTimeStamp); +# endif // defined(IMPL_MFBT) + + LONGLONG& timeInfo = mLoadTimeInfo.QuadPart; + if (!timeInfo) { + return; + } + + timeInfo = endTimeStamp.QuadPart - timeInfo; + } + + /** + * Saves the current thread's call stack. + */ + void CaptureBacktrace() { + const DWORD kMaxBacktraceSize = 512; + + if (!mBacktrace.resize(kMaxBacktraceSize)) { + return; + } + + // We don't use a Win32 variant here because Win32's CaptureStackBackTrace + // is just a macro that resolve to this function anyway. + WORD numCaptured = ::RtlCaptureStackBackTrace(2, kMaxBacktraceSize, + mBacktrace.begin(), nullptr); + Unused << mBacktrace.resize(numCaptured); + // These backtraces might stick around for a while, so let's trim any + // excess memory. + mBacktrace.shrinkStorageToFit(); + } + +#endif // !defined(MOZILLA_INTERNAL_API) + + ModuleLoadInfo(ModuleLoadInfo&&) = default; + ModuleLoadInfo& operator=(ModuleLoadInfo&&) = default; + + ModuleLoadInfo() = delete; + ModuleLoadInfo(const ModuleLoadInfo&) = delete; + ModuleLoadInfo& operator=(const ModuleLoadInfo&) = delete; + + /** + * A "bare" module load is one that was mapped without the code passing + * through a call to ntdll!LdrLoadDll. + */ + bool IsBare() const { + // SetBeginLoadTimeStamp() and SetEndLoadTimeStamp() are only called by the + // LdrLoadDll hook, so when mLoadTimeInfo == 0, we know that we are bare. + return !mLoadTimeInfo.QuadPart; + } + + /** + * Returns true for DLL loads where LdrLoadDll was called but + * NtMapViewOfSection was not. This will happen for DLL requests where the DLL + * was already mapped into memory by a previous request. + */ + bool WasMapped() const { return !mSectionName.IsEmpty(); } + + /** + * Returns true for DLL load which was denied by our blocklist. + */ + bool WasDenied() const { + return mStatus == ModuleLoadInfo::Status::Blocked || + mStatus == ModuleLoadInfo::Status::Redirected; + } + + /** + * Returns true for a DLL load which was blocked by our blocklist. + */ + bool WasBlocked() const { return mStatus == ModuleLoadInfo::Status::Blocked; } + + // Timestamp for the creation of this event + LARGE_INTEGER mBeginTimestamp; + // Duration of the LdrLoadDll call + LARGE_INTEGER mLoadTimeInfo; + // Thread ID of this DLL load + DWORD mThreadId; + // The name requested of LdrLoadDll by its caller + nt::AllocatedUnicodeString mRequestedDllName; + // The name of the DLL that backs section that was mapped by the loader. This + // string is the effective name of the DLL that was resolved by the loader's + // path search algorithm. + nt::AllocatedUnicodeString mSectionName; + // The base address of the module's mapped section + const void* mBaseAddr; + // If the module was successfully loaded, stack trace of the DLL load request + Vector<PVOID, 0, nt::RtlAllocPolicy> mBacktrace; + // The status of DLL load + Status mStatus; + // Whether the module is one of the executables's dependent modules or not + bool mIsDependent; +}; + +using ModuleLoadInfoVec = Vector<ModuleLoadInfo, 0, nt::RtlAllocPolicy>; + +} // namespace mozilla + +#endif // mozilla_ModuleLoadInfo_h diff --git a/toolkit/xre/dllservices/mozglue/NtLoaderAPI.h b/toolkit/xre/dllservices/mozglue/NtLoaderAPI.h new file mode 100644 index 0000000000..628609092b --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/NtLoaderAPI.h @@ -0,0 +1,23 @@ +/* -*- 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/. */ + +#ifndef mozilla_NtLoaderAPI_h +#define mozilla_NtLoaderAPI_h + +#include "mozilla/LoaderAPIInterfaces.h" + +#if !defined(IMPL_MFBT) +# error "This should only be included from mozglue!" +#endif // !defined(IMPL_MFBT) + +namespace mozilla { + +extern "C" MOZ_IMPORT_API nt::LoaderAPI* GetNtLoaderAPI( + nt::LoaderObserver* aNewObserver); + +} // namespace mozilla + +#endif // mozilla_NtLoaderAPI_h diff --git a/toolkit/xre/dllservices/mozglue/SharedSection.h b/toolkit/xre/dllservices/mozglue/SharedSection.h new file mode 100644 index 0000000000..162c8e8343 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/SharedSection.h @@ -0,0 +1,28 @@ +/* -*- 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/. */ + +#ifndef mozilla_glue_SharedSection_h +#define mozilla_glue_SharedSection_h + +#include <winternl.h> +#include "nscore.h" +#include "mozilla/Span.h" +#include "mozilla/WindowsDllBlocklistInfo.h" + +namespace mozilla::nt { + +// This interface provides a way to access winlauncher's shared section +// through DllServices. +struct NS_NO_VTABLE SharedSection { + virtual Span<const wchar_t> GetDependentModules() = 0; + // Returns a span of the whole space for dynamic blocklist entries. + // Use IsValidDynamicBlocklistEntry() to determine the end of the list. + virtual Span<const DllBlockInfoT<UNICODE_STRING>> GetDynamicBlocklist() = 0; +}; + +} // namespace mozilla::nt + +#endif // mozilla_glue_SharedSection_h diff --git a/toolkit/xre/dllservices/mozglue/WindowsBCryptInitialization.cpp b/toolkit/xre/dllservices/mozglue/WindowsBCryptInitialization.cpp new file mode 100644 index 0000000000..e643a096ab --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/WindowsBCryptInitialization.cpp @@ -0,0 +1,24 @@ +/* -*- 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 "mozilla/WindowsBCryptInitialization.h" + +#include "mozilla/RandomNum.h" +#include "nsWindowsDllInterceptor.h" + +#include <bcrypt.h> +#pragma comment(lib, "bcrypt.lib") + +namespace mozilla { + +bool WindowsBCryptInitialization() { + UCHAR buffer[32]; + NTSTATUS status = ::BCryptGenRandom(nullptr, buffer, sizeof(buffer), + BCRYPT_USE_SYSTEM_PREFERRED_RNG); + return NT_SUCCESS(status); +} + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/mozglue/WindowsBCryptInitialization.h b/toolkit/xre/dllservices/mozglue/WindowsBCryptInitialization.h new file mode 100644 index 0000000000..b67e5016fb --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/WindowsBCryptInitialization.h @@ -0,0 +1,24 @@ +/* -*- 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/. */ + +#ifndef mozilla_WindowsBCryptInitialization_h +#define mozilla_WindowsBCryptInitialization_h + +#include "mozilla/Types.h" + +namespace mozilla { + +// This functions ensures that calling BCryptGenRandom will work later. It +// triggers a first call to BCryptGenRandom() to pre-load bcryptPrimitives.dll. +// In sandboxed processes, this must happen while the current thread still has +// an unrestricted impersonation token. We need to perform that operation to +// warmup the BCryptGenRandom() calls is used by others, especially Rust. See +// bug 1746524, bug 1751094, bug 1751177, bug 1788004. +MFBT_API bool WindowsBCryptInitialization(); + +} // namespace mozilla + +#endif // mozilla_WindowsBCryptInitialization_h diff --git a/toolkit/xre/dllservices/mozglue/WindowsDllBlocklist.cpp b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklist.cpp new file mode 100644 index 0000000000..5ac3af4559 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklist.cpp @@ -0,0 +1,811 @@ +/* -*- Mode: C++; tab-width: 40; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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> + +#pragma warning(push) +#pragma warning(disable : 4275 4530) // See msvc-stl-wrapper.template.h +#include <map> +#pragma warning(pop) + +#include "Authenticode.h" +#include "BaseProfiler.h" +#include "nsWindowsDllInterceptor.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/StackWalk_windows.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Vector.h" +#include "mozilla/WindowsProcessMitigations.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "nsWindowsHelpers.h" +#include "WindowsDllBlocklist.h" +#include "mozilla/AutoProfilerLabel.h" +#include "mozilla/glue/Debug.h" +#include "mozilla/glue/WindowsDllServices.h" +#include "mozilla/glue/WinUtils.h" + +// Start new implementation +#include "LoaderObserver.h" +#include "ModuleLoadFrame.h" +#include "mozilla/glue/WindowsUnicode.h" + +namespace mozilla { + +glue::Win32SRWLock gDllServicesLock; +glue::detail::DllServicesBase* gDllServices; + +} // namespace mozilla + +using namespace mozilla; + +using CrashReporter::Annotation; +using CrashReporter::AnnotationWriter; + +#define DLL_BLOCKLIST_ENTRY(name, ...) {name, __VA_ARGS__}, +#define DLL_BLOCKLIST_STRING_TYPE const char* +#include "mozilla/WindowsDllBlocklistLegacyDefs.h" + +// define this for very verbose dll load debug spew +#undef DEBUG_very_verbose + +static uint32_t sInitFlags; +static bool sBlocklistInitAttempted; +static bool sBlocklistInitFailed; +static bool sUser32BeforeBlocklist; + +typedef MOZ_NORETURN_PTR void(__fastcall* BaseThreadInitThunk_func)( + BOOL aIsInitialThread, void* aStartAddress, void* aThreadParam); +static WindowsDllInterceptor::FuncHookType<BaseThreadInitThunk_func> + stub_BaseThreadInitThunk; + +typedef NTSTATUS(NTAPI* LdrLoadDll_func)(PWCHAR filePath, PULONG flags, + PUNICODE_STRING moduleFileName, + PHANDLE handle); +static WindowsDllInterceptor::FuncHookType<LdrLoadDll_func> stub_LdrLoadDll; + +#ifdef _M_AMD64 +typedef decltype(RtlInstallFunctionTableCallback)* + RtlInstallFunctionTableCallback_func; +static WindowsDllInterceptor::FuncHookType<RtlInstallFunctionTableCallback_func> + stub_RtlInstallFunctionTableCallback; + +extern uint8_t* sMsMpegJitCodeRegionStart; +extern size_t sMsMpegJitCodeRegionSize; + +BOOLEAN WINAPI patched_RtlInstallFunctionTableCallback( + DWORD64 TableIdentifier, DWORD64 BaseAddress, DWORD Length, + PGET_RUNTIME_FUNCTION_CALLBACK Callback, PVOID Context, + PCWSTR OutOfProcessCallbackDll) { + // msmpeg2vdec.dll sets up a function table callback for their JIT code that + // just terminates the process, because their JIT doesn't have unwind info. + // If we see this callback being registered, record the region address, so + // that StackWalk.cpp can avoid unwinding addresses in this region. + // + // To keep things simple I'm not tracking unloads of msmpeg2vdec.dll. + // Worst case the stack walker will needlessly avoid a few pages of memory. + + // Tricky: GetModuleHandleExW adds a ref by default; GetModuleHandleW doesn't. + HMODULE callbackModule = nullptr; + DWORD moduleFlags = GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT; + + // These GetModuleHandle calls enter a critical section on Win7. + AutoSuppressStackWalking suppress; + + if (GetModuleHandleExW(moduleFlags, (LPWSTR)Callback, &callbackModule) && + GetModuleHandleW(L"msmpeg2vdec.dll") == callbackModule) { + sMsMpegJitCodeRegionStart = (uint8_t*)BaseAddress; + sMsMpegJitCodeRegionSize = Length; + } + + return stub_RtlInstallFunctionTableCallback(TableIdentifier, BaseAddress, + Length, Callback, Context, + OutOfProcessCallbackDll); +} +#endif + +template <class T> +struct RVAMap { + RVAMap(HANDLE map, DWORD offset) { + SYSTEM_INFO info; + GetSystemInfo(&info); + + DWORD alignedOffset = + (offset / info.dwAllocationGranularity) * info.dwAllocationGranularity; + + MOZ_ASSERT(offset - alignedOffset < info.dwAllocationGranularity, "Wtf"); + + mRealView = ::MapViewOfFile(map, FILE_MAP_READ, 0, alignedOffset, + sizeof(T) + (offset - alignedOffset)); + + mMappedView = + mRealView + ? reinterpret_cast<T*>((char*)mRealView + (offset - alignedOffset)) + : nullptr; + } + ~RVAMap() { + if (mRealView) { + ::UnmapViewOfFile(mRealView); + } + } + operator const T*() const { return mMappedView; } + const T* operator->() const { return mMappedView; } + + private: + const T* mMappedView; + void* mRealView; +}; + +static DWORD GetTimestamp(const wchar_t* path) { + DWORD timestamp = 0; + + HANDLE file = ::CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file != INVALID_HANDLE_VALUE) { + HANDLE map = + ::CreateFileMappingW(file, nullptr, PAGE_READONLY, 0, 0, nullptr); + if (map) { + RVAMap<IMAGE_DOS_HEADER> peHeader(map, 0); + if (peHeader) { + RVAMap<IMAGE_NT_HEADERS> ntHeader(map, peHeader->e_lfanew); + if (ntHeader) { + timestamp = ntHeader->FileHeader.TimeDateStamp; + } + } + ::CloseHandle(map); + } + ::CloseHandle(file); + } + + return timestamp; +} + +// This lock protects both the reentrancy sentinel and the crash reporter +// data structures. +static CRITICAL_SECTION sLock; + +/** + * Some versions of Windows call LoadLibraryEx to get the version information + * for a DLL, which causes our patched LdrLoadDll implementation to re-enter + * itself and cause infinite recursion and a stack-exhaustion crash. We protect + * against reentrancy by allowing recursive loads of the same DLL. + * + * Note that we don't use __declspec(thread) because that doesn't work in DLLs + * loaded via LoadLibrary and there can be a limited number of TLS slots, so + * we roll our own. + */ +class ReentrancySentinel { + public: + explicit ReentrancySentinel(const char* dllName) { + DWORD currentThreadId = GetCurrentThreadId(); + AutoCriticalSection lock(&sLock); + mPreviousDllName = (*sThreadMap)[currentThreadId]; + + // If there is a DLL currently being loaded and it has the same name + // as the current attempt, we're re-entering. + mReentered = mPreviousDllName && !stricmp(mPreviousDllName, dllName); + (*sThreadMap)[currentThreadId] = dllName; + } + + ~ReentrancySentinel() { + DWORD currentThreadId = GetCurrentThreadId(); + AutoCriticalSection lock(&sLock); + (*sThreadMap)[currentThreadId] = mPreviousDllName; + } + + bool BailOut() const { return mReentered; }; + + static void InitializeStatics() { + InitializeCriticalSection(&sLock); + sThreadMap = new std::map<DWORD, const char*>; + } + + private: + static std::map<DWORD, const char*>* sThreadMap; + + const char* mPreviousDllName; + bool mReentered; +}; + +std::map<DWORD, const char*>* ReentrancySentinel::sThreadMap; + +using WritableBuffer = mozilla::glue::detail::WritableBuffer<1024>; + +/** + * This is a linked list of DLLs that have been blocked. It doesn't use + * mozilla::LinkedList because this is an append-only list and doesn't need + * to be doubly linked. + */ +class DllBlockSet { + public: + static void Add(const char* name, unsigned long long version); + + // Write the list of blocked DLLs to a WritableBuffer object. This method is + // run after a crash occurs and must therefore not use the heap, etc. + static void Write(WritableBuffer& buffer); + + private: + DllBlockSet(const char* name, unsigned long long version) + : mName(name), mVersion(version), mNext(nullptr) {} + + const char* mName; // points into the gWindowsDllBlocklist string + unsigned long long mVersion; + DllBlockSet* mNext; + + static DllBlockSet* gFirst; +}; + +DllBlockSet* DllBlockSet::gFirst; + +void DllBlockSet::Add(const char* name, unsigned long long version) { + AutoCriticalSection lock(&sLock); + for (DllBlockSet* b = gFirst; b; b = b->mNext) { + if (0 == strcmp(b->mName, name) && b->mVersion == version) { + return; + } + } + // Not already present + DllBlockSet* n = new DllBlockSet(name, version); + n->mNext = gFirst; + gFirst = n; +} + +void DllBlockSet::Write(WritableBuffer& buffer) { + // It would be nicer to use AutoCriticalSection here. However, its destructor + // might not run if an exception occurs, in which case we would never leave + // the critical section. (MSVC warns about this possibility.) So we + // enter and leave manually. + ::EnterCriticalSection(&sLock); + + // Because this method is called after a crash occurs, and uses heap memory, + // protect this entire block with a structured exception handler. + MOZ_SEH_TRY { + for (DllBlockSet* b = gFirst; b; b = b->mNext) { + // write name[,v.v.v.v]; + buffer.Write(b->mName, strlen(b->mName)); + if (b->mVersion != DllBlockInfo::ALL_VERSIONS) { + buffer.Write(",", 1); + uint16_t parts[4]; + parts[0] = b->mVersion >> 48; + parts[1] = (b->mVersion >> 32) & 0xFFFF; + parts[2] = (b->mVersion >> 16) & 0xFFFF; + parts[3] = b->mVersion & 0xFFFF; + for (int p = 0; p < 4; ++p) { + char buf[32]; + _ltoa_s(parts[p], buf, sizeof(buf), 10); + buffer.Write(buf, strlen(buf)); + if (p != 3) { + buffer.Write(".", 1); + } + } + } + buffer.Write(";", 1); + } + } + MOZ_SEH_EXCEPT(EXCEPTION_EXECUTE_HANDLER) {} + + ::LeaveCriticalSection(&sLock); +} + +static UniquePtr<wchar_t[]> getFullPath(PWCHAR filePath, wchar_t* fname) { + // In Windows 8, the first parameter seems to be used for more than just the + // path name. For example, its numerical value can be 1. Passing a non-valid + // pointer to SearchPathW will cause a crash, so we need to check to see if we + // are handed a valid pointer, and otherwise just pass nullptr to SearchPathW. + PWCHAR sanitizedFilePath = nullptr; + if ((uintptr_t(filePath) >= 65536) && ((uintptr_t(filePath) & 1) == 0)) { + sanitizedFilePath = filePath; + } + + // figure out the length of the string that we need + DWORD pathlen = + SearchPathW(sanitizedFilePath, fname, L".dll", 0, nullptr, nullptr); + if (pathlen == 0) { + return nullptr; + } + + auto full_fname = MakeUnique<wchar_t[]>(pathlen + 1); + if (!full_fname) { + // couldn't allocate memory? + return nullptr; + } + + // now actually grab it + SearchPathW(sanitizedFilePath, fname, L".dll", pathlen + 1, full_fname.get(), + nullptr); + return full_fname; +} + +// No builtin function to find the last character matching a set +static wchar_t* lastslash(wchar_t* s, int len) { + for (wchar_t* c = s + len - 1; c >= s; --c) { + if (*c == L'\\' || *c == L'/') { + return c; + } + } + return nullptr; +} + +static bool ShouldBlockBasedOnBlockInfo(const DllBlockInfo& info, + const char* dllName, PWCHAR filePath, + wchar_t* fname, + unsigned long long* fVersion) { +#ifdef DEBUG_very_verbose + printf_stderr("LdrLoadDll: info->mName: '%s'\n", info->mName); +#endif + + if (info.mFlags & DllBlockInfoFlags::REDIRECT_TO_NOOP_ENTRYPOINT) { + printf_stderr( + "LdrLoadDll: " + "Ignoring the REDIRECT_TO_NOOP_ENTRYPOINT flag\n"); + } + + if ((info.mFlags & DllBlockInfoFlags::BLOCK_WIN8_AND_OLDER) && + IsWin8Point1OrLater()) { + return false; + } + + if ((info.mFlags & DllBlockInfoFlags::BLOCK_WIN7_AND_OLDER) && + IsWin8OrLater()) { + return false; + } + + if ((info.mFlags & DllBlockInfoFlags::CHILD_PROCESSES_ONLY) && + !(sInitFlags & eDllBlocklistInitFlagIsChildProcess)) { + return false; + } + + if ((info.mFlags & DllBlockInfoFlags::UTILITY_PROCESSES_ONLY) && + !(sInitFlags & eDllBlocklistInitFlagIsUtilityProcess)) { + return false; + } + + if ((info.mFlags & DllBlockInfoFlags::SOCKET_PROCESSES_ONLY) && + !(sInitFlags & eDllBlocklistInitFlagIsSocketProcess)) { + return false; + } + + if ((info.mFlags & DllBlockInfoFlags::GPU_PROCESSES_ONLY) && + !(sInitFlags & eDllBlocklistInitFlagIsGPUProcess)) { + return false; + } + + if ((info.mFlags & DllBlockInfoFlags::BROWSER_PROCESS_ONLY) && + (sInitFlags & eDllBlocklistInitFlagIsChildProcess)) { + return false; + } + + if ((info.mFlags & DllBlockInfoFlags::GMPLUGIN_PROCESSES_ONLY) && + !(sInitFlags & eDllBlocklistInitFlagIsGMPluginProcess)) { + return false; + } + + *fVersion = DllBlockInfo::ALL_VERSIONS; + + if (info.mMaxVersion != DllBlockInfo::ALL_VERSIONS) { + ReentrancySentinel sentinel(dllName); + if (sentinel.BailOut()) { + return false; + } + + UniquePtr<wchar_t[]> full_fname = getFullPath(filePath, fname); + if (!full_fname) { + // uh, we couldn't find the DLL at all, so... + printf_stderr( + "LdrLoadDll: Blocking load of '%s' (SearchPathW didn't find " + "it?)\n", + dllName); + return true; + } + + if (info.mFlags & DllBlockInfoFlags::USE_TIMESTAMP) { + *fVersion = GetTimestamp(full_fname.get()); + if (*fVersion > info.mMaxVersion) { + return false; + } + } else { + LauncherResult<ModuleVersion> version = + GetModuleVersion(full_fname.get()); + // If we failed to get the version information, we block. + if (version.isOk()) { + return info.IsVersionBlocked(version.unwrap()); + } + } + } + // Falling through to here means we should block. + return true; +} + +struct CaseSensitiveStringComparator { + explicit CaseSensitiveStringComparator(const char* aTarget) + : mTarget(aTarget) {} + + int operator()(const DllBlockInfo& aVal) const { + return strcmp(mTarget, aVal.mName); + } + + const char* mTarget; +}; + +static NTSTATUS NTAPI patched_LdrLoadDll(PWCHAR filePath, PULONG flags, + PUNICODE_STRING moduleFileName, + PHANDLE handle) { + // We have UCS2 (UTF16?), we want ASCII, but we also just want the filename + // portion +#define DLLNAME_MAX 128 + char dllName[DLLNAME_MAX + 1]; + wchar_t* dll_part; + char* dot; + + int len = moduleFileName->Length / 2; + wchar_t* fname = moduleFileName->Buffer; + + // The filename isn't guaranteed to be null terminated, but in practice + // it always will be; ensure that this is so, and bail if not. + // This is done instead of the more robust approach because of bug 527122, + // where lots of weird things were happening when we tried to make a copy. + if (moduleFileName->MaximumLength < moduleFileName->Length + 2 || + fname[len] != 0) { +#ifdef DEBUG + printf_stderr("LdrLoadDll: non-null terminated string found!\n"); +#endif + goto continue_loading; + } + + dll_part = lastslash(fname, len); + if (dll_part) { + dll_part = dll_part + 1; + len -= dll_part - fname; + } else { + dll_part = fname; + } + +#ifdef DEBUG_very_verbose + printf_stderr("LdrLoadDll: dll_part '%S' %d\n", dll_part, len); +#endif + + // if it's too long, then, we assume we won't want to block it, + // since DLLNAME_MAX should be at least long enough to hold the longest + // entry in our blocklist. + if (len > DLLNAME_MAX) { +#ifdef DEBUG + printf_stderr("LdrLoadDll: len too long! %d\n", len); +#endif + goto continue_loading; + } + + // copy over to our char byte buffer, lowercasing ASCII as we go + for (int i = 0; i < len; i++) { + wchar_t c = dll_part[i]; + + if (c > 0x7f) { + // welp, it's not ascii; if we need to add non-ascii things to + // our blocklist, we'll have to remove this limitation. + goto continue_loading; + } + + // ensure that dll name is all lowercase + if (c >= 'A' && c <= 'Z') c += 'a' - 'A'; + + dllName[i] = (char)c; + } + + dllName[len] = 0; + +#ifdef DEBUG_very_verbose + printf_stderr("LdrLoadDll: dll name '%s'\n", dllName); +#endif + + if (!(sInitFlags & eDllBlocklistInitFlagWasBootstrapped)) { + // Block a suspicious binary that uses various 12-digit hex strings + // e.g. MovieMode.48CA2AEFA22D.dll (bug 973138) + dot = strchr(dllName, '.'); + if (dot && (strchr(dot + 1, '.') == dot + 13)) { + char* end = nullptr; + _strtoui64(dot + 1, &end, 16); + if (end == dot + 13) { + return STATUS_DLL_NOT_FOUND; + } + } + // Block binaries where the filename is at least 16 hex digits + if (dot && ((dot - dllName) >= 16)) { + char* current = dllName; + while (current < dot && isxdigit(*current)) { + current++; + } + if (current == dot) { + return STATUS_DLL_NOT_FOUND; + } + } + + // then compare to everything on the blocklist + DECLARE_POINTER_TO_FIRST_DLL_BLOCKLIST_ENTRY(info); + DECLARE_DLL_BLOCKLIST_NUM_ENTRIES(infoNumEntries); + CaseSensitiveStringComparator comp(dllName); + size_t match = LowerBound(info, 0, infoNumEntries, comp); + if (match != infoNumEntries) { + // There may be multiple entries on the list. Since LowerBound() returns + // the first entry that matches (if there are any matches), + // search forward from there. + while (match < infoNumEntries && (comp(info[match]) == 0)) { + unsigned long long fVersion; + if (ShouldBlockBasedOnBlockInfo(info[match], dllName, filePath, fname, + &fVersion)) { + printf_stderr( + "LdrLoadDll: Blocking load of '%s' -- see " + "http://www.mozilla.com/en-US/blocklist/\n", + dllName); + DllBlockSet::Add(info[match].mName, fVersion); + return STATUS_DLL_NOT_FOUND; + } + ++match; + } + } + } + +continue_loading: +#ifdef DEBUG_very_verbose + printf_stderr("LdrLoadDll: continuing load... ('%S')\n", + moduleFileName->Buffer); +#endif + + glue::ModuleLoadFrame loadFrame(moduleFileName); + + NTSTATUS ret; + HANDLE myHandle; + + { +#if defined(_M_AMD64) || defined(_M_ARM64) + AutoSuppressStackWalking suppress; +#endif + ret = stub_LdrLoadDll(filePath, flags, moduleFileName, &myHandle); + } + + if (handle) { + *handle = myHandle; + } + + loadFrame.SetLoadStatus(ret, myHandle); + + return ret; +} + +#if defined(NIGHTLY_BUILD) +// Map of specific thread proc addresses we should block. In particular, +// LoadLibrary* APIs which indicate DLL injection +static void* gStartAddressesToBlock[4]; +#endif // defined(NIGHTLY_BUILD) + +static bool ShouldBlockThread(void* aStartAddress) { + // Allows crashfirefox.exe to continue to work. Also if your threadproc is + // null, this crash is intentional. + if (aStartAddress == nullptr) { + return false; + } + +#if defined(NIGHTLY_BUILD) + for (auto p : gStartAddressesToBlock) { + if (p == aStartAddress) { + return true; + } + } +#endif + + bool shouldBlock = false; + MEMORY_BASIC_INFORMATION startAddressInfo = {0}; + if (VirtualQuery(aStartAddress, &startAddressInfo, + sizeof(startAddressInfo))) { + shouldBlock |= startAddressInfo.State != MEM_COMMIT; + shouldBlock |= startAddressInfo.Protect != PAGE_EXECUTE_READ; + } + + return shouldBlock; +} + +// Allows blocked threads to still run normally through BaseThreadInitThunk, in +// case there's any magic there that we shouldn't skip. +static DWORD WINAPI NopThreadProc(void* /* aThreadParam */) { return 0; } + +static MOZ_NORETURN void __fastcall patched_BaseThreadInitThunk( + BOOL aIsInitialThread, void* aStartAddress, void* aThreadParam) { + if (ShouldBlockThread(aStartAddress)) { + aStartAddress = (void*)NopThreadProc; + } + + stub_BaseThreadInitThunk(aIsInitialThread, aStartAddress, aThreadParam); +} + +static WindowsDllInterceptor NtDllIntercept; +static WindowsDllInterceptor Kernel32Intercept; + +static void GetNativeNtBlockSetWriter(); + +static glue::LoaderObserver gMozglueLoaderObserver; +static nt::WinLauncherServices gWinLauncher; + +MFBT_API void DllBlocklist_Initialize(uint32_t aInitFlags) { + if (sBlocklistInitAttempted) { + return; + } + sBlocklistInitAttempted = true; + + sInitFlags = aInitFlags; + + glue::ModuleLoadFrame::StaticInit(&gMozglueLoaderObserver, &gWinLauncher); + +#ifdef _M_AMD64 + if (!IsWin8OrLater()) { + Kernel32Intercept.Init(L"kernel32.dll"); + // The crash that this hook works around is only seen on Win7. + stub_RtlInstallFunctionTableCallback.Set( + Kernel32Intercept, "RtlInstallFunctionTableCallback", + &patched_RtlInstallFunctionTableCallback); + } +#endif + + // Bug 1361410: WRusr.dll will overwrite our hook and cause a crash. + // Workaround: If we detect WRusr.dll, don't hook. + if (!GetModuleHandleW(L"WRusr.dll")) { + Kernel32Intercept.Init(L"kernel32.dll"); + if (!stub_BaseThreadInitThunk.SetDetour(Kernel32Intercept, + "BaseThreadInitThunk", + &patched_BaseThreadInitThunk)) { +#ifdef DEBUG + printf_stderr("BaseThreadInitThunk hook failed\n"); +#endif + } + } + +#if defined(NIGHTLY_BUILD) + // Populate a list of thread start addresses to block. + HMODULE hKernel = GetModuleHandleW(L"kernel32.dll"); + if (hKernel) { + void* pProc; + + pProc = (void*)GetProcAddress(hKernel, "LoadLibraryA"); + gStartAddressesToBlock[0] = pProc; + + pProc = (void*)GetProcAddress(hKernel, "LoadLibraryW"); + gStartAddressesToBlock[1] = pProc; + + pProc = (void*)GetProcAddress(hKernel, "LoadLibraryExA"); + gStartAddressesToBlock[2] = pProc; + + pProc = (void*)GetProcAddress(hKernel, "LoadLibraryExW"); + gStartAddressesToBlock[3] = pProc; + } +#endif + + if (aInitFlags & eDllBlocklistInitFlagWasBootstrapped) { + GetNativeNtBlockSetWriter(); + return; + } + + // There are a couple of exceptional cases where we skip user32.dll check. + // - If the the process was bootstrapped by the launcher process, AppInit + // DLLs will be intercepted by the new DllBlockList. No need to check + // here. + // - The code to initialize the base profiler loads winmm.dll which + // statically links user32.dll on an older Windows. This means if the base + // profiler is active before coming here, we cannot fully intercept AppInit + // DLLs. Given that the base profiler is used outside the typical use + // cases, it's ok not to check user32.dll in this scenario. + const bool skipUser32Check = + (sInitFlags & eDllBlocklistInitFlagWasBootstrapped) || + (!IsWin10AnniversaryUpdateOrLater() && + baseprofiler::profiler_is_active()); + + // In order to be effective against AppInit DLLs, the blocklist must be + // initialized before user32.dll is loaded into the process (bug 932100). + if (!skipUser32Check && GetModuleHandleW(L"user32.dll")) { + sUser32BeforeBlocklist = true; +#ifdef DEBUG + printf_stderr("DLL blocklist was unable to intercept AppInit DLLs.\n"); +#endif + } + + NtDllIntercept.Init("ntdll.dll"); + + ReentrancySentinel::InitializeStatics(); + + // We specifically use a detour, because there are cases where external + // code also tries to hook LdrLoadDll, and doesn't know how to relocate our + // nop space patches. (Bug 951827) + bool ok = stub_LdrLoadDll.SetDetour(NtDllIntercept, "LdrLoadDll", + &patched_LdrLoadDll); + + if (!ok) { + sBlocklistInitFailed = true; +#ifdef DEBUG + printf_stderr("LdrLoadDll hook failed, no dll blocklisting active\n"); +#endif + } + + // If someone injects a thread early that causes user32.dll to load off the + // main thread this causes issues, so load it as soon as we've initialized + // the block-list. (See bug 1400637) + if (!sUser32BeforeBlocklist && !IsWin32kLockedDown()) { + ::LoadLibraryW(L"user32.dll"); + } +} + +#ifdef DEBUG +MFBT_API void DllBlocklist_Shutdown() {} +#endif // DEBUG + +static void InternalWriteNotes(AnnotationWriter& aWriter) { + WritableBuffer buffer; + DllBlockSet::Write(buffer); + + aWriter.Write(Annotation::BlockedDllList, buffer.Data(), buffer.Length()); + + if (sBlocklistInitFailed) { + aWriter.Write(Annotation::BlocklistInitFailed, "1"); + } + + if (sUser32BeforeBlocklist) { + aWriter.Write(Annotation::User32BeforeBlocklist, "1"); + } +} + +using WriterFn = void (*)(AnnotationWriter&); +static WriterFn gWriterFn = &InternalWriteNotes; + +static void GetNativeNtBlockSetWriter() { + auto nativeWriter = reinterpret_cast<WriterFn>( + ::GetProcAddress(::GetModuleHandleW(nullptr), "NativeNtBlockSet_Write")); + if (nativeWriter) { + gWriterFn = nativeWriter; + } +} + +MFBT_API void DllBlocklist_WriteNotes(AnnotationWriter& aWriter) { + MOZ_ASSERT(gWriterFn); + gWriterFn(aWriter); +} + +MFBT_API bool DllBlocklist_CheckStatus() { + if (sBlocklistInitFailed || sUser32BeforeBlocklist) return false; + return true; +} + +// ============================================================================ +// This section is for DLL Services +// ============================================================================ + +namespace mozilla { +Authenticode* GetAuthenticode(); +} // namespace mozilla + +/** + * Please note that DllBlocklist_SetFullDllServices is called with + * aSvc = nullptr to de-initialize the resources even though they + * have been initialized via DllBlocklist_SetBasicDllServices. + */ +MFBT_API void DllBlocklist_SetFullDllServices( + mozilla::glue::detail::DllServicesBase* aSvc) { + glue::AutoExclusiveLock lock(gDllServicesLock); + if (aSvc) { + aSvc->SetAuthenticodeImpl(GetAuthenticode()); + aSvc->SetWinLauncherServices(gWinLauncher); + gMozglueLoaderObserver.Forward(aSvc); + } + + gDllServices = aSvc; +} + +MFBT_API void DllBlocklist_SetBasicDllServices( + mozilla::glue::detail::DllServicesBase* aSvc) { + if (!aSvc) { + return; + } + + aSvc->SetAuthenticodeImpl(GetAuthenticode()); + gMozglueLoaderObserver.Disable(); +} diff --git a/toolkit/xre/dllservices/mozglue/WindowsDllBlocklist.h b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklist.h new file mode 100644 index 0000000000..1b43e6d1fc --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklist.h @@ -0,0 +1,95 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_windowsdllblocklist_h +#define mozilla_windowsdllblocklist_h + +#if (defined(_MSC_VER) || defined(__MINGW32__)) && \ + (defined(_M_IX86) || defined(_M_X64) || defined(_M_ARM64)) + +# include <windows.h> +# include "CrashAnnotations.h" +# include "mozilla/Attributes.h" +# include "mozilla/ProcessType.h" +# include "mozilla/Types.h" + +# define HAS_DLL_BLOCKLIST + +enum DllBlocklistInitFlags { + eDllBlocklistInitFlagDefault = 0, + eDllBlocklistInitFlagIsChildProcess = 1 << 0, + eDllBlocklistInitFlagWasBootstrapped = 1 << 1, + eDllBlocklistInitFlagIsUtilityProcess = 1 << 2, + eDllBlocklistInitFlagIsSocketProcess = 1 << 3, + eDllBlocklistInitFlagIsGPUProcess = 1 << 4, + eDllBlocklistInitFlagIsGMPluginProcess = 1 << 5, +}; + +inline void SetDllBlocklistProcessTypeFlags(uint32_t& aFlags, + GeckoProcessType aProcessType) { + if (aProcessType == GeckoProcessType_Utility) { + aFlags |= eDllBlocklistInitFlagIsUtilityProcess; + } else if (aProcessType == GeckoProcessType_Socket) { + aFlags |= eDllBlocklistInitFlagIsSocketProcess; + } else if (aProcessType == GeckoProcessType_GPU) { + aFlags |= eDllBlocklistInitFlagIsGPUProcess; + } else if (aProcessType == GeckoProcessType_GMPlugin) { + aFlags |= eDllBlocklistInitFlagIsGMPluginProcess; + } +} + +// Only available from within firefox.exe +# if !defined(IMPL_MFBT) && !defined(MOZILLA_INTERNAL_API) +extern uint32_t gBlocklistInitFlags; +# endif // !defined(IMPL_MFBT) && !defined(MOZILLA_INTERNAL_API) + +MFBT_API void DllBlocklist_Initialize( + uint32_t aInitFlags = eDllBlocklistInitFlagDefault); +MFBT_API void DllBlocklist_WriteNotes(CrashReporter::AnnotationWriter& aWriter); +MFBT_API bool DllBlocklist_CheckStatus(); + +// This export intends to clean up after DllBlocklist_Initialize(). +// It's disabled in release builds for performance and to limit callers' ability +// to interfere with dll blocking. +# ifdef DEBUG +MFBT_API void DllBlocklist_Shutdown(); +# endif // DEBUG + +namespace mozilla { +namespace glue { +namespace detail { +// Forward declaration +class DllServicesBase; + +template <size_t N> +class WritableBuffer { + char mBuffer[N]; + size_t mLen; + + size_t Available() const { return sizeof(mBuffer) - mLen; } + + public: + WritableBuffer() : mBuffer{0}, mLen(0) {} + + void Write(const char* aData, size_t aLen) { + size_t writable_len = std::min(aLen, Available()); + memcpy(mBuffer + mLen, aData, writable_len); + mLen += writable_len; + } + + size_t Length() const { return mLen; } + const char* Data() const { return mBuffer; } +}; +} // namespace detail +} // namespace glue +} // namespace mozilla + +MFBT_API void DllBlocklist_SetFullDllServices( + mozilla::glue::detail::DllServicesBase* aSvc); +MFBT_API void DllBlocklist_SetBasicDllServices( + mozilla::glue::detail::DllServicesBase* aSvc); + +#endif // defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64)) +#endif // mozilla_windowsdllblocklist_h diff --git a/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistCommon.h b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistCommon.h new file mode 100644 index 0000000000..b9f176ac02 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistCommon.h @@ -0,0 +1,46 @@ +/* -*- 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/. */ + +#ifndef mozilla_WindowsDllBlocklistCommon_h +#define mozilla_WindowsDllBlocklistCommon_h + +#include "mozilla/ArrayUtils.h" +#include "mozilla/WindowsDllBlocklistInfo.h" + +#if !defined(DLL_BLOCKLIST_STRING_TYPE) +# error "You must define DLL_BLOCKLIST_STRING_TYPE" +#endif // !defined(DLL_BLOCKLIST_STRING_TYPE) + +#define DLL_BLOCKLIST_DEFINITIONS_BEGIN_NAMED(name) \ + using DllBlockInfo = mozilla::DllBlockInfoT<DLL_BLOCKLIST_STRING_TYPE>; \ + static const DllBlockInfo name[] = { +#define DLL_BLOCKLIST_DEFINITIONS_BEGIN \ + DLL_BLOCKLIST_DEFINITIONS_BEGIN_NAMED(gWindowsDllBlocklist) + +#define DLL_BLOCKLIST_DEFINITIONS_END \ + {} \ + } \ + ; + +#define DECLARE_POINTER_TO_FIRST_DLL_BLOCKLIST_ENTRY_FOR(name, list) \ + const DllBlockInfo* name = &list[0] + +#define DECLARE_POINTER_TO_FIRST_DLL_BLOCKLIST_ENTRY(name) \ + DECLARE_POINTER_TO_FIRST_DLL_BLOCKLIST_ENTRY_FOR(name, gWindowsDllBlocklist) + +#define DECLARE_POINTER_TO_LAST_DLL_BLOCKLIST_ENTRY_FOR(name, list) \ + const DllBlockInfo* name = &list[mozilla::ArrayLength(list) - 1] + +#define DECLARE_POINTER_TO_LAST_DLL_BLOCKLIST_ENTRY(name) \ + DECLARE_POINTER_TO_LAST_DLL_BLOCKLIST_ENTRY_FOR(name, gWindowsDllBlocklist) + +#define DECLARE_DLL_BLOCKLIST_NUM_ENTRIES_FOR(name, list) \ + const size_t name = mozilla::ArrayLength(list) - 1 + +#define DECLARE_DLL_BLOCKLIST_NUM_ENTRIES(name) \ + DECLARE_DLL_BLOCKLIST_NUM_ENTRIES_FOR(name, gWindowsDllBlocklist) + +#endif // mozilla_WindowsDllBlocklistCommon_h diff --git a/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistDefs.in b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistDefs.in new file mode 100644 index 0000000000..eaeee1b870 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistDefs.in @@ -0,0 +1,434 @@ +# -*- 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/. + +# This file exposes these lists: +# ALL_PROCESSES, BROWSER_PROCESS, CHILD_PROCESSES, GMPLUGIN_PROCESSES, +# GPU_PROCESSES, UTILITY_PROCESSES, and SOCKET_PROCESSES +# +# In addition, each of those lists supports a special variant for test-only +# entries: +# ALL_PROCESSES_TESTS, BROWSER_PROCESS_TESTS, CHILD_PROCESSES_TESTS, +# GMPLUGIN_PROCESSES_TESTS, GPU_PROCESSES_TESTS, UTILITY_PROCESSES_TESTS, and +# SOCKET_PROCESSES_TESTS +# +# Choose the list that is applicable to the applicable process type(s) for your +# DLL block. +# +# The currently supported blocklist entry types are: +# DllBlocklistEntry, A11yBlocklistEntry, LspBlocklistEntry, +# RedirectToNoOpEntryPoint +# (See gen_dll_blocklist_defs.py for their documentation.) +# +# Example: +# ALL_PROCESSES += [ +# DllBlocklistEntry("foo.dll", (1, 2, 3, 4)), +# DllBlocklistEntry("foo.dll", ALL_VERSIONS), +# DllBlocklistEntry("foo.dll", UNVERSIONED), +# DllBlocklistEntry("foo.dll", 0x0000123400000000), +# DllBlocklistEntry("foo.dll", PETimeStamp(0x12345678)), +# ] +# +# The version parameter the "last bad" version, that is, we block anything that +# is less-than or equal to that version. + +ALL_PROCESSES += [ + # NPFFAddon - Known malware + DllBlocklistEntry("npffaddon.dll", ALL_VERSIONS), + + # AVG 8 - Antivirus vendor AVG, old version, plugin already blocklisted + DllBlocklistEntry("avgrsstx.dll", (8,5,0,401)), + + # calc.dll - Suspected malware + DllBlocklistEntry("calc.dll", (1,0,0,1)), + + # hook.dll - Suspected malware + DllBlocklistEntry("hook.dll", ALL_VERSIONS), + + # GoogleDesktopNetwork3.dll - Extremely old, unversioned instances + # of this DLL cause crashes + # GoogleDesktopNetwork3.dll - Discontinued product but seems to crash at + # least on Win7 32-bits + # + # Since it was already blocked for all processes but unversioned DLLs, it + # looks like one cannot have this and a second block declaration for + # utility processes but limiting versions / windows targets + DllBlocklistEntry("googledesktopnetwork3.dll", ALL_VERSIONS), + + # rdolib.dll - Suspected malware + DllBlocklistEntry("rdolib.dll", (6,0,88,4)), + + # fgjk4wvb.dll - Suspected malware + DllBlocklistEntry("fgjk4wvb.dll", (8,8,8,8)), + + # radhslib.dll - Naomi internet filter - unmaintained since 2006 + DllBlocklistEntry("radhslib.dll", UNVERSIONED), + + # Music download filter for vkontakte.ru - old instances + # of this DLL cause crashes + DllBlocklistEntry("vksaver.dll", (2,2,2,0)), + + # Topcrash in Firefox 4.0b1 + DllBlocklistEntry("rlxf.dll", (1,2,323,1)), + + # psicon.dll - Topcrashes in Thunderbird, and some crashes in Firefox + # Adobe photoshop library, now redundant in later installations + DllBlocklistEntry("psicon.dll", ALL_VERSIONS), + + # Topcrash in Firefox 4 betas (bug 618899), + DllBlocklistEntry("accelerator.dll", (3,2,1,6)), + + # Topcrash with Roboform in Firefox 8 (bug 699134), + DllBlocklistEntry("rf-firefox.dll", (7,6,1,0)), + DllBlocklistEntry("roboform.dll", (7,6,1,0)), + + # Topcrash with Babylon Toolbar on FF16+ (bug 721264), + DllBlocklistEntry("babyfox.dll", ALL_VERSIONS), + + # sprotector.dll crashes, bug 957258 + DllBlocklistEntry("sprotector.dll", ALL_VERSIONS), + + # Windows Media Foundation FLAC decoder and type sniffer (bug 839031). + DllBlocklistEntry("mfflac.dll", ALL_VERSIONS), + + # Older Relevant Knowledge DLLs cause us to crash (bug 904001). + DllBlocklistEntry("rlnx.dll", (1, 3, 334, 9)), + DllBlocklistEntry("pmnx.dll", (1, 3, 334, 9)), + DllBlocklistEntry("opnx.dll", (1, 3, 334, 9)), + DllBlocklistEntry("prnx.dll", (1, 3, 334, 9)), + + # Older belgian ID card software causes Firefox to crash or hang on + # shutdown, bug 831285 and 918399. + DllBlocklistEntry("beid35cardlayer.dll", (3, 5, 6, 6968)), + + # bug 925459, bitguard crashes + DllBlocklistEntry("bitguard.dll", ALL_VERSIONS), + + # bug 812683 - crashes in Windows library when Asus Gamer OSD is installed + # Software is discontinued/unsupported + DllBlocklistEntry("atkdx11disp.dll", ALL_VERSIONS), + + # Topcrash with Conduit SearchProtect, bug 944542 + DllBlocklistEntry("spvc32.dll", ALL_VERSIONS), + + # Topcrash with V-bates, bug 1002748 and bug 1023239 + DllBlocklistEntry("libinject.dll", UNVERSIONED), + DllBlocklistEntry("libinject2.dll", PETimeStamp(0x537DDC93)), + DllBlocklistEntry("libredir2.dll", PETimeStamp(0x5385B7ED)), + + # Crashes with RoboForm2Go written against old SDK, bug 988311/1196859 + DllBlocklistEntry("rf-firefox-22.dll", ALL_VERSIONS), + DllBlocklistEntry("rf-firefox-40.dll", ALL_VERSIONS), + + # Crashes with DesktopTemperature, bug 1046382 + DllBlocklistEntry("dtwxsvc.dll", PETimeStamp(0x53153234)), + + # Startup crashes with Lenovo Onekey Theater, bug 1123778 + DllBlocklistEntry("activedetect32.dll", UNVERSIONED), + DllBlocklistEntry("activedetect64.dll", UNVERSIONED), + DllBlocklistEntry("windowsapihookdll32.dll", UNVERSIONED), + DllBlocklistEntry("windowsapihookdll64.dll", UNVERSIONED), + + # Flash crashes with RealNetworks RealDownloader, bug 1132663 + DllBlocklistEntry("rndlnpshimswf.dll", ALL_VERSIONS), + DllBlocklistEntry("rndlmainbrowserrecordplugin.dll", ALL_VERSIONS), + + # Startup crashes with RealNetworks Browser Record Plugin, bug 1170141 + DllBlocklistEntry("nprpffbrowserrecordext.dll", ALL_VERSIONS), + DllBlocklistEntry("nprndlffbrowserrecordext.dll", ALL_VERSIONS), + + # Crashes with CyberLink YouCam, bug 1136968 + DllBlocklistEntry("ycwebcamerasource.ax", (2, 0, 0, 1611)), + + # Old version of WebcamMax crashes WebRTC, bug 1130061 + DllBlocklistEntry("vwcsource.ax", (1, 5, 0, 0)), + + # NetOp School, discontinued product, bug 763395 + DllBlocklistEntry("nlsp.dll", (6, 23, 2012, 19)), + + # Orbit Downloader, bug 1222819 + DllBlocklistEntry("grabdll.dll", (2, 6, 1, 0)), + DllBlocklistEntry("grabkernel.dll", (1, 0, 0, 1)), + + # ESET, bug 1229252 + DllBlocklistEntry("eoppmonitor.dll", ALL_VERSIONS), + + # SS2OSD, bug 1262348 + DllBlocklistEntry("ss2osd.dll", ALL_VERSIONS), + DllBlocklistEntry("ss2devprops.dll", ALL_VERSIONS), + + # NHASUSSTRIXOSD.DLL, bug 1269244 + DllBlocklistEntry("nhasusstrixosd.dll", ALL_VERSIONS), + DllBlocklistEntry("nhasusstrixdevprops.dll", ALL_VERSIONS), + + # Crashes with PremierOpinion/RelevantKnowledge, bug 1277846 + DllBlocklistEntry("opls.dll", ALL_VERSIONS), + DllBlocklistEntry("opls64.dll", ALL_VERSIONS), + DllBlocklistEntry("pmls.dll", ALL_VERSIONS), + DllBlocklistEntry("pmls64.dll", ALL_VERSIONS), + DllBlocklistEntry("prls.dll", ALL_VERSIONS), + DllBlocklistEntry("prls64.dll", ALL_VERSIONS), + DllBlocklistEntry("rlls.dll", ALL_VERSIONS), + DllBlocklistEntry("rlls64.dll", ALL_VERSIONS), + + # Vorbis DirectShow filters, bug 1239690. + DllBlocklistEntry("vorbis.acm", (0, 0, 3, 6)), + + # AhnLab Internet Security, bug 1311969 + DllBlocklistEntry("nzbrcom.dll", ALL_VERSIONS), + + # K7TotalSecurity, bug 1339083 and bug 1694489. + DllBlocklistEntry("k7pswsen.dll", (15, 2, 2, 102)), + + # smci*.dll - goobzo crashware (bug 1339908), + DllBlocklistEntry("smci32.dll", ALL_VERSIONS), + DllBlocklistEntry("smci64.dll", ALL_VERSIONS), + + # Crashes with Internet Download Manager, bug 1333486 + DllBlocklistEntry("idmcchandler7.dll", ALL_VERSIONS), + DllBlocklistEntry("idmcchandler7_64.dll", ALL_VERSIONS), + DllBlocklistEntry("idmcchandler5.dll", ALL_VERSIONS), + DllBlocklistEntry("idmcchandler5_64.dll", ALL_VERSIONS), + + # Nahimic 2 breaks applicaton update (bug 1356637), + DllBlocklistEntry("nahimic2devprops.dll", (2, 5, 19, 0xffff)), + # Nahimic is causing crashes, bug 1233556 + DllBlocklistEntry("nahimicmsiosd.dll", UNVERSIONED), + # Nahimic is causing crashes, bug 1360029 + DllBlocklistEntry("nahimicvrdevprops.dll", UNVERSIONED), + DllBlocklistEntry("nahimic2osd.dll", (2, 5, 19, 0xffff)), + DllBlocklistEntry("nahimicmsidevprops.dll", UNVERSIONED), + + # Bug 1268470 - crashes with Kaspersky Lab on Windows 8 + DllBlocklistEntry("klsihk64.dll", (14, 0, 456, 0xffff), + BLOCK_WIN8_AND_OLDER), + + # Bug 1579758, crashes with OpenSC nightly version 0.19.0.448 and lower + DllBlocklistEntry("onepin-opensc-pkcs11.dll", (0, 19, 0, 448)), + + # Avecto Privilege Guard causes crashes, bug 1385542 + DllBlocklistEntry("pghook.dll", ALL_VERSIONS), + + # Old versions of G DATA BankGuard, bug 1421991 + DllBlocklistEntry("banksafe64.dll", (1, 2, 15299, 65535)), + + # Old versions of G DATA, bug 1043775 + DllBlocklistEntry("gdkbfltdll64.dll", (1, 0, 14141, 240)), + + # Dell Backup and Recovery tool causes crashes, bug 1433408 + DllBlocklistEntry("dbroverlayiconnotbackuped.dll", (1, 8, 0, 9)), + DllBlocklistEntry("dbroverlayiconbackuped.dll", (1, 8, 0, 9)), + + # NVIDIA nView Desktop Management causes crashes, bug 1465787 + DllBlocklistEntry("nviewh64.dll", (6, 14, 10, 14847)), + + # Ivanti Endpoint Security, bug 1553776 + DllBlocklistEntry("sxwmon.dll", ALL_VERSIONS), + DllBlocklistEntry("sxwmon64.dll", ALL_VERSIONS), + + # 360 Safeguard/360 Total Security causes a11y crashes, bug 1536227. + DllBlocklistEntry("safemon64.dll", ALL_VERSIONS), + + # Old versions of Digital Guardian, bug 1318858, bug 1603974, + # and bug 1672367 + RedirectToNoOpEntryPoint("dgapi.dll", (7, 5, 0xffff, 0xffff)), + RedirectToNoOpEntryPoint("dgapi64.dll", (7, 5, 0xffff, 0xffff)), + + # Old versions of COMODO Internet Security, bug 1608048 + DllBlocklistEntry("IseGuard32.dll", (1, 6, 13835, 184)), + DllBlocklistEntry("IseGuard64.dll", (1, 6, 13835, 184)), + + # Old version of COMODO Firewall, bug 1407712 and bug 1624336 + DllBlocklistEntry("guard64.dll", (8, 4, 0, 65535)), + + # Old version of Panda Security, bug 1637984 and bug 1705125 + DllBlocklistEntry("PavLspHook64.dll", (9, 2, 2, 1), BLOCK_WIN7_AND_OLDER), + DllBlocklistEntry("PavSHook64.dll", (9, 2, 2, 1), BLOCK_WIN7_AND_OLDER), + + # Webroot SecureAnywhere causes crashes, bug 1700281 + DllBlocklistEntry("WRDll.x64.dll", (1, 1, 0, 227)), + DllBlocklistEntry("WRDll.x86.dll", (1, 1, 0, 227)), + + # Webroot SecureAnywhere causes deadlocks, bug 1752466 + DllBlocklistEntry("WRusr.dll", (9, 0, 32, 49)), + + # InfoWatch Device Monitor causes crashes, bug 1704276 + DllBlocklistEntry("iwprn.dll", (6, 9, 11, 360)), + DllBlocklistEntry("iwprn_x86.dll", (6, 9, 11, 360)), + + # Forcepoint DLLs causing crashes, bug 1767993 + DllBlocklistEntry("qipcap.dll", (7, 7, 819, 1)), + DllBlocklistEntry("qipcap64.dll", (7, 7, 819, 1)), + + # Cylance, bug 1756190 and bug 1799562 + DllBlocklistEntry("CylanceMemDef64.dll", ALL_VERSIONS), + + # ESET security (bug 1833793) + DllBlocklistEntry("eoppbrowser.dll", (1, 0, 88, 0), BLOCK_WIN7_AND_OLDER), +] + +ALL_PROCESSES_TESTS += [ + # DLLs used by TestDllBlocklist* gTests + DllBlocklistEntry("testdllblocklist_matchbyname.dll", ALL_VERSIONS), + DllBlocklistEntry("testdllblocklist_matchbyversion.dll", (5, 5, 5, 5)), + DllBlocklistEntry("testdllblocklist_allowbyversion.dll", (5, 5, 5, 5)), + RedirectToNoOpEntryPoint("testdllblocklist_noopentrypoint.dll", + (5, 5, 5, 5)), + DllBlocklistEntry("testdllblocklist_singlenotification1.dll", ALL_VERSIONS), + DllBlocklistEntry("testdllblocklist_singlenotification2.dll", (5, 5, 5, 5)), + DllBlocklistEntry("testdllblocklist_multipleentries_differentprocesses.dll", UNVERSIONED), + # Multiple entries so that search is required to account for each one + DllBlocklistEntry("testdllblocklist_multipleentries_sameprocessbackward.dll", (5, 5, 5, 5)), + DllBlocklistEntry("testdllblocklist_multipleentries_sameprocessbackward.dll", (1, 1, 1, 1)), + DllBlocklistEntry("testdllblocklist_multipleentries_sameprocessbackward.dll", (1, 1, 1, 1)), + DllBlocklistEntry("testdllblocklist_multipleentries_sameprocessbackward.dll", (1, 1, 1, 1)), + DllBlocklistEntry("testdllblocklist_multipleentries_sameprocessbackward.dll", (1, 1, 1, 1)), + DllBlocklistEntry("testdllblocklist_multipleentries_sameprocessbackward.dll", (1, 1, 1, 1)), + DllBlocklistEntry("testdllblocklist_multipleentries_sameprocessbackward.dll", (1, 1, 1, 1)), + # Multiple entries so that search is required to account for each one + DllBlocklistEntry("testdllblocklist_multipleentries_sameprocessforward.dll", (1, 1, 1, 1)), + DllBlocklistEntry("testdllblocklist_multipleentries_sameprocessforward.dll", (1, 1, 1, 1)), + DllBlocklistEntry("testdllblocklist_multipleentries_sameprocessforward.dll", (1, 1, 1, 1)), + DllBlocklistEntry("testdllblocklist_multipleentries_sameprocessforward.dll", (1, 1, 1, 1)), + DllBlocklistEntry("testdllblocklist_multipleentries_sameprocessforward.dll", (1, 1, 1, 1)), + DllBlocklistEntry("testdllblocklist_multipleentries_sameprocessforward.dll", (1, 1, 1, 1)), + DllBlocklistEntry("testdllblocklist_multipleentries_sameprocessforward.dll", (1, 1, 1, 1)), + DllBlocklistEntry("testdllblocklist_multipleentries_sameprocessforward.dll", (5, 5, 5, 5)), +] + +BROWSER_PROCESS += [ + # RealPlayer, bug 1418535, bug 1437417 + # Versions before 18.1.11.0 cause severe performance problems. + A11yBlocklistEntry("dtvhooks.dll", (18, 1, 10, 0xffff)), + A11yBlocklistEntry("dtvhooks64.dll", (18, 1, 10, 0xffff)), + + # SolidWorks Windows Explorer integration causes crashes, bug 1566109 + # and bug 1468250 + DllBlocklistEntry("Database.dll", ALL_VERSIONS), + + # Hancom Office shell extension causes crashes when the file picker is + # opened. See bug 1581092. + DllBlocklistEntry("hncshellext64.dll", (1, 0, 0 ,3)), + + # Cambridge Silicon Radio, bug 1634538 + DllBlocklistEntry("BLEtokenCredentialProvider.dll", (2, 1, 63, 0)), + + # FYunZip and PuddingZip, loaded as shell extension, cause crashes + # bug 1576728 + DllBlocklistEntry("oly64.dll", (1, 1, 3, 19920)), + DllBlocklistEntry("oly.dll", (1, 1, 3, 19920)), + DllBlocklistEntry("pdzipmenu64.dll", (1, 4, 4, 20103)), + DllBlocklistEntry("pdzipmenu32.dll", (1, 4, 4, 20103)), + + # McAfee Data Loss Prevention causes crashs with multiple signatures, + # bug 1634090, bug 1717676 + DllBlocklistEntry("fcagff.dll", (11, 6, 300, 2)), + DllBlocklistEntry("fcagff64.dll", (11, 6, 300, 2)), + + # AVerMedia's virtual camera module causes crashes (bug 1692908) + DllBlocklistEntry("avmvirtualsource.ax", (1, 0, 0, 3)), + + # Avast (bug 1794064) + DllBlocklistEntry("aswJsFlt.dll", (18, 0, 1473, 0)), + + # ASUS Web Storage (bug 1714940) + DllBlocklistEntry("asuswsshellext64.dll", (1, 1, 0, 27)), + + # ExplorerPatcher (bug 1798707) + DllBlocklistEntry("explorerpatcher.amd64.dll", ALL_VERSIONS), + + # Sonic Studio 3 (bug 1540883) + DllBlocklistEntry("ss3devprops.dll", ALL_VERSIONS), + + # Nahimic audio driver (bug 1540883) + DllBlocklistEntry("audiodevprops2.dll", (2, 6, 14, 0)), + + # Kingsoft internet security (bug 1837242) + DllBlocklistEntry("kisfdpro64.dll", ALL_VERSIONS), + + # Kingsoft internet security (bug 1837246) + DllBlocklistEntry("dghmpg64.dll", ALL_VERSIONS), + + # Legacy DLL for digital signatures with ID cards from Spain (bug 1709082), + # up-to-date setups use DNIe_P11_x64.dll instead + DllBlocklistEntry("UsrDNIeCertStore.dll", ALL_VERSIONS), +] + +CHILD_PROCESSES += [ + # Causes crashes in the GPU process with WebRender enabled, bug 1544435 + DllBlocklistEntry("wbload.dll", ALL_VERSIONS), + + # Causes crashes in the content process with win32k lockdown, bug 1766022 + DllBlocklistEntry("videocapturer.dll", ALL_VERSIONS), + DllBlocklistEntry("videocapturerhk32.dll", ALL_VERSIONS), + DllBlocklistEntry("videocapturerhk64.dll", ALL_VERSIONS), + + # Causes crashes in the content process with win32k lockdown, bug 1766029 + # The crash seems to be in the *_m module which has been unloaded. + DllBlocklistEntry("safaweb.dll", (3, 0, 21, 3181)), + DllBlocklistEntry("safaweb_m.dll", (3, 0, 21, 3181)), + DllBlocklistEntry("safaweb64.dll", (3, 0, 21, 3181)), + DllBlocklistEntry("safaweb64_m.dll", (3, 0, 21, 3181)), + + # Causes crashes in the content process with win32k lockdown, bug 1769309 + DllBlocklistEntry("hmpalert.dll", (3, 8, 8, 889)), +] + +GPU_PROCESSES += [ + # MSI Nahimic audio driver causes crashes in the GPU process, bug 1804023 + DllBlocklistEntry("nahimicosd.dll", (2, 2, 25, 0)), +] + +GPU_PROCESSES_TESTS += [ + # DLLs used by TestDllBlocklist* gTests + DllBlocklistEntry("testdllblocklist_gpuprocessonly.dll", ALL_VERSIONS), + DllBlocklistEntry("testdllblocklist_multipleentries_differentprocesses.dll", ALL_VERSIONS) +] + +SOCKET_PROCESSES += [ + # Causes crashes in the socket process, bug 1760668 + DllBlocklistEntry("ipseng64.dll", (17, 2, 6, 25)), + DllBlocklistEntry("ipseng32.dll", (17, 2, 6, 25)), + + # Causes crashes in the socket process, bug 1807038 and bug 1845111 + DllBlocklistEntry("kwsui64.dll", ALL_VERSIONS), +] + +SOCKET_PROCESSES_TESTS += [ + # DLLs used by TestDllBlocklist* gTests + DllBlocklistEntry("testdllblocklist_socketprocessonly.dll", ALL_VERSIONS), +] + +UTILITY_PROCESSES += [ + # Generated dynamic code that we block + DllBlocklistEntry("cyinjct.dll", ALL_VERSIONS), + + # Causes crashes in the utility process, bug 1826393 + DllBlocklistEntry("ipseng64.dll", ALL_VERSIONS), + DllBlocklistEntry("ipseng32.dll", ALL_VERSIONS), + DllBlocklistEntry("ks3rdhmpg.dll", ALL_VERSIONS), + DllBlocklistEntry("kwsui64.dll", ALL_VERSIONS), + + # Messes with user32 and breaks Utility on Win7 x86 + DllBlocklistEntry("aswhook.dll", ALL_VERSIONS, BLOCK_WIN7_AND_OLDER), +] + +UTILITY_PROCESSES_TESTS += [ + # DLLs used by TestDllBlocklist* gTests + DllBlocklistEntry("testdllblocklist_utilityprocessonly.dll", ALL_VERSIONS), +] + +GMPLUGIN_PROCESSES += [ + # Causes crashes in the plugin process, bug 1830950 + DllBlocklistEntry("ipseng64.dll", ALL_VERSIONS), + DllBlocklistEntry("ipseng32.dll", ALL_VERSIONS), + DllBlocklistEntry("ks3rdhmpg.dll", ALL_VERSIONS), + DllBlocklistEntry("kwsui64.dll", ALL_VERSIONS), +] + +GMPLUGIN_PROCESSES_TESTS += [ + # DLLs used by TestDllBlocklist* gTests + DllBlocklistEntry("testdllblocklist_gmpluginprocessonly.dll", ALL_VERSIONS), +] diff --git a/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistInfo.h b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistInfo.h new file mode 100644 index 0000000000..3607c3049e --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistInfo.h @@ -0,0 +1,106 @@ +/* -*- 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/. */ + +#ifndef mozilla_WindowsDllBlocklistInfo_h +#define mozilla_WindowsDllBlocklistInfo_h + +#include <stdint.h> + +namespace mozilla { + +// If the USE_TIMESTAMP flag is set, then we use the timestamp from +// the IMAGE_FILE_HEADER in lieu of a version number. +// +// If the UTILITY_PROCESSES_ONLY, SOCKET_PROCESSES_ONLY, GPU_PROCESSES_ONLY, +// or GMPLUGIN_PROCESSES_ONLY flags are set, the dll will only be blocked for +// these specific child processes. Note that these are a subset of +// CHILD_PROCESSES_ONLY. +enum DllBlockInfoFlags : uint32_t { + FLAGS_DEFAULT = 0, + BLOCK_WIN7_AND_OLDER = 1 << 0, + BLOCK_WIN8_AND_OLDER = 1 << 1, + USE_TIMESTAMP = 1 << 2, + CHILD_PROCESSES_ONLY = 1 << 3, + BROWSER_PROCESS_ONLY = 1 << 4, + REDIRECT_TO_NOOP_ENTRYPOINT = 1 << 5, + UTILITY_PROCESSES_ONLY = 1 << 6, + SOCKET_PROCESSES_ONLY = 1 << 7, + GPU_PROCESSES_ONLY = 1 << 8, + GMPLUGIN_PROCESSES_ONLY = 1 << 9, +}; + +constexpr DllBlockInfoFlags operator|(const DllBlockInfoFlags a, + const DllBlockInfoFlags b) { + return static_cast<DllBlockInfoFlags>(static_cast<uint32_t>(a) | + static_cast<uint32_t>(b)); +} + +template <typename StrType> +struct DllBlockInfoT { + // The name of the DLL -- in LOWERCASE! It will be compared to + // a lowercase version of the DLL name only. + StrType mName; + + // If mMaxVersion is ALL_VERSIONS, we'll block all versions of this + // dll. Otherwise, we'll block all versions less than or equal to + // the given version, as queried by GetFileVersionInfo and + // VS_FIXEDFILEINFO's dwFileVersionMS and dwFileVersionLS fields. + // + // Note that the version is usually 4 components, which is A.B.C.D + // encoded as 0x AAAA BBBB CCCC DDDD ULL (spaces added for clarity), + // but it's not required to be of that format. + uint64_t mMaxVersion; + + // If the USE_TIMESTAMP flag is set, then we use the timestamp from + // the IMAGE_FILE_HEADER in lieu of a version number. + // + // If the UTILITY_PROCESSES_ONLY, SOCKET_PROCESSES_ONLY, GPU_PROCESSES_ONLY, + // or GMPLUGIN_PROCESSES_ONLY flags are set, the dll will only be blocked for + // these specific child processes. Note that these are a subset of + // CHILD_PROCESSES_ONLY. These flags cannot be combined. + DllBlockInfoFlags mFlags; + + bool IsVersionBlocked(const uint64_t aOther) const { + if (mMaxVersion == ALL_VERSIONS) { + return true; + } + + return aOther <= mMaxVersion; + } + + bool IsValidDynamicBlocklistEntry() const; + + static const uint64_t ALL_VERSIONS = (uint64_t)-1LL; + + // DLLs sometimes ship without a version number, particularly early + // releases. Blocking "version <= 0" has the effect of blocking unversioned + // DLLs (since the call to get version info fails), but not blocking + // any versioned instance. + static const uint64_t UNVERSIONED = 0ULL; +}; + +} // namespace mozilla + +// Convert the 4 (decimal) components of a DLL version number into a +// single unsigned long long, as needed by the blocklist +#if defined(_MSC_VER) && !defined(__clang__) + +// MSVC does not properly handle the constexpr MAKE_VERSION, so we use a macro +// instead (ugh). +# define MAKE_VERSION(a, b, c, d) \ + ((a##ULL << 48) + (b##ULL << 32) + (c##ULL << 16) + d##ULL) + +#else + +static inline constexpr uint64_t MAKE_VERSION(uint16_t a, uint16_t b, + uint16_t c, uint16_t d) { + return static_cast<uint64_t>(a) << 48 | static_cast<uint64_t>(b) << 32 | + static_cast<uint64_t>(c) << 16 | static_cast<uint64_t>(d); +} + +#endif + +#endif // mozilla_WindowsDllBlocklistInfo_h diff --git a/toolkit/xre/dllservices/mozglue/WindowsDllServices.h b/toolkit/xre/dllservices/mozglue/WindowsDllServices.h new file mode 100644 index 0000000000..70ba5e98b0 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/WindowsDllServices.h @@ -0,0 +1,234 @@ +/* -*- 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/. */ + +#ifndef mozilla_glue_WindowsDllServices_h +#define mozilla_glue_WindowsDllServices_h + +#include <utility> + +#include "mozilla/Assertions.h" +#include "mozilla/Authenticode.h" +#include "mozilla/LoaderAPIInterfaces.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Vector.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "mozilla/WindowsDllBlocklist.h" +#include "mozilla/mozalloc.h" + +#if defined(MOZILLA_INTERNAL_API) +# include "MainThreadUtils.h" +# include "nsISupportsImpl.h" +# include "nsString.h" +# include "nsThreadUtils.h" +# include "prthread.h" +# include "mozilla/SchedulerGroup.h" +#endif // defined(MOZILLA_INTERNAL_API) + +// For PCUNICODE_STRING +#include <winternl.h> + +namespace mozilla { +namespace glue { +namespace detail { + +class DllServicesBase : public Authenticode { + public: + /** + * WARNING: This method is called from within an unsafe context that holds + * multiple locks inside the Windows loader. The only thing that + * this function should be used for is dispatching the event to our + * event loop so that it may be handled in a safe context. + */ + virtual void DispatchDllLoadNotification(ModuleLoadInfo&& aModLoadInfo) = 0; + + /** + * This function accepts module load events to be processed later for + * the untrusted modules telemetry ping. + * + * WARNING: This method is run from within the Windows loader and should + * only perform trivial, loader-friendly operations. + */ + virtual void DispatchModuleLoadBacklogNotification( + ModuleLoadInfoVec&& aEvents) = 0; + + void SetAuthenticodeImpl(Authenticode* aAuthenticode) { + mAuthenticode = aAuthenticode; + } + + void SetWinLauncherServices(const nt::WinLauncherServices& aWinLauncher) { + mWinLauncher = aWinLauncher; + } + + template <typename... Args> + LauncherVoidResultWithLineInfo InitDllBlocklistOOP(Args&&... aArgs) { + MOZ_RELEASE_ASSERT(mWinLauncher.mInitDllBlocklistOOP); + return mWinLauncher.mInitDllBlocklistOOP(std::forward<Args>(aArgs)...); + } + + template <typename... Args> + void HandleLauncherError(Args&&... aArgs) { + MOZ_RELEASE_ASSERT(mWinLauncher.mHandleLauncherError); + mWinLauncher.mHandleLauncherError(std::forward<Args>(aArgs)...); + } + + nt::SharedSection* GetSharedSection() { return mWinLauncher.mSharedSection; } + + // In debug builds we override GetBinaryOrgName to add a Gecko-specific + // assertion. OTOH, we normally do not want people overriding this function, + // so we'll make it final in the release case, thus covering all bases. +#if defined(DEBUG) + UniquePtr<wchar_t[]> GetBinaryOrgName( + const wchar_t* aFilePath, + AuthenticodeFlags aFlags = AuthenticodeFlags::Default) override +#else + UniquePtr<wchar_t[]> GetBinaryOrgName( + const wchar_t* aFilePath, + AuthenticodeFlags aFlags = AuthenticodeFlags::Default) final +#endif // defined(DEBUG) + { + if (!mAuthenticode) { + return nullptr; + } + + return mAuthenticode->GetBinaryOrgName(aFilePath, aFlags); + } + + virtual void DisableFull() { DllBlocklist_SetFullDllServices(nullptr); } + + DllServicesBase(const DllServicesBase&) = delete; + DllServicesBase(DllServicesBase&&) = delete; + DllServicesBase& operator=(const DllServicesBase&) = delete; + DllServicesBase& operator=(DllServicesBase&&) = delete; + + protected: + DllServicesBase() : mAuthenticode(nullptr) {} + + virtual ~DllServicesBase() = default; + + void EnableFull() { DllBlocklist_SetFullDllServices(this); } + void EnableBasic() { DllBlocklist_SetBasicDllServices(this); } + + private: + Authenticode* mAuthenticode; + nt::WinLauncherServices mWinLauncher; +}; + +} // namespace detail + +#if defined(MOZILLA_INTERNAL_API) + +struct EnhancedModuleLoadInfo final { + explicit EnhancedModuleLoadInfo(ModuleLoadInfo&& aModLoadInfo) + : mNtLoadInfo(std::move(aModLoadInfo)) { + // Only populate mThreadName when we're on the same thread as the event + if (mNtLoadInfo.mThreadId == ::GetCurrentThreadId()) { + mThreadName = PR_GetThreadName(PR_GetCurrentThread()); + } + MOZ_ASSERT(!mNtLoadInfo.mSectionName.IsEmpty()); + } + + EnhancedModuleLoadInfo(EnhancedModuleLoadInfo&&) = default; + EnhancedModuleLoadInfo& operator=(EnhancedModuleLoadInfo&&) = default; + + EnhancedModuleLoadInfo(const EnhancedModuleLoadInfo&) = delete; + EnhancedModuleLoadInfo& operator=(const EnhancedModuleLoadInfo&) = delete; + + nsDependentString GetSectionName() const { + return mNtLoadInfo.mSectionName.AsString(); + } + + using BacktraceType = decltype(ModuleLoadInfo::mBacktrace); + + ModuleLoadInfo mNtLoadInfo; + nsCString mThreadName; +}; + +class DllServices : public detail::DllServicesBase { + public: + void DispatchDllLoadNotification(ModuleLoadInfo&& aModLoadInfo) final { + // We only notify one blocked DLL load event per blocked DLL for the main + // thread, because dispatching a notification can trigger a new blocked + // DLL load if the DLL is registered as a WH_GETMESSAGE hook. In that case, + // dispatching a notification with every load results in an infinite cycle, + // see bug 1823412. + if (aModLoadInfo.WasBlocked() && NS_IsMainThread()) { + nsDependentString sectionName(aModLoadInfo.mSectionName.AsString()); + + for (const auto& blockedModule : mMainThreadBlockedModules) { + if (sectionName == blockedModule) { + return; + } + } + + MOZ_ALWAYS_TRUE(mMainThreadBlockedModules.append(sectionName)); + } + + nsCOMPtr<nsIRunnable> runnable( + NewRunnableMethod<StoreCopyPassByRRef<EnhancedModuleLoadInfo>>( + "DllServices::NotifyDllLoad", this, &DllServices::NotifyDllLoad, + std::move(aModLoadInfo))); + + SchedulerGroup::Dispatch(TaskCategory::Other, runnable.forget()); + } + + void DispatchModuleLoadBacklogNotification( + ModuleLoadInfoVec&& aEvents) final { + nsCOMPtr<nsIRunnable> runnable( + NewRunnableMethod<StoreCopyPassByRRef<ModuleLoadInfoVec>>( + "DllServices::NotifyModuleLoadBacklog", this, + &DllServices::NotifyModuleLoadBacklog, std::move(aEvents))); + + SchedulerGroup::Dispatch(TaskCategory::Other, runnable.forget()); + } + +# if defined(DEBUG) + UniquePtr<wchar_t[]> GetBinaryOrgName( + const wchar_t* aFilePath, + AuthenticodeFlags aFlags = AuthenticodeFlags::Default) final { + // This function may perform disk I/O, so we should never call it on the + // main thread. + MOZ_ASSERT(!NS_IsMainThread()); + return detail::DllServicesBase::GetBinaryOrgName(aFilePath, aFlags); + } +# endif // defined(DEBUG) + + NS_INLINE_DECL_THREADSAFE_VIRTUAL_REFCOUNTING(DllServices) + + protected: + DllServices() = default; + ~DllServices() = default; + + virtual void NotifyDllLoad(EnhancedModuleLoadInfo&& aModLoadInfo) = 0; + virtual void NotifyModuleLoadBacklog(ModuleLoadInfoVec&& aEvents) = 0; + + private: + // This vector has no associated lock. It must only be used on the main + // thread. + Vector<nsString> mMainThreadBlockedModules; +}; + +#else + +class BasicDllServices final : public detail::DllServicesBase { + public: + BasicDllServices() { EnableBasic(); } + + ~BasicDllServices() = default; + + // Not useful in this class, so provide a default implementation + virtual void DispatchDllLoadNotification( + ModuleLoadInfo&& aModLoadInfo) override {} + + virtual void DispatchModuleLoadBacklogNotification( + ModuleLoadInfoVec&& aEvents) override {} +}; + +#endif // defined(MOZILLA_INTERNAL_API) + +} // namespace glue +} // namespace mozilla + +#endif // mozilla_glue_WindowsDllServices_h diff --git a/toolkit/xre/dllservices/mozglue/WindowsFallbackLoaderAPI.cpp b/toolkit/xre/dllservices/mozglue/WindowsFallbackLoaderAPI.cpp new file mode 100644 index 0000000000..bda309d499 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/WindowsFallbackLoaderAPI.cpp @@ -0,0 +1,91 @@ +/* -*- 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 "WindowsFallbackLoaderAPI.h" + +namespace mozilla { + +ModuleLoadInfo FallbackLoaderAPI::ConstructAndNotifyBeginDllLoad( + void** aContext, PCUNICODE_STRING aRequestedDllName) { + ModuleLoadInfo loadInfo(aRequestedDllName); + + MOZ_ASSERT(mLoaderObserver); + if (mLoaderObserver) { + mLoaderObserver->OnBeginDllLoad(aContext, aRequestedDllName); + } + + return loadInfo; +} + +bool FallbackLoaderAPI::SubstituteForLSP(PCUNICODE_STRING aLSPLeafName, + PHANDLE aOutHandle) { + MOZ_ASSERT(mLoaderObserver); + if (!mLoaderObserver) { + return false; + } + + return mLoaderObserver->SubstituteForLSP(aLSPLeafName, aOutHandle); +} + +void FallbackLoaderAPI::NotifyEndDllLoad(void* aContext, NTSTATUS aLoadNtStatus, + ModuleLoadInfo&& aModuleLoadInfo) { + aModuleLoadInfo.SetEndLoadTimeStamp(); + + if (NT_SUCCESS(aLoadNtStatus)) { + aModuleLoadInfo.CaptureBacktrace(); + } + + MOZ_ASSERT(mLoaderObserver); + if (mLoaderObserver) { + mLoaderObserver->OnEndDllLoad(aContext, aLoadNtStatus, + std::move(aModuleLoadInfo)); + } +} + +nt::AllocatedUnicodeString FallbackLoaderAPI::GetSectionName( + void* aSectionAddr) { + static const StaticDynamicallyLinkedFunctionPtr< + decltype(&::NtQueryVirtualMemory)> + pNtQueryVirtualMemory(L"ntdll.dll", "NtQueryVirtualMemory"); + MOZ_ASSERT(pNtQueryVirtualMemory); + + if (!pNtQueryVirtualMemory) { + return nt::AllocatedUnicodeString(); + } + + nt::MemorySectionNameBuf buf; + NTSTATUS ntStatus = + pNtQueryVirtualMemory(::GetCurrentProcess(), aSectionAddr, + MemorySectionName, &buf, sizeof(buf), nullptr); + if (!NT_SUCCESS(ntStatus)) { + return nt::AllocatedUnicodeString(); + } + + return nt::AllocatedUnicodeString(&buf.mSectionFileName); +} + +nt::LoaderAPI::InitDllBlocklistOOPFnPtr +FallbackLoaderAPI::GetDllBlocklistInitFn() { + MOZ_ASSERT_UNREACHABLE("This should not be called so soon!"); + return nullptr; +} + +nt::LoaderAPI::HandleLauncherErrorFnPtr +FallbackLoaderAPI::GetHandleLauncherErrorFn() { + MOZ_ASSERT_UNREACHABLE("This should not be called so soon!"); + return nullptr; +} + +nt::SharedSection* FallbackLoaderAPI::GetSharedSection() { + MOZ_ASSERT_UNREACHABLE("This should not be called so soon!"); + return nullptr; +} + +void FallbackLoaderAPI::SetObserver(nt::LoaderObserver* aLoaderObserver) { + mLoaderObserver = aLoaderObserver; +} + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/mozglue/WindowsFallbackLoaderAPI.h b/toolkit/xre/dllservices/mozglue/WindowsFallbackLoaderAPI.h new file mode 100644 index 0000000000..8d6fcb2f0c --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/WindowsFallbackLoaderAPI.h @@ -0,0 +1,39 @@ +/* -*- 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/. */ + +#ifndef mozilla_WindowsFallbackLoaderAPI_h +#define mozilla_WindowsFallbackLoaderAPI_h + +#include "mozilla/Attributes.h" +#include "NtLoaderAPI.h" + +namespace mozilla { + +class MOZ_ONLY_USED_TO_AVOID_STATIC_CONSTRUCTORS FallbackLoaderAPI final + : public nt::LoaderAPI { + public: + constexpr FallbackLoaderAPI() : mLoaderObserver(nullptr) {} + + ModuleLoadInfo ConstructAndNotifyBeginDllLoad( + void** aContext, PCUNICODE_STRING aRequestedDllName) final; + bool SubstituteForLSP(PCUNICODE_STRING aLSPLeafName, + PHANDLE aOutHandle) final; + void NotifyEndDllLoad(void* aContext, NTSTATUS aLoadNtStatus, + ModuleLoadInfo&& aModuleLoadInfo) final; + nt::AllocatedUnicodeString GetSectionName(void* aSectionAddr) final; + nt::LoaderAPI::InitDllBlocklistOOPFnPtr GetDllBlocklistInitFn() final; + nt::LoaderAPI::HandleLauncherErrorFnPtr GetHandleLauncherErrorFn() final; + nt::SharedSection* GetSharedSection() final; + + void SetObserver(nt::LoaderObserver* aLoaderObserver); + + private: + nt::LoaderObserver* mLoaderObserver; +}; + +} // namespace mozilla + +#endif // mozilla_WindowsFallbackLoaderAPI_h diff --git a/toolkit/xre/dllservices/mozglue/gen_dll_blocklist_defs.py b/toolkit/xre/dllservices/mozglue/gen_dll_blocklist_defs.py new file mode 100644 index 0000000000..35e1cc300d --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/gen_dll_blocklist_defs.py @@ -0,0 +1,755 @@ +# -*- 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/. + +import os +from copy import deepcopy +from struct import unpack +from uuid import UUID + +from six import iteritems + +H_HEADER = """/* -*- 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 file was auto-generated from {0} by gen_dll_blocklist_data.py. */ + +#ifndef mozilla_{1}_h +#define mozilla_{1}_h + +""" + +H_FOOTER = """#endif // mozilla_{1}_h + +""" + +H_DEFS_BEGIN_DEFAULT = """#include "mozilla/WindowsDllBlocklistCommon.h" + +DLL_BLOCKLIST_DEFINITIONS_BEGIN + +""" + +H_DEFS_END_DEFAULT = """ +DLL_BLOCKLIST_DEFINITIONS_END + +""" + +H_BEGIN_LSP = """#include <guiddef.h> + +static const GUID gLayerGuids[] = { + +""" + +H_END_LSP = """ +}; + +""" + +H_BEGIN_A11Y = """#include "mozilla/WindowsDllBlocklistCommon.h" + +DLL_BLOCKLIST_DEFINITIONS_BEGIN_NAMED(gBlockedInprocDlls) + +""" + +# These flag names should match the ones defined in WindowsDllBlocklistInfo.h +FLAGS_DEFAULT = "FLAGS_DEFAULT" +BLOCK_WIN8_AND_OLDER = "BLOCK_WIN8_AND_OLDER" +BLOCK_WIN7_AND_OLDER = "BLOCK_WIN7_AND_OLDER" +USE_TIMESTAMP = "USE_TIMESTAMP" +CHILD_PROCESSES_ONLY = "CHILD_PROCESSES_ONLY" +BROWSER_PROCESS_ONLY = "BROWSER_PROCESS_ONLY" +SUBSTITUTE_LSP_PASSTHROUGH = "SUBSTITUTE_LSP_PASSTHROUGH" +REDIRECT_TO_NOOP_ENTRYPOINT = "REDIRECT_TO_NOOP_ENTRYPOINT" +UTILITY_PROCESSES_ONLY = "UTILITY_PROCESSES_ONLY" +SOCKET_PROCESSES_ONLY = "SOCKET_PROCESSES_ONLY" +GPU_PROCESSES_ONLY = "GPU_PROCESSES_ONLY" +GMPLUGIN_PROCESSES_ONLY = "GMPLUGIN_PROCESSES_ONLY" + +# Only these flags are available in the input script +INPUT_ONLY_FLAGS = { + BLOCK_WIN8_AND_OLDER, + BLOCK_WIN7_AND_OLDER, +} + + +def FILTER_ALLOW_ALL(entry): + # A11y entries are special, so we always exclude those by default + # (so it's not really allowing 'all', but it is simpler to reason about by + # pretending that it is allowing all.) + return not isinstance(entry, A11yBlocklistEntry) + + +def FILTER_DENY_ALL(entry): + return False + + +def FILTER_ALLOW_ONLY_A11Y(entry): + return isinstance(entry, A11yBlocklistEntry) + + +def FILTER_ALLOW_ONLY_LSP(entry): + return isinstance(entry, LspBlocklistEntry) + + +def FILTER_TESTS_ONLY(entry): + return not isinstance(entry, A11yBlocklistEntry) and entry.is_test() + + +def derive_test_key(key): + return key + "_TESTS" + + +ALL_DEFINITION_LISTS = ( + "ALL_PROCESSES", + "BROWSER_PROCESS", + "CHILD_PROCESSES", + "GMPLUGIN_PROCESSES", + "GPU_PROCESSES", + "UTILITY_PROCESSES", + "SOCKET_PROCESSES", +) + + +class BlocklistDescriptor(object): + """This class encapsulates every file that is output from this script. + Each instance has a name, an "input specification", and optional "flag + specification" and "output specification" entries. + """ + + DEFAULT_OUTSPEC = { + "mode": "", + "filter": FILTER_ALLOW_ALL, + "begin": H_DEFS_BEGIN_DEFAULT, + "end": H_DEFS_END_DEFAULT, + } + + FILE_NAME_TPL = "WindowsDllBlocklist{0}Defs" + + OutputDir = None + ExistingFd = None + ExistingFdLeafName = None + + def __init__(self, name, inspec, **kwargs): + """Positional arguments: + + name -- String containing the name of the output list. + + inspec -- One or more members of ALL_DEFINITION_LISTS. The input used + for this blocklist file is the union of all lists specified by this + variable. + + Keyword arguments: + + outspec -- an optional list of dicts that specify how the lists output + will be written out to a file. Each dict may contain the following keys: + + 'mode' -- a string that specifies a mode that is used when writing + out list entries to this particular output. This is passed in as the + mode argument to DllBlocklistEntry's write method. + + 'filter' -- a function that, given a blocklist entry, decides + whether or not that entry shall be included in this output file. + + 'begin' -- a string that is written to the output file after writing + the file's header, but prior to writing out any blocklist entries. + + 'end' -- a string that is written to the output file after writing + out any blocklist entries but before the file's footer. + + Any unspecified keys will be assigned default values. + + flagspec -- an optional dict whose keys consist of one or more of the + list names from inspec. Each corresponding value is a set of flags that + should be applied to each entry from that list. For example, the + flagspec: + + {'CHILD_PROCESSES': {CHILD_PROCESSES_ONLY}} + + causes any blocklist entries from the CHILD_PROCESSES list to be output + with the CHILD_PROCESSES_ONLY flag set. + + """ + + self._name = name + + # inspec's elements must all come from ALL_DEFINITION_LISTS + assert not (set(inspec).difference(set(ALL_DEFINITION_LISTS))) + + # Internally to the descriptor, we store input specifications as a dict + # that maps each input blocklist name to the set of flags to be applied + # to each entry in that blocklist. + self._inspec = {blocklist: set() for blocklist in inspec} + + self._outspecs = kwargs.get("outspec", BlocklistDescriptor.DEFAULT_OUTSPEC) + if isinstance(self._outspecs, dict): + # _outspecs should always be stored as a list of dicts + self._outspecs = [self._outspecs] + + flagspecs = kwargs.get("flagspec", dict()) + # flagspec's keys must all come from ALL_DEFINITION_LISTS + assert not (set(flagspecs.keys()).difference(set(self._inspec.keys()))) + + # Merge the flags from flagspec into _inspec's sets + for blocklist, flagspec in iteritems(flagspecs): + spec = self._inspec[blocklist] + if not isinstance(spec, set): + raise TypeError("Flag spec for list %s must be a set!" % blocklist) + spec.update(flagspec) + + @staticmethod + def set_output_fd(fd): + """The build system has already provided an open file descriptor for + one of our outputs. We save that here so that we may use that fd once + we're ready to process that fd's file. We also obtain the output dir for + use as the base directory for the other output files that we open. + """ + BlocklistDescriptor.ExistingFd = fd + ( + BlocklistDescriptor.OutputDir, + BlocklistDescriptor.ExistingFdLeafName, + ) = os.path.split(fd.name) + + @staticmethod + def ensure_no_dupes(defs): + """Ensure that defs does not contain any dupes. We raise a ValueError + because this is a bug in the input and requires developer intervention. + """ + seen = set() + for e in defs: + name = e.get_name() + if name not in seen: + seen.add(name) + else: + raise ValueError("Duplicate entry found: %s" % name) + + @staticmethod + def get_test_entries(exec_env, blocklist): + """Obtains all test entries for the corresponding blocklist, and also + ensures that each entry has its test flag set. + + Positional arguments: + + exec_env -- dict containing the globals that were passed to exec to + when the input script was run. + + blocklist -- The name of the blocklist to obtain tests from. This + should be one of the members of ALL_DEFINITION_LISTS + """ + test_key = derive_test_key(blocklist) + + def set_is_test(elem): + elem.set_is_test() + return elem + + return map(set_is_test, exec_env[test_key]) + + def gen_list(self, exec_env, filter_func): + """Generates a sorted list of blocklist entries from the input script, + filtered via filter_func. + + Positional arguments: + + exec_env -- dict containing the globals that were passed to exec to + when the input script was run. This function expects exec_env to + contain lists of blocklist entries, keyed using one of the members of + ALL_DEFINITION_LISTS. + + filter_func -- a filter function that evaluates each blocklist entry + to determine whether or not it should be included in the results. + """ + + # This list contains all the entries across all blocklists that we + # potentially want to process + unified_list = [] + + # For each blocklist specified in the _inspec, we query the globals + # for their entries, add any flags, and then add them to the + # unified_list. + for blocklist, listflags in iteritems(self._inspec): + + def add_list_flags(elem): + # We deep copy so that flags set for an entry in one blocklist + # do not interfere with flags set for an entry in a different + # list. + result = deepcopy(elem) + result.add_flags(listflags) + return result + + # We add list flags *before* filtering because the filters might + # want to access flags as part of their operation. + unified_list.extend(map(add_list_flags, exec_env[blocklist])) + + # We also add any test entries for the lists specified by _inspec + unified_list.extend( + map(add_list_flags, self.get_test_entries(exec_env, blocklist)) + ) + + # Now we filter out any unwanted list entries + filtered_list = filter(filter_func, unified_list) + + # Sort the list on entry name so that the blocklist code may use + # binary search if it so chooses. + return sorted(filtered_list, key=lambda e: e.get_name()) + + @staticmethod + def get_fd(outspec_leaf_name): + """If BlocklistDescriptor.ExistingFd corresponds to outspec_leaf_name, + then we return that. Otherwise, we construct a new absolute path to + outspec_leaf_name and open a new file descriptor for writing. + """ + if ( + not BlocklistDescriptor.ExistingFd + or BlocklistDescriptor.ExistingFdLeafName != outspec_leaf_name + ): + new_name = os.path.join(BlocklistDescriptor.OutputDir, outspec_leaf_name) + return open(new_name, "w") + + fd = BlocklistDescriptor.ExistingFd + BlocklistDescriptor.ExistingFd = None + return fd + + def write(self, src_file, exec_env): + """Write out all output files that are specified by this descriptor. + + Positional arguments: + + src_file -- name of the input file from which the lists were generated. + + exec_env -- dictionary containing the lists that were parsed from the + input file when it was executed. + """ + + for outspec in self._outspecs: + # Use DEFAULT_OUTSPEC to supply defaults for any unused outspec keys + effective_outspec = BlocklistDescriptor.DEFAULT_OUTSPEC.copy() + effective_outspec.update(outspec) + + entries = self.gen_list(exec_env, effective_outspec["filter"]) + if not entries: + continue + + mode = effective_outspec["mode"] + + # Since each output descriptor may generate output across multiple + # modes, each list is uniquified via the concatenation of our name + # and the mode. + listname = self._name + mode + leafname_no_ext = BlocklistDescriptor.FILE_NAME_TPL.format(listname) + leafname = leafname_no_ext + ".h" + + with self.get_fd(leafname) as output: + print(H_HEADER.format(src_file, leafname_no_ext), file=output, end="") + print(effective_outspec["begin"], file=output, end="") + + for e in entries: + e.write(output, mode) + + print(effective_outspec["end"], file=output, end="") + print(H_FOOTER.format(src_file, leafname_no_ext), file=output, end="") + + +A11Y_OUTPUT_SPEC = { + "filter": FILTER_ALLOW_ONLY_A11Y, + "begin": H_BEGIN_A11Y, +} + +LSP_MODE_GUID = "Guid" + +LSP_OUTPUT_SPEC = [ + { + "mode": LSP_MODE_GUID, + "filter": FILTER_ALLOW_ONLY_LSP, + "begin": H_BEGIN_LSP, + "end": H_END_LSP, + }, +] + +GENERATED_BLOCKLIST_FILES = [ + BlocklistDescriptor("A11y", ["BROWSER_PROCESS"], outspec=A11Y_OUTPUT_SPEC), + BlocklistDescriptor( + "Launcher", + ALL_DEFINITION_LISTS, + flagspec={ + "BROWSER_PROCESS": {BROWSER_PROCESS_ONLY}, + "CHILD_PROCESSES": {CHILD_PROCESSES_ONLY}, + "GMPLUGIN_PROCESSES": {GMPLUGIN_PROCESSES_ONLY}, + "GPU_PROCESSES": {GPU_PROCESSES_ONLY}, + "UTILITY_PROCESSES": {UTILITY_PROCESSES_ONLY}, + "SOCKET_PROCESSES": {SOCKET_PROCESSES_ONLY}, + }, + ), + BlocklistDescriptor( + "Legacy", + ALL_DEFINITION_LISTS, + flagspec={ + "BROWSER_PROCESS": {BROWSER_PROCESS_ONLY}, + "CHILD_PROCESSES": {CHILD_PROCESSES_ONLY}, + "GMPLUGIN_PROCESSES": {GMPLUGIN_PROCESSES_ONLY}, + "GPU_PROCESSES": {GPU_PROCESSES_ONLY}, + "UTILITY_PROCESSES": {UTILITY_PROCESSES_ONLY}, + "SOCKET_PROCESSES": {SOCKET_PROCESSES_ONLY}, + }, + ), + # Roughed-in for the moment; we'll enable this in bug 1238735 + # BlocklistDescriptor('LSP', ALL_DEFINITION_LISTS, outspec=LSP_OUTPUT_SPEC), + BlocklistDescriptor( + "Test", ALL_DEFINITION_LISTS, outspec={"filter": FILTER_TESTS_ONLY} + ), +] + + +class PETimeStamp(object): + def __init__(self, ts): + max_timestamp = (2 ** 32) - 1 + if ts < 0 or ts > max_timestamp: + raise ValueError("Invalid timestamp value") + self._value = ts + + def __str__(self): + return "0x%08XU" % self._value + + +class Version(object): + """Encapsulates a DLL version.""" + + ALL_VERSIONS = 0xFFFFFFFFFFFFFFFF + UNVERSIONED = 0 + + def __init__(self, *args): + """There are multiple ways to construct a Version: + + As a tuple containing four elements (recommended); + As four integral arguments; + As a PETimeStamp; + As a long integer. + + The tuple and list formats require the value of each element to be + between 0 and 0xFFFF, inclusive. + """ + + self._ver = Version.UNVERSIONED + + if len(args) == 1: + if isinstance(args[0], tuple): + self.validate_iterable(args[0]) + + self._ver = "MAKE_VERSION%r" % (args[0],) + elif isinstance(args[0], PETimeStamp): + self._ver = args[0] + else: + self._ver = int(args[0]) + elif len(args) == 4: + self.validate_iterable(args) + + self._ver = "MAKE_VERSION%r" % (tuple(args),) + else: + raise ValueError("Bad arguments to Version constructor") + + def validate_iterable(self, arg): + if len(arg) != 4: + raise ValueError("Versions must be a 4-tuple") + + for component in arg: + if not isinstance(component, int) or component < 0 or component > 0xFFFF: + raise ValueError( + "Each version component must be a 16-bit " "unsigned integer" + ) + + def build_long(self, args): + self.validate_iterable(args) + return ( + (int(args[0]) << 48) + | (int(args[1]) << 32) + | (int(args[2]) << 16) + | int(args[3]) + ) + + def is_timestamp(self): + return isinstance(self._ver, PETimeStamp) + + def __str__(self): + if isinstance(self._ver, int): + if self._ver == Version.ALL_VERSIONS: + return "DllBlockInfo::ALL_VERSIONS" + + if self._ver == Version.UNVERSIONED: + return "DllBlockInfo::UNVERSIONED" + + return "0x%016XULL" % self._ver + + return str(self._ver) + + +class DllBlocklistEntry(object): + TEST_CONDITION = "defined(ENABLE_TESTS)" + + def __init__(self, name, ver, flags=(), **kwargs): + """Positional arguments: + + name -- The leaf name of the DLL. + + ver -- The maximum version to be blocked. NB: The comparison used by the + blocklist is <=, so you should specify the last bad version, as opposed + to the first good version. + + flags -- iterable containing the flags that should be applicable to + this blocklist entry. + + Keyword arguments: + + condition -- a string containing a C++ preprocessor expression. This + condition is written as a condition for an #if/#endif block that is + generated around the entry during output. + """ + + self.check_ascii(name) + self._name = name.lower() + self._ver = Version(ver) + + self._flags = set() + self.add_flags(flags) + if self._ver.is_timestamp(): + self._flags.add(USE_TIMESTAMP) + + self._cond = kwargs.get("condition", set()) + if isinstance(self._cond, str): + self._cond = {self._cond} + + @staticmethod + def check_ascii(name): + try: + # Supported in Python 3.7 + if not name.isascii(): + raise ValueError('DLL name "%s" must be ASCII!' % name) + return + except AttributeError: + pass + + try: + name.encode("ascii") + except UnicodeEncodeError: + raise ValueError('DLL name "%s" must be ASCII!' % name) + + def get_name(self): + return self._name + + def set_condition(self, cond): + self._cond.add(cond) + + def get_condition(self): + if len(self._cond) == 1: + fmt = "{0}" + else: + fmt = "({0})" + + return " && ".join([fmt.format(c) for c in self._cond]) + + def set_is_test(self): + self.set_condition(DllBlocklistEntry.TEST_CONDITION) + + def is_test(self): + return self._cond and DllBlocklistEntry.TEST_CONDITION in self._cond + + def add_flags(self, new_flags): + if isinstance(new_flags, str): + self._flags.add(new_flags) + else: + self._flags.update(new_flags) + + @staticmethod + def get_flag_string(flag): + return "mozilla::DllBlockInfoFlags::" + flag + + def get_flags_list(self): + return self._flags + + def write(self, output, mode): + if self._cond: + print("#if %s" % self.get_condition(), file=output) + + flags_str = "" + + flags = self.get_flags_list() + if flags: + flags_str = ", " + " | ".join(map(self.get_flag_string, flags)) + + entry_str = ' DLL_BLOCKLIST_ENTRY("%s", %s%s)' % ( + self._name, + str(self._ver), + flags_str, + ) + print(entry_str, file=output) + + if self._cond: + print("#endif // %s" % self.get_condition(), file=output) + + +class A11yBlocklistEntry(DllBlocklistEntry): + """Represents a blocklist entry for injected a11y DLLs. This class does + not need to do anything special compared to a DllBlocklistEntry; we just + use this type to distinguish a11y entries from "regular" blocklist entries. + """ + + def __init__(self, name, ver, flags=(), **kwargs): + """These arguments are identical to DllBlocklistEntry.__init__""" + + super(A11yBlocklistEntry, self).__init__(name, ver, flags, **kwargs) + + +class RedirectToNoOpEntryPoint(DllBlocklistEntry): + """Represents a blocklist entry to hook the entrypoint into a function + just returning TRUE to keep a module alive and harmless. + This entry is intended to block a DLL which is injected by IAT patching + which is planted by a kernel callback routine for LoadImage because + blocking such a DLL makes a process fail to launch. + """ + + def __init__(self, name, ver, flags=(), **kwargs): + """These arguments are identical to DllBlocklistEntry.__init__""" + + super(RedirectToNoOpEntryPoint, self).__init__(name, ver, flags, **kwargs) + + def get_flags_list(self): + flags = super(RedirectToNoOpEntryPoint, self).get_flags_list() + # RedirectToNoOpEntryPoint items always include the following flag + flags.add(REDIRECT_TO_NOOP_ENTRYPOINT) + return flags + + +class LspBlocklistEntry(DllBlocklistEntry): + """Represents a blocklist entry for a WinSock Layered Service Provider (LSP).""" + + GUID_UNPACK_FMT_LE = "<IHHBBBBBBBB" + Guids = dict() + + def __init__(self, name, ver, guids, flags=(), **kwargs): + """Positional arguments: + + name -- The leaf name of the DLL. + + ver -- The maximum version to be blocked. NB: The comparison used by the + blocklist is <=, so you should specify the last bad version, as opposed + to the first good version. + + guids -- Either a string or list of strings containing the GUIDs that + uniquely identify the LSP. These GUIDs are generated by the developer of + the LSP and are registered with WinSock alongside the LSP. We record + this GUID as part of the "Winsock_LSP" annotation in our crash reports. + + flags -- iterable containing the flags that should be applicable to + this blocklist entry. + + Keyword arguments: + + condition -- a string containing a C++ preprocessor expression. This + condition is written as a condition for an #if/#endif block that is + generated around the entry during output. + """ + + super(LspBlocklistEntry, self).__init__(name, ver, flags, **kwargs) + if not guids: + raise ValueError("Missing GUID(s)!") + + if isinstance(guids, str): + self.insert(UUID(guids), name) + else: + for guid in guids: + self.insert(UUID(guid), name) + + def insert(self, guid, name): + # Some explanation here: Multiple DLLs (and thus multiple instances of + # LspBlocklistEntry) may share the same GUIDs. To ensure that we do not + # have any duplicates, we store each GUID in a class variable, Guids. + # We include the original DLL name from the blocklist definitions so + # that we may output a comment above each GUID indicating which entries + # correspond to which GUID. + LspBlocklistEntry.Guids.setdefault(guid, []).append(name) + + def get_flags_list(self): + flags = super(LspBlocklistEntry, self).get_flags_list() + # LSP entries always include the following flag + flags.add(SUBSTITUTE_LSP_PASSTHROUGH) + return flags + + @staticmethod + def as_c_struct(guid, names): + parts = unpack(LspBlocklistEntry.GUID_UNPACK_FMT_LE, guid.bytes_le) + str_guid = ( + " // %r\n // {%s}\n { 0x%08x, 0x%04x, 0x%04x, " + "{ 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x }" + " }" + % ( + names, + str(guid), + parts[0], + parts[1], + parts[2], + parts[3], + parts[4], + parts[5], + parts[6], + parts[7], + parts[8], + parts[9], + parts[10], + ) + ) + return str_guid + + def write(self, output, mode): + if mode != LSP_MODE_GUID: + super(LspBlocklistEntry, self).write(output, mode) + return + + # We dump the entire contents of Guids on the first call, and then + # clear it. Remaining invocations of this method are no-ops. + if LspBlocklistEntry.Guids: + result = ",\n".join( + [ + self.as_c_struct(guid, names) + for guid, names in iteritems(LspBlocklistEntry.Guids) + ] + ) + print(result, file=output) + LspBlocklistEntry.Guids.clear() + + +def exec_script_file(script_name, globals): + with open(script_name) as script: + exec(compile(script.read(), script_name, "exec"), globals) + + +def gen_blocklists(first_fd, defs_filename): + + BlocklistDescriptor.set_output_fd(first_fd) + + # exec_env defines the global variables that will be present in the + # execution environment when defs_filename is run by exec. + exec_env = { + # Add the blocklist entry types + "A11yBlocklistEntry": A11yBlocklistEntry, + "DllBlocklistEntry": DllBlocklistEntry, + "LspBlocklistEntry": LspBlocklistEntry, + "RedirectToNoOpEntryPoint": RedirectToNoOpEntryPoint, + # Add the special version types + "ALL_VERSIONS": Version.ALL_VERSIONS, + "UNVERSIONED": Version.UNVERSIONED, + "PETimeStamp": PETimeStamp, + } + + # Import definition lists into exec_env + for defname in ALL_DEFINITION_LISTS: + exec_env[defname] = [] + # For each defname, add a special list for test-only entries + exec_env[derive_test_key(defname)] = [] + + # Import flags into exec_env + exec_env.update({flag: flag for flag in INPUT_ONLY_FLAGS}) + + # Now execute the input script with exec_env providing the globals + exec_script_file(defs_filename, exec_env) + + # Tell the output descriptors to write out the output files. + for desc in GENERATED_BLOCKLIST_FILES: + desc.write(defs_filename, exec_env) diff --git a/toolkit/xre/dllservices/mozglue/interceptor/Arm64.cpp b/toolkit/xre/dllservices/mozglue/interceptor/Arm64.cpp new file mode 100644 index 0000000000..81d8e6d09b --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/Arm64.cpp @@ -0,0 +1,89 @@ +/* -*- 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 "Arm64.h" + +#include "mozilla/ResultVariant.h" + +namespace mozilla { +namespace interceptor { +namespace arm64 { + +struct PCRelativeLoadTest { + // Bitmask to be ANDed with the instruction to isolate the bits that this + // instance is interested in + uint32_t mTestMask; + // The desired bits that we want to see after masking + uint32_t mMatchBits; + // If we match, mDecodeFn provide the code to decode the instruction. + LoadOrBranch (*mDecodeFn)(const uintptr_t aPC, const uint32_t aInst); +}; + +static LoadOrBranch ADRPDecode(const uintptr_t aPC, const uint32_t aInst) { + // Keep in mind that on Windows aarch64, uint32_t is little-endian + const uint32_t kMaskDataProcImmPcRelativeImmLo = 0x60000000; + const uint32_t kMaskDataProcImmPcRelativeImmHi = 0x00FFFFE0; + + uintptr_t base = aPC; + intptr_t offset = SignExtend<intptr_t>( + ((aInst & kMaskDataProcImmPcRelativeImmHi) >> 3) | + ((aInst & kMaskDataProcImmPcRelativeImmLo) >> 29), + 21); + + base &= ~0xFFFULL; + offset <<= 12; + + uint8_t reg = aInst & 0x1F; + + return LoadOrBranch(base + offset, reg); +} + +MFBT_API LoadOrBranch BUncondImmDecode(const uintptr_t aPC, + const uint32_t aInst) { + int32_t offset = SignExtend<int32_t>(aInst & 0x03FFFFFFU, 26); + return LoadOrBranch(aPC + offset); +} + +// Order is important here; more specific encoding tests must be placed before +// less specific encoding tests. +static const PCRelativeLoadTest gPCRelTests[] = { + {0x9FC00000, 0x10000000, nullptr}, // ADR + {0x9FC00000, 0x90000000, &ADRPDecode}, // ADRP + {0xFF000000, 0x58000000, nullptr}, // LDR (literal) 64-bit GPR + {0x3B000000, 0x18000000, nullptr}, // LDR (literal) (remaining forms) + {0x7C000000, 0x14000000, nullptr}, // B (unconditional immediate) + {0xFE000000, 0x54000000, nullptr}, // B.Cond + {0x7E000000, 0x34000000, nullptr}, // Compare and branch (imm) + {0x7E000000, 0x36000000, nullptr}, // Test and branch (imm) + {0xFE000000, 0xD6000000, nullptr} // Unconditional branch (reg) +}; + +/** + * In this function we interate through each entry in |gPCRelTests|, AND + * |aInst| with |test.mTestMask| to isolate the bits that we're interested in, + * then compare that result against |test.mMatchBits|. If we have a match, + * then that particular entry is applicable to |aInst|. If |test.mDecodeFn| is + * present, then we call it to decode the instruction. If it is not present, + * then we assume that this particular instruction is unsupported. + */ +MFBT_API Result<LoadOrBranch, PCRelCheckError> CheckForPCRel( + const uintptr_t aPC, const uint32_t aInst) { + for (auto&& test : gPCRelTests) { + if ((aInst & test.mTestMask) == test.mMatchBits) { + if (!test.mDecodeFn) { + return Err(PCRelCheckError::NoDecoderAvailable); + } + + return test.mDecodeFn(aPC, aInst); + } + } + + return Err(PCRelCheckError::InstructionNotPCRel); +} + +} // namespace arm64 +} // namespace interceptor +} // namespace mozilla diff --git a/toolkit/xre/dllservices/mozglue/interceptor/Arm64.h b/toolkit/xre/dllservices/mozglue/interceptor/Arm64.h new file mode 100644 index 0000000000..4070fbf99f --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/Arm64.h @@ -0,0 +1,221 @@ +/* -*- 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/. */ + +#ifndef mozilla_interceptor_Arm64_h +#define mozilla_interceptor_Arm64_h + +#include <type_traits> + +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "mozilla/Saturate.h" +#include "mozilla/Types.h" + +namespace mozilla { +namespace interceptor { +namespace arm64 { + +// clang-format off +enum class IntegerConditionCode : uint8_t { + // From the ARMv8 Architectural Reference Manual, Section C1.2.4 + // Description Condition Flags + EQ = 0b0000, // == Z == 1 + NE = 0b0001, // != Z == 0 + CS = 0b0010, // carry set C == 1 + HS = 0b0010, // carry set (alias) C == 1 + CC = 0b0011, // carry clear C == 0 + LO = 0b0011, // carry clear (alias) C == 0 + MI = 0b0100, // < 0 N == 1 + PL = 0b0101, // >= 0 N == 0 + VS = 0b0110, // overflow V == 1 + VC = 0b0111, // no overflow V == 0 + HI = 0b1000, // unsigned > C == 1 && Z == 0 + LS = 0b1001, // unsigned <= !(C == 1 && Z == 0) + GE = 0b1010, // signed >= N == V + LT = 0b1011, // signed < N != V + GT = 0b1100, // signed > Z == 0 && N == V + LE = 0b1101, // signed <= !(Z == 0 && N == V) + AL = 0b1110, // unconditional <Any> + NV = 0b1111 // unconditional (but AL is the preferred encoding) +}; +// clang-format on + +struct LoadOrBranch { + enum class Type { + Load, + Branch, + }; + + // Load constructor + LoadOrBranch(const uintptr_t aAbsAddress, const uint8_t aDestReg) + : mType(Type::Load), mAbsAddress(aAbsAddress), mDestReg(aDestReg) { + MOZ_ASSERT(aDestReg < 32); + } + + // Unconditional branch constructor + explicit LoadOrBranch(const uintptr_t aAbsAddress) + : mType(Type::Branch), + mAbsAddress(aAbsAddress), + mCond(IntegerConditionCode::AL) {} + + // Conditional branch constructor + LoadOrBranch(const uintptr_t aAbsAddress, const IntegerConditionCode aCond) + : mType(Type::Branch), mAbsAddress(aAbsAddress), mCond(aCond) {} + + Type mType; + + // The absolute address to be loaded into a register, or branched to + uintptr_t mAbsAddress; + + union { + // The destination register for the load + uint8_t mDestReg; + + // The condition code for the branch + IntegerConditionCode mCond; + }; +}; + +enum class PCRelCheckError { + InstructionNotPCRel, + NoDecoderAvailable, +}; + +MFBT_API Result<LoadOrBranch, PCRelCheckError> CheckForPCRel( + const uintptr_t aPC, const uint32_t aInst); + +/** + * Casts |aValue| to a |ResultT| via sign extension. + * + * This function should be used when extracting signed immediate values from + * an instruction. + * + * @param aValue The value to be sign extended. This value should already be + * isolated from the remainder of the instruction's bits and + * shifted all the way to the right. + * @param aNumValidBits The number of bits in |aValue| that contain the + * immediate signed value, including the sign bit. + */ +template <typename ResultT> +inline ResultT SignExtend(const uint32_t aValue, const uint8_t aNumValidBits) { + static_assert(std::is_integral_v<ResultT> && std::is_signed_v<ResultT>, + "ResultT must be a signed integral type"); + MOZ_ASSERT(aNumValidBits < 32U && aNumValidBits > 1); + + using UnsignedResultT = std::decay_t<std::make_unsigned_t<ResultT>>; + + const uint8_t kResultWidthBits = sizeof(ResultT) * 8; + + // Shift left unsigned + const uint8_t shiftAmt = kResultWidthBits - aNumValidBits; + UnsignedResultT shiftedLeft = static_cast<UnsignedResultT>(aValue) + << shiftAmt; + + // Now shift right signed + auto result = static_cast<ResultT>(shiftedLeft); + result >>= shiftAmt; + + return result; +} + +inline static uint32_t BuildUnconditionalBranchToRegister(const uint32_t aReg) { + MOZ_ASSERT(aReg < 32); + // BR aReg + return 0xD61F0000 | (aReg << 5); +} + +MFBT_API LoadOrBranch BUncondImmDecode(const uintptr_t aPC, + const uint32_t aInst); + +/** + * If |aTarget| is more than 128MB away from |aPC|, we need to use a veneer. + */ +inline static bool IsVeneerRequired(const uintptr_t aPC, + const uintptr_t aTarget) { + detail::Saturate<intptr_t> saturated(aTarget); + saturated -= aPC; + + uintptr_t absDiff = Abs(saturated.value()); + + return absDiff >= 0x08000000U; +} + +inline static bool IsUnconditionalBranchImm(const uint32_t aInst) { + return (aInst & 0xFC000000U) == 0x14000000U; +} + +inline static Maybe<uint32_t> BuildUnconditionalBranchImm( + const uintptr_t aPC, const uintptr_t aTarget) { + detail::Saturate<intptr_t> saturated(aTarget); + saturated -= aPC; + + CheckedInt<int32_t> offset(saturated.value()); + if (!offset.isValid()) { + return Nothing(); + } + + // offset should be a multiple of 4 + MOZ_ASSERT(offset.value() % 4 == 0); + if (offset.value() % 4) { + return Nothing(); + } + + offset /= 4; + if (!offset.isValid()) { + return Nothing(); + } + + uint32_t signbits = static_cast<uint32_t>(offset.value()) & 0xFE000000; + // Ensure that offset is small enough to fit into the 26 bit region. + // We check that the sign bits are either all ones or all zeros. + MOZ_ASSERT(signbits == 0xFE000000 || !signbits); + if (signbits && signbits != 0xFE000000) { + return Nothing(); + } + + uint32_t masked = static_cast<uint32_t>(offset.value()) & 0x03FFFFFF; + + // B imm26 + return Some(0x14000000U | masked); +} + +/** + * Allocate and construct a veneer that provides an absolute 64-bit branch to + * the hook function. + */ +template <typename TrampPoolT> +inline static uintptr_t MakeVeneer(TrampPoolT& aTrampPool, void* aPrimaryTramp, + const uintptr_t aDestAddress) { + auto maybeVeneer = aTrampPool.GetNextTrampoline(); + if (!maybeVeneer) { + return 0; + } + + Trampoline<typename TrampPoolT::MMPolicyT> veneer( + std::move(maybeVeneer.ref())); + + // Write the same header information that is used for trampolines + veneer.WriteEncodedPointer(nullptr); + veneer.WriteEncodedPointer(aPrimaryTramp); + + veneer.StartExecutableCode(); + + // Register 16 is explicitly intended for veneers in ARM64, so we use that + // register without fear of clobbering anything important. + veneer.WriteLoadLiteral(aDestAddress, 16); + veneer.WriteInstruction(BuildUnconditionalBranchToRegister(16)); + + return reinterpret_cast<uintptr_t>(veneer.EndExecutableCode()); +} + +} // namespace arm64 +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_Arm64_h diff --git a/toolkit/xre/dllservices/mozglue/interceptor/MMPolicies.h b/toolkit/xre/dllservices/mozglue/interceptor/MMPolicies.h new file mode 100644 index 0000000000..0a309a1065 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/MMPolicies.h @@ -0,0 +1,1031 @@ +/* -*- 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/. */ + +#ifndef mozilla_interceptor_MMPolicies_h +#define mozilla_interceptor_MMPolicies_h + +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/DynamicallyLinkedFunctionPtr.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/Maybe.h" +#include "mozilla/Span.h" +#include "mozilla/TypedEnumBits.h" +#include "mozilla/Types.h" +#include "mozilla/WindowsMapRemoteView.h" +#include "mozilla/WindowsUnwindInfo.h" + +#include <windows.h> + +#if (NTDDI_VERSION < NTDDI_WIN10_RS4) || defined(__MINGW32__) +PVOID WINAPI VirtualAlloc2(HANDLE Process, PVOID BaseAddress, SIZE_T Size, + ULONG AllocationType, ULONG PageProtection, + MEM_EXTENDED_PARAMETER* ExtendedParameters, + ULONG ParameterCount); +PVOID WINAPI MapViewOfFile3(HANDLE FileMapping, HANDLE Process, + PVOID BaseAddress, ULONG64 Offset, SIZE_T ViewSize, + ULONG AllocationType, ULONG PageProtection, + MEM_EXTENDED_PARAMETER* ExtendedParameters, + ULONG ParameterCount); +#endif // (NTDDI_VERSION < NTDDI_WIN10_RS4) || defined(__MINGW32__) + +// _CRT_RAND_S is not defined everywhere, but we need it. +#if !defined(_CRT_RAND_S) +extern "C" errno_t rand_s(unsigned int* randomValue); +#endif // !defined(_CRT_RAND_S) + +// Declaring only the functions we need in NativeNt.h. To include the entire +// NativeNt.h causes circular dependency. +namespace mozilla { +namespace nt { +SIZE_T WINAPI VirtualQueryEx(HANDLE aProcess, LPCVOID aAddress, + PMEMORY_BASIC_INFORMATION aMemInfo, + SIZE_T aMemInfoLen); + +SIZE_T WINAPI VirtualQuery(LPCVOID aAddress, PMEMORY_BASIC_INFORMATION aMemInfo, + SIZE_T aMemInfoLen); +} // namespace nt +} // namespace mozilla + +namespace mozilla { +namespace interceptor { + +// This class implements memory operations not involving any kernel32's +// functions, so that derived classes can use them. +class MOZ_TRIVIAL_CTOR_DTOR MMPolicyInProcessPrimitive { + protected: + bool ProtectInternal(decltype(&::VirtualProtect) aVirtualProtect, + void* aVAddress, size_t aSize, uint32_t aProtFlags, + uint32_t* aPrevProtFlags) const { + MOZ_ASSERT(aPrevProtFlags); + BOOL ok = aVirtualProtect(aVAddress, aSize, aProtFlags, + reinterpret_cast<PDWORD>(aPrevProtFlags)); + if (!ok && aPrevProtFlags) { + // VirtualProtect can fail but still set valid protection flags. + // Let's clear those upon failure. + *aPrevProtFlags = 0; + } + + return !!ok; + } + + public: + bool Read(void* aToPtr, const void* aFromPtr, size_t aLen) const { + ::memcpy(aToPtr, aFromPtr, aLen); + return true; + } + + bool Write(void* aToPtr, const void* aFromPtr, size_t aLen) const { + ::memcpy(aToPtr, aFromPtr, aLen); + return true; + } + + /** + * @return true if the page that hosts aVAddress is accessible. + */ + bool IsPageAccessible(uintptr_t aVAddress) const { + MEMORY_BASIC_INFORMATION mbi; + SIZE_T result = nt::VirtualQuery(reinterpret_cast<LPCVOID>(aVAddress), &mbi, + sizeof(mbi)); + + return result && mbi.AllocationProtect && mbi.State == MEM_COMMIT && + mbi.Protect != PAGE_NOACCESS; + } +}; + +class MOZ_TRIVIAL_CTOR_DTOR MMPolicyBase { + protected: + static uintptr_t AlignDown(const uintptr_t aUnaligned, + const uintptr_t aAlignTo) { + MOZ_ASSERT(IsPowerOfTwo(aAlignTo)); +#pragma warning(suppress : 4146) + return aUnaligned & (-aAlignTo); + } + + static uintptr_t AlignUp(const uintptr_t aUnaligned, + const uintptr_t aAlignTo) { + MOZ_ASSERT(IsPowerOfTwo(aAlignTo)); +#pragma warning(suppress : 4146) + return aUnaligned + ((-aUnaligned) & (aAlignTo - 1)); + } + + static PVOID AlignUpToRegion(PVOID aUnaligned, uintptr_t aAlignTo, + size_t aLen, size_t aDesiredLen) { + uintptr_t unaligned = reinterpret_cast<uintptr_t>(aUnaligned); + uintptr_t aligned = AlignUp(unaligned, aAlignTo); + MOZ_ASSERT(aligned >= unaligned); + + if (aLen < aligned - unaligned) { + return nullptr; + } + + aLen -= (aligned - unaligned); + return reinterpret_cast<PVOID>((aLen >= aDesiredLen) ? aligned : 0); + } + + public: +#if defined(NIGHTLY_BUILD) + Maybe<DetourError> mLastError; + const Maybe<DetourError>& GetLastDetourError() const { return mLastError; } + template <typename... Args> + void SetLastDetourError(Args&&... aArgs) { + mLastError = Some(DetourError(std::forward<Args>(aArgs)...)); + } +#else + template <typename... Args> + void SetLastDetourError(Args&&... aArgs) {} +#endif // defined(NIGHTLY_BUILD) + + DWORD ComputeAllocationSize(const uint32_t aRequestedSize) const { + MOZ_ASSERT(aRequestedSize); + DWORD result = aRequestedSize; + + const uint32_t granularity = GetAllocGranularity(); + + uint32_t mod = aRequestedSize % granularity; + if (mod) { + result += (granularity - mod); + } + + return result; + } + + DWORD GetAllocGranularity() const { + static const DWORD kAllocGranularity = []() -> DWORD { + SYSTEM_INFO sysInfo; + ::GetSystemInfo(&sysInfo); + return sysInfo.dwAllocationGranularity; + }(); + + return kAllocGranularity; + } + + DWORD GetPageSize() const { + static const DWORD kPageSize = []() -> DWORD { + SYSTEM_INFO sysInfo; + ::GetSystemInfo(&sysInfo); + return sysInfo.dwPageSize; + }(); + + return kPageSize; + } + + uintptr_t GetMaxUserModeAddress() const { + static const uintptr_t kMaxUserModeAddr = []() -> uintptr_t { + SYSTEM_INFO sysInfo; + ::GetSystemInfo(&sysInfo); + return reinterpret_cast<uintptr_t>(sysInfo.lpMaximumApplicationAddress); + }(); + + return kMaxUserModeAddr; + } + + static const uint8_t* GetLowerBound(const Span<const uint8_t>& aBounds) { + return &(*aBounds.cbegin()); + } + + static const uint8_t* GetUpperBoundIncl(const Span<const uint8_t>& aBounds) { + // We return an upper bound that is inclusive. + return &(*(aBounds.cend() - 1)); + } + + static const uint8_t* GetUpperBoundExcl(const Span<const uint8_t>& aBounds) { + // We return an upper bound that is exclusive by adding 1 to the inclusive + // upper bound. + return GetUpperBoundIncl(aBounds) + 1; + } + + /** + * It is convenient for us to provide address range information based on a + * "pivot" and a distance from that pivot, as branch instructions operate + * within a range of the program counter. OTOH, to actually manage the + * regions of memory, it is easier to think about them in terms of their + * lower and upper bounds. This function converts from the former format to + * the latter format. + */ + Maybe<Span<const uint8_t>> SpanFromPivotAndDistance( + const uint32_t aSize, const uintptr_t aPivotAddr, + const uint32_t aMaxDistanceFromPivot) const { + if (!aPivotAddr || !aMaxDistanceFromPivot) { + return Nothing(); + } + + // We don't allow regions below 1MB so that we're not allocating near any + // sensitive areas in our address space. + const uintptr_t kMinAllowableAddress = 0x100000; + + const uintptr_t kGranularity(GetAllocGranularity()); + + // We subtract the max distance from the pivot to determine our lower bound. + CheckedInt<uintptr_t> lowerBound(aPivotAddr); + lowerBound -= aMaxDistanceFromPivot; + if (lowerBound.isValid()) { + // In this case, the subtraction has not underflowed, but we still want + // the lower bound to be at least kMinAllowableAddress. + lowerBound = std::max(lowerBound.value(), kMinAllowableAddress); + } else { + // In this case, we underflowed. Forcibly set the lower bound to + // kMinAllowableAddress. + lowerBound = CheckedInt<uintptr_t>(kMinAllowableAddress); + } + + // Align up to the next unit of allocation granularity when necessary. + lowerBound = AlignUp(lowerBound.value(), kGranularity); + MOZ_ASSERT(lowerBound.isValid()); + if (!lowerBound.isValid()) { + return Nothing(); + } + + // We must ensure that our region is below the maximum allowable user-mode + // address, or our reservation will fail. + const uintptr_t kMaxUserModeAddr = GetMaxUserModeAddress(); + + // We add the max distance from the pivot to determine our upper bound. + CheckedInt<uintptr_t> upperBound(aPivotAddr); + upperBound += aMaxDistanceFromPivot; + if (upperBound.isValid()) { + // In this case, the addition has not overflowed, but we still want + // the upper bound to be at most kMaxUserModeAddr. + upperBound = std::min(upperBound.value(), kMaxUserModeAddr); + } else { + // In this case, we overflowed. Forcibly set the upper bound to + // kMaxUserModeAddr. + upperBound = CheckedInt<uintptr_t>(kMaxUserModeAddr); + } + + // Subtract the desired allocation size so that any chunk allocated in the + // region will be reachable. + upperBound -= aSize; + if (!upperBound.isValid()) { + return Nothing(); + } + + // Align down to the next unit of allocation granularity when necessary. + upperBound = AlignDown(upperBound.value(), kGranularity); + if (!upperBound.isValid()) { + return Nothing(); + } + + MOZ_ASSERT(lowerBound.value() < upperBound.value()); + if (lowerBound.value() >= upperBound.value()) { + return Nothing(); + } + + // Return the result as a Span + return Some(Span(reinterpret_cast<const uint8_t*>(lowerBound.value()), + upperBound.value() - lowerBound.value())); + } + + /** + * This function locates a virtual memory region of |aDesiredBytesLen| that + * resides in the interval [aRangeMin, aRangeMax). We do this by scanning the + * virtual memory space for a block of unallocated memory that is sufficiently + * large. + */ + PVOID FindRegion(HANDLE aProcess, const size_t aDesiredBytesLen, + const uint8_t* aRangeMin, const uint8_t* aRangeMax) { + // Convert the given pointers to uintptr_t because we should not + // compare two pointers unless they are from the same array or object. + uintptr_t rangeMin = reinterpret_cast<uintptr_t>(aRangeMin); + uintptr_t rangeMax = reinterpret_cast<uintptr_t>(aRangeMax); + + const DWORD kGranularity = GetAllocGranularity(); + if (!aDesiredBytesLen) { + SetLastDetourError(MMPOLICY_RESERVE_FINDREGION_INVALIDLEN); + return nullptr; + } + + MOZ_ASSERT(rangeMin < rangeMax); + if (rangeMin >= rangeMax) { + SetLastDetourError(MMPOLICY_RESERVE_FINDREGION_INVALIDRANGE); + return nullptr; + } + + // Generate a randomized base address that falls within the interval + // [aRangeMin, aRangeMax - aDesiredBytesLen] + unsigned int rnd = 0; + rand_s(&rnd); + + // Reduce rnd to a value that falls within the acceptable range + uintptr_t maxOffset = + (rangeMax - rangeMin - aDesiredBytesLen) / kGranularity; + // Divide by maxOffset + 1 because maxOffset * kGranularity is acceptable. + uintptr_t offset = (uintptr_t(rnd) % (maxOffset + 1)) * kGranularity; + + // Start searching at this address + const uintptr_t searchStart = rangeMin + offset; + // The max address needs to incorporate the desired length + const uintptr_t kMaxPtr = rangeMax - aDesiredBytesLen; + + MOZ_DIAGNOSTIC_ASSERT(searchStart <= kMaxPtr); + + MEMORY_BASIC_INFORMATION mbi; + SIZE_T len = sizeof(mbi); + + // Scan the range for a free chunk that is at least as large as + // aDesiredBytesLen + // Scan [searchStart, kMaxPtr] + for (uintptr_t address = searchStart; address <= kMaxPtr;) { + if (nt::VirtualQueryEx(aProcess, reinterpret_cast<uint8_t*>(address), + &mbi, len) != len) { + SetLastDetourError(MMPOLICY_RESERVE_FINDREGION_VIRTUALQUERY_ERROR, + ::GetLastError()); + return nullptr; + } + + if (mbi.State == MEM_FREE) { + // |mbi.BaseAddress| is aligned with the page granularity, but may not + // be aligned with the allocation granularity. VirtualAlloc does not + // accept such a non-aligned address unless the corresponding allocation + // region is free. So we get the next boundary's start address. + PVOID regionStart = AlignUpToRegion(mbi.BaseAddress, kGranularity, + mbi.RegionSize, aDesiredBytesLen); + if (regionStart) { + return regionStart; + } + } + + address = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize; + } + + // Scan [aRangeMin, searchStart) + for (uintptr_t address = rangeMin; address < searchStart;) { + if (nt::VirtualQueryEx(aProcess, reinterpret_cast<uint8_t*>(address), + &mbi, len) != len) { + SetLastDetourError(MMPOLICY_RESERVE_FINDREGION_VIRTUALQUERY_ERROR, + ::GetLastError()); + return nullptr; + } + + if (mbi.State == MEM_FREE) { + PVOID regionStart = AlignUpToRegion(mbi.BaseAddress, kGranularity, + mbi.RegionSize, aDesiredBytesLen); + if (regionStart) { + return regionStart; + } + } + + address = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize; + } + + SetLastDetourError(MMPOLICY_RESERVE_FINDREGION_NO_FREE_REGION, + ::GetLastError()); + return nullptr; + } + + /** + * This function reserves a |aSize| block of virtual memory. + * + * When |aBounds| is Nothing, it just calls |aReserveFn| and lets Windows + * choose the base address. + * + * Otherwise, it tries to call |aReserveRangeFn| to reserve the memory within + * the bounds provided by |aBounds|. It is advantageous to use this function + * because the OS's VM manager has better information as to which base + * addresses are the best to use. + * + * If |aReserveRangeFn| retuns Nothing, this means that the platform support + * is not available. In that case, we fall back to manually computing a region + * to use for reserving the memory by calling |FindRegion|. + */ + template <typename ReserveFnT, typename ReserveRangeFnT> + PVOID Reserve(HANDLE aProcess, const uint32_t aSize, + const ReserveFnT& aReserveFn, + const ReserveRangeFnT& aReserveRangeFn, + const Maybe<Span<const uint8_t>>& aBounds) { + if (!aBounds) { + // No restrictions, let the OS choose the base address + PVOID ret = aReserveFn(aProcess, nullptr, aSize); + if (!ret) { + SetLastDetourError(MMPOLICY_RESERVE_NOBOUND_RESERVE_ERROR, + ::GetLastError()); + } + return ret; + } + + const uint8_t* lowerBound = GetLowerBound(aBounds.ref()); + const uint8_t* upperBoundExcl = GetUpperBoundExcl(aBounds.ref()); + + Maybe<PVOID> result = + aReserveRangeFn(aProcess, aSize, lowerBound, upperBoundExcl); + if (result) { + return result.value(); + } + + // aReserveRangeFn is not available on this machine. We'll do a manual + // search. + + size_t curAttempt = 0; + const size_t kMaxAttempts = 8; + + // We loop here because |FindRegion| may return a base address that + // is reserved elsewhere before we have had a chance to reserve it + // ourselves. + while (curAttempt < kMaxAttempts) { + PVOID base = FindRegion(aProcess, aSize, lowerBound, upperBoundExcl); + if (!base) { + return nullptr; + } + + result = Some(aReserveFn(aProcess, base, aSize)); + if (result.value()) { + return result.value(); + } + + ++curAttempt; + } + + // If we run out of attempts, we fall through to the default case where + // the system chooses any base address it wants. In that case, the hook + // will be set on a best-effort basis. + PVOID ret = aReserveFn(aProcess, nullptr, aSize); + if (!ret) { + SetLastDetourError(MMPOLICY_RESERVE_FINAL_RESERVE_ERROR, + ::GetLastError()); + } + return ret; + } +}; + +class MOZ_TRIVIAL_CTOR_DTOR MMPolicyInProcess + : public MMPolicyInProcessPrimitive, + public MMPolicyBase { + public: + typedef MMPolicyInProcess MMPolicyT; + + constexpr MMPolicyInProcess() + : mBase(nullptr), mReservationSize(0), mCommitOffset(0) {} + + MMPolicyInProcess(const MMPolicyInProcess&) = delete; + MMPolicyInProcess& operator=(const MMPolicyInProcess&) = delete; + + MMPolicyInProcess(MMPolicyInProcess&& aOther) + : mBase(nullptr), mReservationSize(0), mCommitOffset(0) { + *this = std::move(aOther); + } + + MMPolicyInProcess& operator=(MMPolicyInProcess&& aOther) { + mBase = aOther.mBase; + aOther.mBase = nullptr; + + mCommitOffset = aOther.mCommitOffset; + aOther.mCommitOffset = 0; + + mReservationSize = aOther.mReservationSize; + aOther.mReservationSize = 0; + + return *this; + } + + explicit operator bool() const { return !!mBase; } + + /** + * Should we unhook everything upon destruction? + */ + bool ShouldUnhookUponDestruction() const { return true; } + +#if defined(_M_IX86) + bool WriteAtomic(void* aDestPtr, const uint16_t aValue) const { + *static_cast<uint16_t*>(aDestPtr) = aValue; + return true; + } +#endif // defined(_M_IX86) + + bool Protect(void* aVAddress, size_t aSize, uint32_t aProtFlags, + uint32_t* aPrevProtFlags) const { + return ProtectInternal(::VirtualProtect, aVAddress, aSize, aProtFlags, + aPrevProtFlags); + } + + bool FlushInstructionCache() const { + return !!::FlushInstructionCache(::GetCurrentProcess(), nullptr, 0); + } + + static DWORD GetTrampWriteProtFlags() { return PAGE_EXECUTE_READWRITE; } + +#if defined(_M_X64) + bool IsTrampolineSpaceInLowest2GB() const { + return (mBase + mReservationSize) <= + reinterpret_cast<uint8_t*>(0x0000000080000000ULL); + } + + static constexpr bool kSupportsUnwindInfo = true; + + mozilla::UniquePtr<uint8_t[]> LookupUnwindInfo( + uintptr_t aOrigFuncAddr, uint32_t* aOffsetFromBeginAddr, + uint32_t* aOffsetToEndAddr, uintptr_t* aOrigImageBase) const { + DWORD64 origImageBase = 0; + auto origFuncEntry = + RtlLookupFunctionEntry(aOrigFuncAddr, &origImageBase, nullptr); + if (!origFuncEntry) { + return nullptr; + } + + if (aOffsetFromBeginAddr) { + *aOffsetFromBeginAddr = + aOrigFuncAddr - (origImageBase + origFuncEntry->BeginAddress); + } + if (aOffsetToEndAddr) { + *aOffsetToEndAddr = + (origImageBase + origFuncEntry->EndAddress) - aOrigFuncAddr; + } + if (aOrigImageBase) { + *aOrigImageBase = origImageBase; + } + return reinterpret_cast<const UnwindInfo*>(origImageBase + + origFuncEntry->UnwindData) + ->Copy(); + } + + bool AddFunctionTable(uintptr_t aFunctionTable, uint32_t aEntryCount, + uintptr_t aBaseAddress) const { + return bool( + RtlAddFunctionTable(reinterpret_cast<PRUNTIME_FUNCTION>(aFunctionTable), + aEntryCount, aBaseAddress)); + } +#endif // defined(_M_X64) + + protected: + uint8_t* GetLocalView() const { return mBase; } + + uintptr_t GetRemoteView() const { + // Same as local view for in-process + return reinterpret_cast<uintptr_t>(mBase); + } + + /** + * @return the effective number of bytes reserved, or 0 on failure + */ + uint32_t Reserve(const uint32_t aSize, + const Maybe<Span<const uint8_t>>& aBounds) { + if (!aSize) { + return 0; + } + + if (mBase) { + MOZ_ASSERT(mReservationSize >= aSize); + return mReservationSize; + } + + mReservationSize = ComputeAllocationSize(aSize); + + auto reserveFn = [](HANDLE aProcess, PVOID aBase, uint32_t aSize) -> PVOID { + return ::VirtualAlloc(aBase, aSize, MEM_RESERVE, PAGE_NOACCESS); + }; + + auto reserveWithinRangeFn = + [](HANDLE aProcess, uint32_t aSize, const uint8_t* aRangeMin, + const uint8_t* aRangeMaxExcl) -> Maybe<PVOID> { + static const StaticDynamicallyLinkedFunctionPtr< + decltype(&::VirtualAlloc2)> + pVirtualAlloc2(L"kernelbase.dll", "VirtualAlloc2"); + if (!pVirtualAlloc2) { + return Nothing(); + } + + // NB: MEM_ADDRESS_REQUIREMENTS::HighestEndingAddress is *inclusive* + MEM_ADDRESS_REQUIREMENTS memReq = { + const_cast<uint8_t*>(aRangeMin), + const_cast<uint8_t*>(aRangeMaxExcl - 1)}; + + MEM_EXTENDED_PARAMETER memParam = {}; + memParam.Type = MemExtendedParameterAddressRequirements; + memParam.Pointer = &memReq; + + return Some(pVirtualAlloc2(aProcess, nullptr, aSize, MEM_RESERVE, + PAGE_NOACCESS, &memParam, 1)); + }; + + mBase = static_cast<uint8_t*>( + MMPolicyBase::Reserve(::GetCurrentProcess(), mReservationSize, + reserveFn, reserveWithinRangeFn, aBounds)); + + if (!mBase) { + return 0; + } + + return mReservationSize; + } + + bool MaybeCommitNextPage(const uint32_t aRequestedOffset, + const uint32_t aRequestedLength) { + if (!(*this)) { + return false; + } + + uint32_t limit = aRequestedOffset + aRequestedLength - 1; + if (limit < mCommitOffset) { + // No commit required + return true; + } + + MOZ_DIAGNOSTIC_ASSERT(mCommitOffset < mReservationSize); + if (mCommitOffset >= mReservationSize) { + return false; + } + + PVOID local = ::VirtualAlloc(mBase + mCommitOffset, GetPageSize(), + MEM_COMMIT, PAGE_EXECUTE_READ); + if (!local) { + return false; + } + + mCommitOffset += GetPageSize(); + return true; + } + + private: + uint8_t* mBase; + uint32_t mReservationSize; + uint32_t mCommitOffset; +}; + +// This class manages in-process memory access without using functions +// imported from kernel32.dll. Instead, it uses functions in its own +// function table that are provided from outside. +class MMPolicyInProcessEarlyStage : public MMPolicyInProcessPrimitive { + public: + struct Kernel32Exports { + decltype(&::FlushInstructionCache) mFlushInstructionCache; + decltype(&::GetModuleHandleW) mGetModuleHandleW; + decltype(&::GetSystemInfo) mGetSystemInfo; + decltype(&::VirtualProtect) mVirtualProtect; + }; + + private: + static DWORD GetPageSize(const Kernel32Exports& aK32Exports) { + SYSTEM_INFO sysInfo; + aK32Exports.mGetSystemInfo(&sysInfo); + return sysInfo.dwPageSize; + } + + const Kernel32Exports& mK32Exports; + const DWORD mPageSize; + + public: + explicit MMPolicyInProcessEarlyStage(const Kernel32Exports& aK32Exports) + : mK32Exports(aK32Exports), mPageSize(GetPageSize(mK32Exports)) {} + + // The pattern of constructing a local static variable with a lambda, + // which can be seen in MMPolicyBase, is compiled into code with the + // critical section APIs like EnterCriticalSection imported from kernel32.dll. + // Because this class needs to be able to run in a process's early stage + // when IAT is not yet resolved, we cannot use that patten, thus simply + // caching a value as a local member in the class. + DWORD GetPageSize() const { return mPageSize; } + + bool Protect(void* aVAddress, size_t aSize, uint32_t aProtFlags, + uint32_t* aPrevProtFlags) const { + return ProtectInternal(mK32Exports.mVirtualProtect, aVAddress, aSize, + aProtFlags, aPrevProtFlags); + } + + bool FlushInstructionCache() const { + const HANDLE kCurrentProcess = reinterpret_cast<HANDLE>(-1); + return !!mK32Exports.mFlushInstructionCache(kCurrentProcess, nullptr, 0); + } +}; + +class MMPolicyOutOfProcess : public MMPolicyBase { + public: + typedef MMPolicyOutOfProcess MMPolicyT; + + explicit MMPolicyOutOfProcess(HANDLE aProcess) + : mProcess(nullptr), + mMapping(nullptr), + mLocalView(nullptr), + mRemoteView(nullptr), + mReservationSize(0), + mCommitOffset(0) { + MOZ_ASSERT(aProcess); + ::DuplicateHandle(::GetCurrentProcess(), aProcess, ::GetCurrentProcess(), + &mProcess, kAccessFlags, FALSE, 0); + MOZ_ASSERT(mProcess); + } + + explicit MMPolicyOutOfProcess(DWORD aPid) + : mProcess(::OpenProcess(kAccessFlags, FALSE, aPid)), + mMapping(nullptr), + mLocalView(nullptr), + mRemoteView(nullptr), + mReservationSize(0), + mCommitOffset(0) { + MOZ_ASSERT(mProcess); + } + + ~MMPolicyOutOfProcess() { Destroy(); } + + MMPolicyOutOfProcess(MMPolicyOutOfProcess&& aOther) + : mProcess(nullptr), + mMapping(nullptr), + mLocalView(nullptr), + mRemoteView(nullptr), + mReservationSize(0), + mCommitOffset(0) { + *this = std::move(aOther); + } + + MMPolicyOutOfProcess(const MMPolicyOutOfProcess& aOther) = delete; + MMPolicyOutOfProcess& operator=(const MMPolicyOutOfProcess&) = delete; + + MMPolicyOutOfProcess& operator=(MMPolicyOutOfProcess&& aOther) { + Destroy(); + + mProcess = aOther.mProcess; + aOther.mProcess = nullptr; + + mMapping = aOther.mMapping; + aOther.mMapping = nullptr; + + mLocalView = aOther.mLocalView; + aOther.mLocalView = nullptr; + + mRemoteView = aOther.mRemoteView; + aOther.mRemoteView = nullptr; + + mReservationSize = aOther.mReservationSize; + aOther.mReservationSize = 0; + + mCommitOffset = aOther.mCommitOffset; + aOther.mCommitOffset = 0; + + return *this; + } + + explicit operator bool() const { + return mProcess && mMapping && mLocalView && mRemoteView; + } + + bool ShouldUnhookUponDestruction() const { + // We don't clean up hooks for remote processes; they are expected to + // outlive our process. + return false; + } + + // This function reads as many bytes as |aLen| from the target process and + // succeeds only when the entire area to be read is accessible. + bool Read(void* aToPtr, const void* aFromPtr, size_t aLen) const { + MOZ_ASSERT(mProcess); + if (!mProcess) { + return false; + } + + SIZE_T numBytes = 0; + BOOL ok = ::ReadProcessMemory(mProcess, aFromPtr, aToPtr, aLen, &numBytes); + return ok && numBytes == aLen; + } + + // This function reads as many bytes as possible from the target process up + // to |aLen| bytes and returns the number of bytes which was actually read. + size_t TryRead(void* aToPtr, const void* aFromPtr, size_t aLen) const { + MOZ_ASSERT(mProcess); + if (!mProcess) { + return 0; + } + + uint32_t pageSize = GetPageSize(); + uintptr_t pageMask = pageSize - 1; + + auto rangeStart = reinterpret_cast<uintptr_t>(aFromPtr); + auto rangeEnd = rangeStart + aLen; + + while (rangeStart < rangeEnd) { + SIZE_T numBytes = 0; + BOOL ok = ::ReadProcessMemory(mProcess, aFromPtr, aToPtr, + rangeEnd - rangeStart, &numBytes); + if (ok) { + return numBytes; + } + + // If ReadProcessMemory fails, try to read up to each page boundary from + // the end of the requested area one by one. + if (rangeEnd & pageMask) { + rangeEnd &= ~pageMask; + } else { + rangeEnd -= pageSize; + } + } + + return 0; + } + + bool Write(void* aToPtr, const void* aFromPtr, size_t aLen) const { + MOZ_ASSERT(mProcess); + if (!mProcess) { + return false; + } + + SIZE_T numBytes = 0; + BOOL ok = ::WriteProcessMemory(mProcess, aToPtr, aFromPtr, aLen, &numBytes); + return ok && numBytes == aLen; + } + + bool Protect(void* aVAddress, size_t aSize, uint32_t aProtFlags, + uint32_t* aPrevProtFlags) const { + MOZ_ASSERT(mProcess); + if (!mProcess) { + return false; + } + + MOZ_ASSERT(aPrevProtFlags); + BOOL ok = ::VirtualProtectEx(mProcess, aVAddress, aSize, aProtFlags, + reinterpret_cast<PDWORD>(aPrevProtFlags)); + if (!ok && aPrevProtFlags) { + // VirtualProtectEx can fail but still set valid protection flags. + // Let's clear those upon failure. + *aPrevProtFlags = 0; + } + + return !!ok; + } + + /** + * @return true if the page that hosts aVAddress is accessible. + */ + bool IsPageAccessible(uintptr_t aVAddress) const { + MEMORY_BASIC_INFORMATION mbi; + SIZE_T result = nt::VirtualQueryEx( + mProcess, reinterpret_cast<LPCVOID>(aVAddress), &mbi, sizeof(mbi)); + + return result && mbi.AllocationProtect && mbi.State == MEM_COMMIT && + mbi.Protect != PAGE_NOACCESS; + } + + bool FlushInstructionCache() const { + return !!::FlushInstructionCache(mProcess, nullptr, 0); + } + + static DWORD GetTrampWriteProtFlags() { return PAGE_READWRITE; } + +#if defined(_M_X64) + bool IsTrampolineSpaceInLowest2GB() const { + return (GetRemoteView() + mReservationSize) <= 0x0000000080000000ULL; + } + + // TODO: We should also implement unwind info for our out-of-process policy. + static constexpr bool kSupportsUnwindInfo = false; + + inline mozilla::UniquePtr<uint8_t[]> LookupUnwindInfo( + uintptr_t aOrigFuncAddr, uint32_t* aOffsetFromBeginAddr, + uint32_t* aOffsetToEndAddr, uintptr_t* aOrigImageBase) const { + return nullptr; + } + + inline bool AddFunctionTable(uintptr_t aNewTable, uint32_t aEntryCount, + uintptr_t aBaseAddress) const { + return false; + } +#endif // defined(_M_X64) + + protected: + uint8_t* GetLocalView() const { return mLocalView; } + + uintptr_t GetRemoteView() const { + return reinterpret_cast<uintptr_t>(mRemoteView); + } + + /** + * @return the effective number of bytes reserved, or 0 on failure + */ + uint32_t Reserve(const uint32_t aSize, + const Maybe<Span<const uint8_t>>& aBounds) { + if (!aSize || !mProcess) { + SetLastDetourError(MMPOLICY_RESERVE_INVALIDARG); + return 0; + } + + if (mRemoteView) { + MOZ_ASSERT(mReservationSize >= aSize); + SetLastDetourError(MMPOLICY_RESERVE_ZERO_RESERVATIONSIZE); + return mReservationSize; + } + + mReservationSize = ComputeAllocationSize(aSize); + + mMapping = ::CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, + PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, + mReservationSize, nullptr); + if (!mMapping) { + SetLastDetourError(MMPOLICY_RESERVE_CREATEFILEMAPPING, ::GetLastError()); + return 0; + } + + mLocalView = static_cast<uint8_t*>( + ::MapViewOfFile(mMapping, FILE_MAP_WRITE, 0, 0, 0)); + if (!mLocalView) { + SetLastDetourError(MMPOLICY_RESERVE_MAPVIEWOFFILE, ::GetLastError()); + return 0; + } + + auto reserveFn = [mapping = mMapping](HANDLE aProcess, PVOID aBase, + uint32_t aSize) -> PVOID { + return mozilla::MapRemoteViewOfFile(mapping, aProcess, 0ULL, aBase, 0, 0, + PAGE_EXECUTE_READ); + }; + + auto reserveWithinRangeFn = + [mapping = mMapping](HANDLE aProcess, uint32_t aSize, + const uint8_t* aRangeMin, + const uint8_t* aRangeMaxExcl) -> Maybe<PVOID> { + static const StaticDynamicallyLinkedFunctionPtr< + decltype(&::MapViewOfFile3)> + pMapViewOfFile3(L"kernelbase.dll", "MapViewOfFile3"); + if (!pMapViewOfFile3) { + return Nothing(); + } + + // NB: MEM_ADDRESS_REQUIREMENTS::HighestEndingAddress is *inclusive* + MEM_ADDRESS_REQUIREMENTS memReq = { + const_cast<uint8_t*>(aRangeMin), + const_cast<uint8_t*>(aRangeMaxExcl - 1)}; + + MEM_EXTENDED_PARAMETER memParam = {}; + memParam.Type = MemExtendedParameterAddressRequirements; + memParam.Pointer = &memReq; + + return Some(pMapViewOfFile3(mapping, aProcess, nullptr, 0, aSize, 0, + PAGE_EXECUTE_READ, &memParam, 1)); + }; + + mRemoteView = MMPolicyBase::Reserve(mProcess, mReservationSize, reserveFn, + reserveWithinRangeFn, aBounds); + if (!mRemoteView) { + return 0; + } + + return mReservationSize; + } + + bool MaybeCommitNextPage(const uint32_t aRequestedOffset, + const uint32_t aRequestedLength) { + if (!(*this)) { + return false; + } + + uint32_t limit = aRequestedOffset + aRequestedLength - 1; + if (limit < mCommitOffset) { + // No commit required + return true; + } + + MOZ_DIAGNOSTIC_ASSERT(mCommitOffset < mReservationSize); + if (mCommitOffset >= mReservationSize) { + return false; + } + + PVOID local = ::VirtualAlloc(mLocalView + mCommitOffset, GetPageSize(), + MEM_COMMIT, PAGE_READWRITE); + if (!local) { + return false; + } + + PVOID remote = ::VirtualAllocEx( + mProcess, static_cast<uint8_t*>(mRemoteView) + mCommitOffset, + GetPageSize(), MEM_COMMIT, PAGE_EXECUTE_READ); + if (!remote) { + return false; + } + + mCommitOffset += GetPageSize(); + return true; + } + + private: + void Destroy() { + // We always leak the remote view + if (mLocalView) { + ::UnmapViewOfFile(mLocalView); + mLocalView = nullptr; + } + + if (mMapping) { + ::CloseHandle(mMapping); + mMapping = nullptr; + } + + if (mProcess) { + ::CloseHandle(mProcess); + mProcess = nullptr; + } + } + + private: + HANDLE mProcess; + HANDLE mMapping; + uint8_t* mLocalView; + PVOID mRemoteView; + uint32_t mReservationSize; + uint32_t mCommitOffset; + + static const DWORD kAccessFlags = PROCESS_QUERY_INFORMATION | + PROCESS_VM_OPERATION | PROCESS_VM_READ | + PROCESS_VM_WRITE; +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_MMPolicies_h diff --git a/toolkit/xre/dllservices/mozglue/interceptor/PatcherBase.h b/toolkit/xre/dllservices/mozglue/interceptor/PatcherBase.h new file mode 100644 index 0000000000..e39a38fafd --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/PatcherBase.h @@ -0,0 +1,141 @@ +/* -*- 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/. */ + +#ifndef mozilla_interceptor_PatcherBase_h +#define mozilla_interceptor_PatcherBase_h + +#include "mozilla/interceptor/TargetFunction.h" + +namespace mozilla { +namespace interceptor { + +template <typename MMPolicy> +struct GetProcAddressSelector; + +template <> +struct GetProcAddressSelector<MMPolicyOutOfProcess> { + FARPROC operator()(HMODULE aModule, const char* aName, + const MMPolicyOutOfProcess& aMMPolicy) const { + auto exportSection = + mozilla::nt::PEExportSection<MMPolicyOutOfProcess>::Get(aModule, + aMMPolicy); + return exportSection.GetProcAddress(aName); + } +}; + +template <> +struct GetProcAddressSelector<MMPolicyInProcess> { + FARPROC operator()(HMODULE aModule, const char* aName, + const MMPolicyInProcess&) const { + // PEExportSection works for MMPolicyInProcess, too, but the native + // GetProcAddress is still better because PEExportSection does not + // solve a forwarded entry. + return ::GetProcAddress(aModule, aName); + } +}; + +template <typename VMPolicy> +class WindowsDllPatcherBase { + protected: + typedef typename VMPolicy::MMPolicyT MMPolicyT; + + template <typename... Args> + explicit WindowsDllPatcherBase(Args&&... aArgs) + : mVMPolicy(std::forward<Args>(aArgs)...) {} + + ReadOnlyTargetFunction<MMPolicyT> ResolveRedirectedAddress( + FARPROC aOriginalFunction) { + uintptr_t currAddr = reinterpret_cast<uintptr_t>(aOriginalFunction); + +#if defined(_M_IX86) || defined(_M_X64) + uintptr_t prevAddr = 0; + while (prevAddr != currAddr) { + ReadOnlyTargetFunction<MMPolicyT> currFunc(mVMPolicy, currAddr); + prevAddr = currAddr; + + // If function entry is jmp rel8 stub to the internal implementation, we + // resolve redirected address from the jump target. + uintptr_t nextAddr = 0; + if (currFunc.IsRelativeShortJump(&nextAddr)) { + int8_t offset = nextAddr - currFunc.GetAddress() - 2; + +# if defined(_M_X64) + // We redirect to the target of a short jump backwards if the target + // is another jump (only 32-bit displacement is currently supported). + // This case is used by GetFileAttributesW in Win7 x64. + if ((offset < 0) && (currFunc.IsValidAtOffset(2 + offset))) { + ReadOnlyTargetFunction<MMPolicyT> redirectFn(mVMPolicy, nextAddr); + if (redirectFn.IsIndirectNearJump(&nextAddr)) { + return redirectFn; + } + } +# endif + + // We check the downstream has enough nop-space only when the offset is + // positive. Otherwise we stop chasing redirects and let the caller + // fail to hook. + if (offset > 0) { + bool isNopSpace = true; + for (int8_t i = 0; i < offset; i++) { + if (currFunc[2 + i] != 0x90) { + isNopSpace = false; + break; + } + } + + if (isNopSpace) { + currAddr = nextAddr; + } + } +# if defined(_M_X64) + } else if (currFunc.IsIndirectNearJump(&nextAddr) || + currFunc.IsRelativeNearJump(&nextAddr)) { +# else + } else if (currFunc.IsIndirectNearJump(&nextAddr)) { +# endif + // If function entry is jmp [disp32] such as used by kernel32, we + // resolve redirected address from import table. For x64, we resolve + // a relative near jump for TestDllInterceptor with --disable-optimize. + currAddr = nextAddr; + } + } +#endif // defined(_M_IX86) || defined(_M_X64) + + if (currAddr != reinterpret_cast<uintptr_t>(aOriginalFunction) && + !mVMPolicy.IsPageAccessible(currAddr)) { + currAddr = reinterpret_cast<uintptr_t>(aOriginalFunction); + } + return ReadOnlyTargetFunction<MMPolicyT>(mVMPolicy, currAddr); + } + + public: + FARPROC GetProcAddress(HMODULE aModule, const char* aName) const { + GetProcAddressSelector<MMPolicyT> selector; + return selector(aModule, aName, mVMPolicy); + } + + bool IsPageAccessible(uintptr_t aAddress) const { + return mVMPolicy.IsPageAccessible(aAddress); + } + +#if defined(NIGHTLY_BUILD) + const Maybe<DetourError>& GetLastDetourError() const { + return mVMPolicy.GetLastDetourError(); + } +#endif // defined(NIGHTLY_BUILD) + template <typename... Args> + void SetLastDetourError(Args&&... aArgs) { + mVMPolicy.SetLastDetourError(std::forward<Args>(aArgs)...); + } + + protected: + VMPolicy mVMPolicy; +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_PatcherBase_h diff --git a/toolkit/xre/dllservices/mozglue/interceptor/PatcherDetour.h b/toolkit/xre/dllservices/mozglue/interceptor/PatcherDetour.h new file mode 100644 index 0000000000..e0b33c7add --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/PatcherDetour.h @@ -0,0 +1,1739 @@ +/* -*- 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/. */ + +#ifndef mozilla_interceptor_PatcherDetour_h +#define mozilla_interceptor_PatcherDetour_h + +#if defined(_M_ARM64) +# include "mozilla/interceptor/Arm64.h" +#endif // defined(_M_ARM64) +#include <utility> + +#include "mozilla/Maybe.h" +#include "mozilla/NativeNt.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/TypedEnumBits.h" +#include "mozilla/Types.h" +#include "mozilla/Unused.h" +#include "mozilla/interceptor/PatcherBase.h" +#include "mozilla/interceptor/Trampoline.h" +#include "mozilla/interceptor/VMSharingPolicies.h" + +#define COPY_CODES(NBYTES) \ + do { \ + tramp.CopyCodes(origBytes.GetAddress(), NBYTES); \ + origBytes += NBYTES; \ + } while (0) + +namespace mozilla { +namespace interceptor { + +enum class DetourFlags : uint32_t { + eDefault = 0, + eEnable10BytePatch = 1, // Allow 10-byte patches when conditions allow + eTestOnlyForceShortPatch = + 2, // Force short patches at all times (x86-64 and arm64 testing only) + eDontResolveRedirection = + 4, // Don't resolve the redirection of JMP (e.g. kernel32 -> kernelbase) +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(DetourFlags) + +// This class is responsible to do tasks which depend on MMPolicy, decoupled +// from VMPolicy. We already have WindowsDllPatcherBase, but it needs to +// depend on VMPolicy to hold an instance of VMPolicy as a member. +template <typename MMPolicyT> +class WindowsDllDetourPatcherPrimitive { + protected: +#if defined(_M_ARM64) + // LDR x16, .+8 + static const uint32_t kLdrX16Plus8 = 0x58000050U; +#endif // defined(_M_ARM64) + + static void ApplyDefaultPatch(WritableTargetFunction<MMPolicyT>& target, + intptr_t aDest) { +#if defined(_M_IX86) + target.WriteByte(0xe9); // jmp + target.WriteDisp32(aDest); // hook displacement +#elif defined(_M_X64) + // mov r11, address + target.WriteByte(0x49); + target.WriteByte(0xbb); + target.WritePointer(aDest); + + // jmp r11 + target.WriteByte(0x41); + target.WriteByte(0xff); + target.WriteByte(0xe3); +#elif defined(_M_ARM64) + // The default patch requires 16 bytes + // LDR x16, .+8 + target.WriteLong(kLdrX16Plus8); + // BR x16 + target.WriteLong(arm64::BuildUnconditionalBranchToRegister(16)); + target.WritePointer(aDest); +#else +# error "Unsupported processor architecture" +#endif + } + + public: + constexpr static uint32_t GetWorstCaseRequiredBytesToPatch() { +#if defined(_M_IX86) + return 5; +#elif defined(_M_X64) + return 13; +#elif defined(_M_ARM64) + return 16; +#else +# error "Unsupported processor architecture" +#endif + } + + WindowsDllDetourPatcherPrimitive() = default; + + WindowsDllDetourPatcherPrimitive(const WindowsDllDetourPatcherPrimitive&) = + delete; + WindowsDllDetourPatcherPrimitive(WindowsDllDetourPatcherPrimitive&&) = delete; + WindowsDllDetourPatcherPrimitive& operator=( + const WindowsDllDetourPatcherPrimitive&) = delete; + WindowsDllDetourPatcherPrimitive& operator=( + WindowsDllDetourPatcherPrimitive&&) = delete; + + bool AddIrreversibleHook(const MMPolicyT& aMMPolicy, FARPROC aTargetFn, + intptr_t aHookDest) { + ReadOnlyTargetFunction<MMPolicyT> targetReadOnly(aMMPolicy, aTargetFn); + + WritableTargetFunction<MMPolicyT> targetWritable( + targetReadOnly.Promote(GetWorstCaseRequiredBytesToPatch())); + if (!targetWritable) { + return false; + } + + ApplyDefaultPatch(targetWritable, aHookDest); + + return targetWritable.Commit(); + } +}; + +template <typename VMPolicy> +class WindowsDllDetourPatcher final + : public WindowsDllDetourPatcherPrimitive<typename VMPolicy::MMPolicyT>, + public WindowsDllPatcherBase<VMPolicy> { + using MMPolicyT = typename VMPolicy::MMPolicyT; + using TrampPoolT = typename VMPolicy::PoolType; + using PrimitiveT = WindowsDllDetourPatcherPrimitive<MMPolicyT>; + Maybe<DetourFlags> mFlags; + + public: + template <typename... Args> + explicit WindowsDllDetourPatcher(Args&&... aArgs) + : WindowsDllPatcherBase<VMPolicy>(std::forward<Args>(aArgs)...) {} + + ~WindowsDllDetourPatcher() { Clear(); } + + WindowsDllDetourPatcher(const WindowsDllDetourPatcher&) = delete; + WindowsDllDetourPatcher(WindowsDllDetourPatcher&&) = delete; + WindowsDllDetourPatcher& operator=(const WindowsDllDetourPatcher&) = delete; + WindowsDllDetourPatcher& operator=(WindowsDllDetourPatcher&&) = delete; + + void Clear() { + if (!this->mVMPolicy.ShouldUnhookUponDestruction()) { + return; + } + +#if defined(_M_IX86) + size_t nBytes = 1 + sizeof(intptr_t); +#elif defined(_M_X64) + size_t nBytes = 2 + sizeof(intptr_t); +#elif defined(_M_ARM64) + size_t nBytes = 2 * sizeof(uint32_t) + sizeof(uintptr_t); +#else +# error "Unknown processor type" +#endif + + const auto& tramps = this->mVMPolicy.Items(); + for (auto&& tramp : tramps) { + // First we read the pointer to the interceptor instance. + Maybe<uintptr_t> instance = tramp.ReadEncodedPointer(); + if (!instance) { + continue; + } + + if (instance.value() != reinterpret_cast<uintptr_t>(this)) { + // tramp does not belong to this interceptor instance. + continue; + } + + auto clearInstance = MakeScopeExit([&tramp]() -> void { + // Clear the instance pointer so that no future instances with the same + // |this| pointer will attempt to reset its hook. + tramp.Rewind(); + tramp.WriteEncodedPointer(nullptr); + }); + + // Now we read the pointer to the intercepted function. + Maybe<uintptr_t> interceptedFn = tramp.ReadEncodedPointer(); + if (!interceptedFn) { + continue; + } + + WritableTargetFunction<MMPolicyT> origBytes( + this->mVMPolicy, interceptedFn.value(), nBytes); + if (!origBytes) { + continue; + } + +#if defined(_M_IX86) || defined(_M_X64) + + Maybe<uint8_t> maybeOpcode1 = origBytes.ReadByte(); + if (!maybeOpcode1) { + continue; + } + + uint8_t opcode1 = maybeOpcode1.value(); + +# if defined(_M_IX86) + // Ensure the JMP from CreateTrampoline is where we expect it to be. + MOZ_ASSERT(opcode1 == 0xE9); + if (opcode1 != 0xE9) { + continue; + } + + intptr_t startOfTrampInstructions = + static_cast<intptr_t>(tramp.GetCurrentRemoteAddress()); + + origBytes.WriteDisp32(startOfTrampInstructions); + if (!origBytes) { + continue; + } + + origBytes.Commit(); +# elif defined(_M_X64) + // Note: At the moment we clear 13-byte patches by replacing the jump to + // the patched function by a jump to the stub code. The original + // bytes of the original function are *not* restored. This implies + // that the stub code outlives our cleaning, so unwind information + // remains useful and must not be removed here. + if (opcode1 == 0x49) { + if (!Clear13BytePatch(origBytes, tramp.GetCurrentRemoteAddress())) { + continue; + } + } else if (opcode1 == 0xB8) { + if (!Clear10BytePatch(origBytes)) { + continue; + } + } else if (opcode1 == 0x48) { + // The original function was just a different trampoline + if (!ClearTrampolinePatch(origBytes, tramp.GetCurrentRemoteAddress())) { + continue; + } + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized patch!"); + continue; + } +# endif + +#elif defined(_M_ARM64) + + // Ensure that we see the instruction that we expect + Maybe<uint32_t> inst1 = origBytes.ReadLong(); + if (!inst1) { + continue; + } + + if (inst1.value() == this->kLdrX16Plus8) { + if (!Clear16BytePatch(origBytes, tramp.GetCurrentRemoteAddress())) { + continue; + } + } else if (arm64::IsUnconditionalBranchImm(inst1.value())) { + if (!Clear4BytePatch(inst1.value(), origBytes)) { + continue; + } + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized patch!"); + continue; + } + +#else +# error "Unknown processor type" +#endif + } + + this->mVMPolicy.Clear(); + } + +#if defined(_M_X64) + bool Clear13BytePatch(WritableTargetFunction<MMPolicyT>& aOrigBytes, + const uintptr_t aResetToAddress) { + Maybe<uint8_t> maybeOpcode2 = aOrigBytes.ReadByte(); + if (!maybeOpcode2) { + return false; + } + + uint8_t opcode2 = maybeOpcode2.value(); + if (opcode2 != 0xBB) { + return false; + } + + aOrigBytes.WritePointer(aResetToAddress); + if (!aOrigBytes) { + return false; + } + + return aOrigBytes.Commit(); + } + + bool ClearTrampolinePatch(WritableTargetFunction<MMPolicyT>& aOrigBytes, + const uintptr_t aPtrToResetToAddress) { + // The target of the trampoline we replaced is stored at + // aPtrToResetToAddress. We simply put it back where we got it from. + Maybe<uint8_t> maybeOpcode2 = aOrigBytes.ReadByte(); + if (!maybeOpcode2) { + return false; + } + + uint8_t opcode2 = maybeOpcode2.value(); + if (opcode2 != 0xB8) { + return false; + } + + auto oldPtr = *(reinterpret_cast<const uintptr_t*>(aPtrToResetToAddress)); + + aOrigBytes.WritePointer(oldPtr); + if (!aOrigBytes) { + return false; + } + + return aOrigBytes.Commit(); + } + + bool Clear10BytePatch(WritableTargetFunction<MMPolicyT>& aOrigBytes) { + Maybe<uint32_t> maybePtr32 = aOrigBytes.ReadLong(); + if (!maybePtr32) { + return false; + } + + uint32_t ptr32 = maybePtr32.value(); + // We expect the high bit to be clear + if (ptr32 & 0x80000000) { + return false; + } + + uintptr_t trampPtr = ptr32; + + // trampPtr points to an intermediate trampoline that contains a 13-byte + // patch. We back up by sizeof(uintptr_t) so that we can access the pointer + // to the stub trampoline. + WritableTargetFunction<MMPolicyT> writableIntermediate( + this->mVMPolicy, trampPtr - sizeof(uintptr_t), 13 + sizeof(uintptr_t)); + if (!writableIntermediate) { + return false; + } + + Maybe<uintptr_t> stubTramp = writableIntermediate.ReadEncodedPtr(); + if (!stubTramp || !stubTramp.value()) { + return false; + } + + Maybe<uint8_t> maybeOpcode1 = writableIntermediate.ReadByte(); + if (!maybeOpcode1) { + return false; + } + + // We expect this opcode to be the beginning of our normal mov r11, ptr + // patch sequence. + uint8_t opcode1 = maybeOpcode1.value(); + if (opcode1 != 0x49) { + return false; + } + + // Now we can just delegate the rest to our normal 13-byte patch clearing. + return Clear13BytePatch(writableIntermediate, stubTramp.value()); + } +#endif // defined(_M_X64) + +#if defined(_M_ARM64) + bool Clear4BytePatch(const uint32_t aBranchImm, + WritableTargetFunction<MMPolicyT>& aOrigBytes) { + MOZ_ASSERT(arm64::IsUnconditionalBranchImm(aBranchImm)); + + arm64::LoadOrBranch decoded = arm64::BUncondImmDecode( + aOrigBytes.GetCurrentAddress() - sizeof(uint32_t), aBranchImm); + + uintptr_t trampPtr = decoded.mAbsAddress; + + // trampPtr points to an intermediate trampoline that contains a veneer. + // We back up by sizeof(uintptr_t) so that we can access the pointer to the + // stub trampoline. + + // We want trampLen to be the size of the veneer, plus one pointer (since + // we are backing up trampPtr by one pointer) + size_t trampLen = 16 + sizeof(uintptr_t); + + WritableTargetFunction<MMPolicyT> writableIntermediate( + this->mVMPolicy, trampPtr - sizeof(uintptr_t), trampLen); + if (!writableIntermediate) { + return false; + } + + Maybe<uintptr_t> stubTramp = writableIntermediate.ReadEncodedPtr(); + if (!stubTramp || !stubTramp.value()) { + return false; + } + + Maybe<uint32_t> inst1 = writableIntermediate.ReadLong(); + if (!inst1 || inst1.value() != this->kLdrX16Plus8) { + return false; + } + + return Clear16BytePatch(writableIntermediate, stubTramp.value()); + } + + bool Clear16BytePatch(WritableTargetFunction<MMPolicyT>& aOrigBytes, + const uintptr_t aResetToAddress) { + Maybe<uint32_t> inst2 = aOrigBytes.ReadLong(); + if (!inst2) { + return false; + } + + if (inst2.value() != arm64::BuildUnconditionalBranchToRegister(16)) { + MOZ_ASSERT_UNREACHABLE("Unrecognized patch!"); + return false; + } + + // Clobber the pointer to our hook function with a pointer to the + // start of the trampoline. + aOrigBytes.WritePointer(aResetToAddress); + aOrigBytes.Commit(); + + return true; + } +#endif // defined(_M_ARM64) + + void Init(DetourFlags aFlags = DetourFlags::eDefault) { + if (Initialized()) { + return; + } + +#if defined(_M_X64) + if (aFlags & DetourFlags::eTestOnlyForceShortPatch) { + aFlags |= DetourFlags::eEnable10BytePatch; + } +#endif // defined(_M_X64) + + mFlags = Some(aFlags); + } + + bool Initialized() const { return mFlags.isSome(); } + + bool AddHook(FARPROC aTargetFn, intptr_t aHookDest, void** aOrigFunc) { + ReadOnlyTargetFunction<MMPolicyT> target( + (mFlags.value() & DetourFlags::eDontResolveRedirection) + ? ReadOnlyTargetFunction<MMPolicyT>( + this->mVMPolicy, reinterpret_cast<uintptr_t>(aTargetFn)) + : this->ResolveRedirectedAddress(aTargetFn)); + + TrampPoolT* trampPool = nullptr; + +#if defined(_M_ARM64) + // ARM64 uses two passes to build its trampoline. The first pass uses a + // null tramp to determine how many bytes are needed. Once that is known, + // CreateTrampoline calls itself recursively with a "real" tramp. + Trampoline<MMPolicyT> tramp(nullptr); +#else + Maybe<TrampPoolT> maybeTrampPool = DoReserve(); + MOZ_ASSERT(maybeTrampPool); + if (!maybeTrampPool) { + return false; + } + + trampPool = maybeTrampPool.ptr(); + + Maybe<Trampoline<MMPolicyT>> maybeTramp(trampPool->GetNextTrampoline()); + if (!maybeTramp) { + this->SetLastDetourError( + DetourResultCode::DETOUR_PATCHER_NEXT_TRAMPOLINE_ERROR); + return false; + } + + Trampoline<MMPolicyT> tramp(std::move(maybeTramp.ref())); +#endif + + CreateTrampoline(target, trampPool, tramp, aHookDest, aOrigFunc); + if (!*aOrigFunc) { + return false; + } + + return true; + } + + private: + /** + * This function returns a maximum distance that can be reached by a single + * unconditional jump instruction. This is dependent on the processor ISA. + * Note that this distance is *exclusive* when added to the pivot, so the + * distance returned by this function is actually + * (maximum_absolute_offset + 1). + */ + static uint32_t GetDefaultPivotDistance() { +#if defined(_M_ARM64) + // Immediate unconditional branch allows for +/- 128MB + return 0x08000000U; +#elif defined(_M_IX86) || defined(_M_X64) + // For these ISAs, our distance will assume the use of an unconditional jmp + // with a 32-bit signed displacement. + return 0x80000000U; +#else +# error "Not defined for this processor arch" +#endif + } + + /** + * If we're reserving trampoline space for a specific module, we base the + * pivot off of the median address of the module's .text section. While this + * may not be precise, it should be accurate enough for our purposes: To + * ensure that the trampoline space is reachable by any executable code in the + * module. + */ + Maybe<TrampPoolT> ReserveForModule(HMODULE aModule) { + nt::PEHeaders moduleHeaders(aModule); + if (!moduleHeaders) { + this->SetLastDetourError( + DetourResultCode::DETOUR_PATCHER_RESERVE_FOR_MODULE_PE_ERROR); + return Nothing(); + } + + Maybe<Span<const uint8_t>> textSectionInfo = + moduleHeaders.GetTextSectionInfo(); + if (!textSectionInfo) { + this->SetLastDetourError( + DetourResultCode::DETOUR_PATCHER_RESERVE_FOR_MODULE_TEXT_ERROR); + return Nothing(); + } + + const uint8_t* median = textSectionInfo.value().data() + + (textSectionInfo.value().LengthBytes() / 2); + + Maybe<TrampPoolT> maybeTrampPool = this->mVMPolicy.Reserve( + reinterpret_cast<uintptr_t>(median), GetDefaultPivotDistance()); + if (!maybeTrampPool) { + this->SetLastDetourError( + DetourResultCode::DETOUR_PATCHER_RESERVE_FOR_MODULE_RESERVE_ERROR); + } + return maybeTrampPool; + } + + Maybe<TrampPoolT> DoReserve(HMODULE aModule = nullptr) { + if (aModule) { + return ReserveForModule(aModule); + } + + uintptr_t pivot = 0; + uint32_t distance = 0; + +#if defined(_M_X64) + if (mFlags.value() & DetourFlags::eEnable10BytePatch) { + // We must stay below the 2GB mark because a 10-byte patch uses movsxd + // (ie, sign extension) to expand the pointer to 64-bits, so bit 31 of any + // pointers into the reserved region must be 0. + pivot = 0x40000000U; + distance = 0x40000000U; + } +#endif // defined(_M_X64) + + Maybe<TrampPoolT> maybeTrampPool = this->mVMPolicy.Reserve(pivot, distance); +#if defined(NIGHTLY_BUILD) + if (!maybeTrampPool && this->GetLastDetourError().isNothing()) { + this->SetLastDetourError( + DetourResultCode::DETOUR_PATCHER_DO_RESERVE_ERROR); + } +#endif // defined(NIGHTLY_BUILD) + return maybeTrampPool; + } + + protected: +#if !defined(_M_ARM64) + + const static int kPageSize = 4096; + + // rex bits + static const BYTE kMaskHighNibble = 0xF0; + static const BYTE kRexOpcode = 0x40; + static const BYTE kMaskRexW = 0x08; + static const BYTE kMaskRexR = 0x04; + static const BYTE kMaskRexX = 0x02; + static const BYTE kMaskRexB = 0x01; + + // mod r/m bits + static const BYTE kRegFieldShift = 3; + static const BYTE kMaskMod = 0xC0; + static const BYTE kMaskReg = 0x38; + static const BYTE kMaskRm = 0x07; + static const BYTE kRmNeedSib = 0x04; + static const BYTE kModReg = 0xC0; + static const BYTE kModDisp32 = 0x80; + static const BYTE kModDisp8 = 0x40; + static const BYTE kModNoRegDisp = 0x00; + static const BYTE kRmNoRegDispDisp32 = 0x05; + + // sib bits + static const BYTE kMaskSibScale = 0xC0; + static const BYTE kMaskSibIndex = 0x38; + static const BYTE kMaskSibBase = 0x07; + static const BYTE kSibBaseEbp = 0x05; + + // Register bit IDs. + static const BYTE kRegAx = 0x0; + static const BYTE kRegCx = 0x1; + static const BYTE kRegDx = 0x2; + static const BYTE kRegBx = 0x3; + static const BYTE kRegSp = 0x4; + static const BYTE kRegBp = 0x5; + static const BYTE kRegSi = 0x6; + static const BYTE kRegDi = 0x7; + + // Special ModR/M codes. These indicate operands that cannot be simply + // memcpy-ed. + // Operand is a 64-bit RIP-relative address. + static const int kModOperand64 = -2; + // Operand is not yet handled by our trampoline. + static const int kModUnknown = -1; + + /** + * Returns the number of bytes taken by the ModR/M byte, SIB (if present) + * and the instruction's operand. In special cases, the special MODRM codes + * above are returned. + * aModRm points to the ModR/M byte of the instruction. + * On return, aSubOpcode (if present) is filled with the subopcode/register + * code found in the ModR/M byte. + */ + int CountModRmSib(const ReadOnlyTargetFunction<MMPolicyT>& aModRm, + BYTE* aSubOpcode = nullptr) { + int numBytes = 1; // Start with 1 for mod r/m byte itself + switch (*aModRm & kMaskMod) { + case kModReg: + return numBytes; + case kModDisp8: + numBytes += 1; + break; + case kModDisp32: + numBytes += 4; + break; + case kModNoRegDisp: + if ((*aModRm & kMaskRm) == kRmNoRegDispDisp32) { +# if defined(_M_X64) + if (aSubOpcode) { + *aSubOpcode = (*aModRm & kMaskReg) >> kRegFieldShift; + } + return kModOperand64; +# else + // On IA-32, all ModR/M instruction modes address memory relative to 0 + numBytes += 4; +# endif + } else if (((*aModRm & kMaskRm) == kRmNeedSib && + (*(aModRm + 1) & kMaskSibBase) == kSibBaseEbp)) { + numBytes += 4; + } + break; + default: + // This should not be reachable + MOZ_ASSERT_UNREACHABLE("Impossible value for modr/m byte mod bits"); + return kModUnknown; + } + if ((*aModRm & kMaskRm) == kRmNeedSib) { + // SIB byte + numBytes += 1; + } + if (aSubOpcode) { + *aSubOpcode = (*aModRm & kMaskReg) >> kRegFieldShift; + } + return numBytes; + } + +# if defined(_M_X64) + enum class JumpType{Je, Jne, Jae, Jmp, Call}; + + static bool GenerateJump(Trampoline<MMPolicyT>& aTramp, + uintptr_t aAbsTargetAddress, const JumpType aType) { + // Near call, absolute indirect, address given in r/m32 + if (aType == JumpType::Call) { + // CALL [RIP+0] + aTramp.WriteByte(0xff); + aTramp.WriteByte(0x15); + // The offset to jump destination -- 2 bytes after the current position. + aTramp.WriteInteger(2); + aTramp.WriteByte(0xeb); // JMP + 8 (jump over target address) + aTramp.WriteByte(8); + aTramp.WritePointer(aAbsTargetAddress); + return !!aTramp; + } + + // Write an opposite conditional jump because the destination branches + // are swapped. + if (aType == JumpType::Je) { + // JNE RIP+14 + aTramp.WriteByte(0x75); + aTramp.WriteByte(14); + } else if (aType == JumpType::Jne) { + // JE RIP+14 + aTramp.WriteByte(0x74); + aTramp.WriteByte(14); + } else if (aType == JumpType::Jae) { + // JB RIP+14 + aTramp.WriteByte(0x72); + aTramp.WriteByte(14); + } + + // Near jmp, absolute indirect, address given in r/m32 + // JMP [RIP+0] + aTramp.WriteByte(0xff); + aTramp.WriteByte(0x25); + // The offset to jump destination is 0 + aTramp.WriteInteger(0); + aTramp.WritePointer(aAbsTargetAddress); + + return !!aTramp; + } +# endif + + enum ePrefixGroupBits{eNoPrefixes = 0, ePrefixGroup1 = (1 << 0), + ePrefixGroup2 = (1 << 1), ePrefixGroup3 = (1 << 2), + ePrefixGroup4 = (1 << 3)}; + + int CountPrefixBytes(const ReadOnlyTargetFunction<MMPolicyT>& aBytes, + unsigned char* aOutGroupBits) { + unsigned char& groupBits = *aOutGroupBits; + groupBits = eNoPrefixes; + int index = 0; + while (true) { + switch (aBytes[index]) { + // Group 1 + case 0xF0: // LOCK + case 0xF2: // REPNZ + case 0xF3: // REP / REPZ + if (groupBits & ePrefixGroup1) { + return -1; + } + groupBits |= ePrefixGroup1; + ++index; + break; + + // Group 2 + case 0x2E: // CS override / branch not taken + case 0x36: // SS override + case 0x3E: // DS override / branch taken + case 0x64: // FS override + case 0x65: // GS override + if (groupBits & ePrefixGroup2) { + return -1; + } + groupBits |= ePrefixGroup2; + ++index; + break; + + // Group 3 + case 0x66: // operand size override + if (groupBits & ePrefixGroup3) { + return -1; + } + groupBits |= ePrefixGroup3; + ++index; + break; + + // Group 4 + case 0x67: // Address size override + if (groupBits & ePrefixGroup4) { + return -1; + } + groupBits |= ePrefixGroup4; + ++index; + break; + + default: + return index; + } + } + } + + // Return a ModR/M byte made from the 2 Mod bits, the register used for the + // reg bits and the register used for the R/M bits. + BYTE BuildModRmByte(BYTE aModBits, BYTE aReg, BYTE aRm) { + MOZ_ASSERT((aRm & kMaskRm) == aRm); + MOZ_ASSERT((aModBits & kMaskMod) == aModBits); + MOZ_ASSERT(((aReg << kRegFieldShift) & kMaskReg) == + (aReg << kRegFieldShift)); + return aModBits | (aReg << kRegFieldShift) | aRm; + } + +#endif // !defined(_M_ARM64) + + // If originalFn is a recognized trampoline then patch it to call aDest, + // set *aTramp and *aOutTramp to that trampoline's target and return true. + bool PatchIfTargetIsRecognizedTrampoline( + Trampoline<MMPolicyT>& aTramp, + ReadOnlyTargetFunction<MMPolicyT>& aOriginalFn, intptr_t aDest, + void** aOutTramp) { +#if defined(_M_X64) + // Variation 1: + // 48 b8 imm64 mov rax, imm64 + // ff e0 jmp rax + // + // Variation 2: + // 48 b8 imm64 mov rax, imm64 + // 50 push rax + // c3 ret + if ((aOriginalFn[0] == 0x48) && (aOriginalFn[1] == 0xB8) && + ((aOriginalFn[10] == 0xFF && aOriginalFn[11] == 0xE0) || + (aOriginalFn[10] == 0x50 && aOriginalFn[11] == 0xC3))) { + uintptr_t originalTarget = + (aOriginalFn + 2).template ChasePointer<uintptr_t>(); + + // Skip the first two bytes (48 b8) so that we can overwrite the imm64 + WritableTargetFunction<MMPolicyT> target(aOriginalFn.Promote(8, 2)); + if (!target) { + return false; + } + + // Write the new JMP target address. + target.WritePointer(aDest); + if (!target.Commit()) { + return false; + } + + // Store the old target address so we can restore it when we're cleared + aTramp.WritePointer(originalTarget); + if (!aTramp) { + return false; + } + + *aOutTramp = reinterpret_cast<void*>(originalTarget); + return true; + } +#endif // defined(_M_X64) + + return false; + } + +#if defined(_M_ARM64) + bool Apply4BytePatch(TrampPoolT* aTrampPool, void* aTrampPtr, + WritableTargetFunction<MMPolicyT>& target, + intptr_t aDest) { + MOZ_ASSERT(aTrampPool); + if (!aTrampPool) { + return false; + } + + uintptr_t hookDest = arm64::MakeVeneer(*aTrampPool, aTrampPtr, aDest); + if (!hookDest) { + return false; + } + + Maybe<uint32_t> branchImm = arm64::BuildUnconditionalBranchImm( + target.GetCurrentAddress(), hookDest); + if (!branchImm) { + return false; + } + + target.WriteLong(branchImm.value()); + + return true; + } +#endif // defined(_M_ARM64) + +#if defined(_M_X64) + bool Apply10BytePatch(TrampPoolT* aTrampPool, void* aTrampPtr, + WritableTargetFunction<MMPolicyT>& target, + intptr_t aDest) { + // Note: Even if the target function is also below 2GB, we still use an + // intermediary trampoline so that we consistently have a 64-bit pointer + // that we can use to reset the trampoline upon interceptor shutdown. + Maybe<Trampoline<MMPolicyT>> maybeCallTramp( + aTrampPool->GetNextTrampoline()); + if (!maybeCallTramp) { + return false; + } + + Trampoline<MMPolicyT> callTramp(std::move(maybeCallTramp.ref())); + + // Write a null instance so that Clear() does not consider this tramp to + // be a normal tramp to be torn down. + callTramp.WriteEncodedPointer(nullptr); + // Use the second pointer slot to store a pointer to the primary tramp + callTramp.WriteEncodedPointer(aTrampPtr); + callTramp.StartExecutableCode(); + + // mov r11, address + callTramp.WriteByte(0x49); + callTramp.WriteByte(0xbb); + callTramp.WritePointer(aDest); + + // jmp r11 + callTramp.WriteByte(0x41); + callTramp.WriteByte(0xff); + callTramp.WriteByte(0xe3); + + void* callTrampStart = callTramp.EndExecutableCode(); + if (!callTrampStart) { + return false; + } + + target.WriteByte(0xB8); // MOV EAX, IMM32 + + // Assert that the topmost 33 bits are 0 + MOZ_ASSERT( + !(reinterpret_cast<uintptr_t>(callTrampStart) & (~0x7FFFFFFFULL))); + + target.WriteLong(static_cast<uint32_t>( + reinterpret_cast<uintptr_t>(callTrampStart) & 0x7FFFFFFFU)); + target.WriteByte(0x48); // REX.W + target.WriteByte(0x63); // MOVSXD r64, r/m32 + // dest: rax, src: eax + target.WriteByte(BuildModRmByte(kModReg, kRegAx, kRegAx)); + target.WriteByte(0xFF); // JMP /4 + target.WriteByte(BuildModRmByte(kModReg, 4, kRegAx)); // rax + + return true; + } +#endif // defined(_M_X64) + + void CreateTrampoline(ReadOnlyTargetFunction<MMPolicyT>& origBytes, + TrampPoolT* aTrampPool, Trampoline<MMPolicyT>& aTramp, + intptr_t aDest, void** aOutTramp) { + *aOutTramp = nullptr; + + Trampoline<MMPolicyT>& tramp = aTramp; + if (!tramp) { + this->SetLastDetourError( + DetourResultCode::DETOUR_PATCHER_INVALID_TRAMPOLINE); + return; + } + + // The beginning of the trampoline contains two pointer-width slots: + // [0]: |this|, so that we know whether the trampoline belongs to us; + // [1]: Pointer to original function, so that we can reset the hooked + // function to its original behavior upon destruction. In rare cases + // where the function was already a different trampoline, this is + // just a pointer to that trampoline's target address. + tramp.WriteEncodedPointer(this); + if (!tramp) { + this->SetLastDetourError( + DetourResultCode::DETOUR_PATCHER_WRITE_POINTER_ERROR); + return; + } + + auto clearInstanceOnFailure = MakeScopeExit([this, aOutTramp, &tramp, + &origBytes]() -> void { + // *aOutTramp is not set until CreateTrampoline has completed + // successfully, so we can use that to check for success. + if (*aOutTramp) { + return; + } + + // Clear the instance pointer so that we don't try to reset a + // nonexistent hook. + tramp.Rewind(); + tramp.WriteEncodedPointer(nullptr); + +#if defined(NIGHTLY_BUILD) + origBytes.Rewind(); + this->SetLastDetourError( + DetourResultCode::DETOUR_PATCHER_CREATE_TRAMPOLINE_ERROR); + DetourError& lastError = *this->mVMPolicy.mLastError; + size_t bytesToCapture = std::min( + ArrayLength(lastError.mOrigBytes), + static_cast<size_t>(PrimitiveT::GetWorstCaseRequiredBytesToPatch())); +# if defined(_M_ARM64) + size_t numInstructionsToCapture = bytesToCapture / sizeof(uint32_t); + auto origBytesDst = reinterpret_cast<uint32_t*>(lastError.mOrigBytes); + for (size_t i = 0; i < numInstructionsToCapture; ++i) { + origBytesDst[i] = origBytes.ReadNextInstruction(); + } +# else + for (size_t i = 0; i < bytesToCapture; ++i) { + lastError.mOrigBytes[i] = origBytes[i]; + } +# endif // defined(_M_ARM64) +#else + // Silence -Wunused-lambda-capture in non-Nightly. + Unused << this; + Unused << origBytes; +#endif // defined(NIGHTLY_BUILD) + }); + + tramp.WritePointer(origBytes.AsEncodedPtr()); + if (!tramp) { + return; + } + + if (PatchIfTargetIsRecognizedTrampoline(tramp, origBytes, aDest, + aOutTramp)) { + return; + } + + tramp.StartExecutableCode(); + + constexpr uint32_t kWorstCaseBytesRequired = + PrimitiveT::GetWorstCaseRequiredBytesToPatch(); + +#if defined(_M_IX86) + int pJmp32 = -1; + while (origBytes.GetOffset() < kWorstCaseBytesRequired) { + // Understand some simple instructions that might be found in a + // prologue; we might need to extend this as necessary. + // + // Note! If we ever need to understand jump instructions, we'll + // need to rewrite the displacement argument. + unsigned char prefixGroups; + int numPrefixBytes = CountPrefixBytes(origBytes, &prefixGroups); + if (numPrefixBytes < 0 || + (prefixGroups & (ePrefixGroup3 | ePrefixGroup4))) { + // Either the prefix sequence was bad, or there are prefixes that + // we don't currently support (groups 3 and 4) + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + + origBytes += numPrefixBytes; + if (*origBytes >= 0x88 && *origBytes <= 0x8B) { + // various MOVs + ++origBytes; + int len = CountModRmSib(origBytes); + if (len < 0) { + MOZ_ASSERT_UNREACHABLE("Unrecognized MOV opcode sequence"); + return; + } + origBytes += len; + } else if (*origBytes == 0x0f && + (origBytes[1] == 0x10 || origBytes[1] == 0x11)) { + // SSE: movups xmm, xmm/m128 + // movups xmm/m128, xmm + origBytes += 2; + int len = CountModRmSib(origBytes); + if (len < 0) { + MOZ_ASSERT_UNREACHABLE("Unrecognized MOV opcode sequence"); + return; + } + origBytes += len; + } else if (*origBytes == 0xA1) { + // MOV eax, [seg:offset] + origBytes += 5; + } else if (*origBytes == 0xB8) { + // MOV 0xB8: http://ref.x86asm.net/coder32.html#xB8 + origBytes += 5; + } else if (*origBytes == 0x33 && (origBytes[1] & kMaskMod) == kModReg) { + // XOR r32, r32 + origBytes += 2; + } else if ((*origBytes & 0xf8) == 0x40) { + // INC r32 + origBytes += 1; + } else if (*origBytes == 0x83) { + uint8_t mod = static_cast<uint8_t>(origBytes[1]) & kMaskMod; + uint8_t rm = static_cast<uint8_t>(origBytes[1]) & kMaskRm; + if (mod == kModReg) { + // ADD|OR|ADC|SBB|AND|SUB|XOR|CMP r, imm8 + origBytes += 3; + } else if (mod == kModDisp8 && rm != kRmNeedSib) { + // ADD|OR|ADC|SBB|AND|SUB|XOR|CMP [r+disp8], imm8 + origBytes += 4; + } else { + // bail + MOZ_ASSERT_UNREACHABLE("Unrecognized bit opcode sequence"); + return; + } + } else if (*origBytes == 0x68) { + // PUSH with 4-byte operand + origBytes += 5; + } else if ((*origBytes & 0xf0) == 0x50) { + // 1-byte PUSH/POP + ++origBytes; + } else if (*origBytes == 0x6A) { + // PUSH imm8 + origBytes += 2; + } else if (*origBytes == 0xe9) { + pJmp32 = origBytes.GetOffset(); + // jmp 32bit offset + origBytes += 5; + } else if (*origBytes == 0xff && origBytes[1] == 0x25) { + // jmp [disp32] + origBytes += 6; + } else if (*origBytes == 0xc2) { + // ret imm16. We can't handle this but it happens. We don't ASSERT but + // we do fail to hook. +# if defined(MOZILLA_INTERNAL_API) + NS_WARNING("Cannot hook method -- RET opcode found"); +# endif + return; + } else { + // printf ("Unknown x86 instruction byte 0x%02x, aborting trampoline\n", + // *origBytes); + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } + + // The trampoline is a copy of the instructions that we just traced, + // followed by a jump that we add below. + tramp.CopyFrom(origBytes.GetBaseAddress(), origBytes.GetOffset()); + if (!tramp) { + return; + } +#elif defined(_M_X64) + bool foundJmp = false; + // |use10BytePatch| should always default to |false| in production. It is + // not set to true unless we detect that a 10-byte patch is necessary. + // OTOH, for testing purposes, if we want to force a 10-byte patch, we + // always initialize |use10BytePatch| to |true|. + bool use10BytePatch = + (mFlags.value() & DetourFlags::eTestOnlyForceShortPatch) == + DetourFlags::eTestOnlyForceShortPatch; + const uint32_t bytesRequired = + use10BytePatch ? 10 : kWorstCaseBytesRequired; + + while (origBytes.GetOffset() < bytesRequired) { + // If we found JMP 32bit offset, we require that the next bytes must + // be NOP or INT3. There is no reason to copy them. + // TODO: This used to trigger for Je as well. Now that I allow + // instructions after CALL and JE, I don't think I need that. + // The only real value of this condition is that if code follows a JMP + // then its _probably_ the target of a JMP somewhere else and we + // will be overwriting it, which would be tragic. This seems + // highly unlikely. + if (foundJmp) { + if (*origBytes == 0x90 || *origBytes == 0xcc) { + ++origBytes; + continue; + } + + // If our trampoline space is located in the lowest 2GB, we can do a ten + // byte patch instead of a thirteen byte patch. + if (aTrampPool && aTrampPool->IsInLowest2GB() && + origBytes.GetOffset() >= 10) { + use10BytePatch = true; + break; + } + + MOZ_ASSERT_UNREACHABLE("Opcode sequence includes commands after JMP"); + return; + } + if (*origBytes == 0x0f) { + COPY_CODES(1); + if (*origBytes == 0x1f) { + // nop (multibyte) + COPY_CODES(1); + if ((*origBytes & 0xc0) == 0x40 && (*origBytes & 0x7) == 0x04) { + COPY_CODES(3); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x05) { + // syscall + COPY_CODES(1); + } else if (*origBytes == 0x10 || *origBytes == 0x11) { + // SSE: movups xmm, xmm/m128 + // movups xmm/m128, xmm + COPY_CODES(1); + int nModRmSibBytes = CountModRmSib(origBytes); + if (nModRmSibBytes < 0) { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } else { + COPY_CODES(nModRmSibBytes); + } + } else if (*origBytes >= 0x83 && *origBytes <= 0x85) { + // 0f 83 cd JAE rel32 + // 0f 84 cd JE rel32 + // 0f 85 cd JNE rel32 + const JumpType kJumpTypes[] = {JumpType::Jae, JumpType::Je, + JumpType::Jne}; + auto jumpType = kJumpTypes[*origBytes - 0x83]; + ++origBytes; + --tramp; // overwrite the 0x0f we copied above + + if (!GenerateJump(tramp, origBytes.ReadDisp32AsAbsolute(), + jumpType)) { + return; + } + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes >= 0x88 && *origBytes <= 0x8B) { + // various 32-bit MOVs + COPY_CODES(1); + int len = CountModRmSib(origBytes); + if (len < 0) { + MOZ_ASSERT_UNREACHABLE("Unrecognized MOV opcode sequence"); + return; + } + COPY_CODES(len); + } else if (*origBytes == 0x40 || *origBytes == 0x41) { + // Plain REX or REX.B + COPY_CODES(1); + if ((*origBytes & 0xf0) == 0x50) { + // push/pop with Rx register + COPY_CODES(1); + } else if (*origBytes >= 0xb8 && *origBytes <= 0xbf) { + // mov r32, imm32 + COPY_CODES(5); + } else if (*origBytes == 0x8b && (origBytes[1] & kMaskMod) == kModReg) { + // 8B /r: mov r32, r/m32 + COPY_CODES(2); + } else if (*origBytes == 0xf7 && + (origBytes[1] & (kMaskMod | kMaskReg)) == + (kModReg | (0 << kRegFieldShift))) { + // F7 /0 id: test r/m32, imm32 + COPY_CODES(6); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x44) { + // REX.R + COPY_CODES(1); + + // TODO: Combine with the "0x89" case below in the REX.W section + if (*origBytes == 0x89) { + // mov r/m32, r32 + COPY_CODES(1); + int len = CountModRmSib(origBytes); + if (len < 0) { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + COPY_CODES(len); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x45) { + // REX.R & REX.B + COPY_CODES(1); + + if (*origBytes == 0x33) { + // xor r32, r32 + COPY_CODES(2); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if ((*origBytes & 0xfa) == 0x48) { + // REX.W | REX.WR | REX.WRB | REX.WB + COPY_CODES(1); + + if (*origBytes == 0x81 && (origBytes[1] & 0xf8) == 0xe8) { + // sub r, dword + COPY_CODES(6); + } else if (*origBytes == 0x83 && (origBytes[1] & 0xf8) == 0xe8) { + // sub r, byte + COPY_CODES(3); + } else if (*origBytes == 0x83 && + (origBytes[1] & (kMaskMod | kMaskReg)) == kModReg) { + // add r, byte + COPY_CODES(3); + } else if (*origBytes == 0x83 && (origBytes[1] & 0xf8) == 0x60) { + // and [r+d], imm8 + COPY_CODES(5); + } else if (*origBytes == 0x2b && (origBytes[1] & kMaskMod) == kModReg) { + // sub r64, r64 + COPY_CODES(2); + } else if (*origBytes == 0x85) { + // 85 /r => TEST r/m32, r32 + if ((origBytes[1] & 0xc0) == 0xc0) { + COPY_CODES(2); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if ((*origBytes & 0xfd) == 0x89) { + // MOV r/m64, r64 | MOV r64, r/m64 + BYTE reg; + int len = CountModRmSib(origBytes + 1, ®); + if (len < 0) { + MOZ_ASSERT(len == kModOperand64); + if (len != kModOperand64) { + return; + } + origBytes += 2; // skip the MOV and MOD R/M bytes + + // The instruction MOVs 64-bit data from a RIP-relative memory + // address (determined with a 32-bit offset from RIP) into a + // 64-bit register. + uintptr_t absAddr = origBytes.ReadDisp32AsAbsolute(); + + if (reg == kRegAx) { + // Destination is RAX. Encode instruction as MOVABS with a + // 64-bit absolute address as its immediate operand. + tramp.WriteByte(0xa1); + tramp.WritePointer(absAddr); + } else { + // The MOV must be done in two steps. First, we MOVABS the + // absolute 64-bit address into our target register. + // Then, we MOV from that address into the register + // using register-indirect addressing. + tramp.WriteByte(0xb8 + reg); + tramp.WritePointer(absAddr); + tramp.WriteByte(0x48); + tramp.WriteByte(0x8b); + tramp.WriteByte(BuildModRmByte(kModNoRegDisp, reg, reg)); + } + } else { + COPY_CODES(len + 1); + } + } else if ((*origBytes & 0xf8) == 0xb8) { + // MOV r64, imm64 + COPY_CODES(9); + } else if (*origBytes == 0xc7) { + // MOV r/m64, imm32 + if (origBytes[1] == 0x44) { + // MOV [r64+disp8], imm32 + // ModR/W + SIB + disp8 + imm32 + COPY_CODES(8); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0xff) { + // JMP/4 or CALL/2 + if ((origBytes[1] & 0xc0) == 0x0 && (origBytes[1] & 0x07) == 0x5 && + ((origBytes[1] & 0x38) == 0x20 || + (origBytes[1] & 0x38) == 0x10)) { + origBytes += 2; + --tramp; // overwrite the REX.W/REX.RW we copied above + + foundJmp = (origBytes[1] & 0x38) == 0x20; + if (!GenerateJump(tramp, origBytes.ChasePointerFromDisp(), + foundJmp ? JumpType::Jmp : JumpType::Call)) { + return; + } + } else { + // not support yet! + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x8d) { + // LEA reg, addr + if ((origBytes[1] & kMaskMod) == 0x0 && + (origBytes[1] & kMaskRm) == 0x5) { + // [rip+disp32] + // convert 32bit offset to 64bit direct and convert instruction + // to a simple 64-bit mov + BYTE reg = (origBytes[1] & kMaskReg) >> kRegFieldShift; + origBytes += 2; + uintptr_t absAddr = origBytes.ReadDisp32AsAbsolute(); + tramp.WriteByte(0xb8 + reg); // move + tramp.WritePointer(absAddr); + } else { + // Above we dealt with RIP-relative instructions. Any other + // operand form can simply be copied. + int len = CountModRmSib(origBytes + 1); + // We handled the kModOperand64 -- ie RIP-relative -- case above + MOZ_ASSERT(len > 0); + COPY_CODES(len + 1); + } + } else if (*origBytes == 0x63 && (origBytes[1] & kMaskMod) == kModReg) { + // movsxd r64, r32 (move + sign extend) + COPY_CODES(2); + } else { + // not support yet! + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x66) { + // operand override prefix + COPY_CODES(1); + // This is the same as the x86 version + if (*origBytes >= 0x88 && *origBytes <= 0x8B) { + // various MOVs + unsigned char b = origBytes[1]; + if (((b & 0xc0) == 0xc0) || + (((b & 0xc0) == 0x00) && ((b & 0x07) != 0x04) && + ((b & 0x07) != 0x05))) { + // REG=r, R/M=r or REG=r, R/M=[r] + COPY_CODES(2); + } else if ((b & 0xc0) == 0x40) { + if ((b & 0x07) == 0x04) { + // REG=r, R/M=[SIB + disp8] + COPY_CODES(4); + } else { + // REG=r, R/M=[r + disp8] + COPY_CODES(3); + } + } else { + // complex MOV, bail + MOZ_ASSERT_UNREACHABLE("Unrecognized MOV opcode sequence"); + return; + } + } else if (*origBytes == 0x44 && origBytes[1] == 0x89) { + // mov word ptr [reg+disp8], reg + COPY_CODES(2); + int len = CountModRmSib(origBytes); + if (len < 0) { + // no way to support this yet. + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + COPY_CODES(len); + } + } else if ((*origBytes & 0xf0) == 0x50) { + // 1-byte push/pop + COPY_CODES(1); + } else if (*origBytes == 0x65) { + // GS prefix + // + // The entry of GetKeyState on Windows 10 has the following code. + // 65 48 8b 04 25 30 00 00 00 mov rax,qword ptr gs:[30h] + // (GS prefix + REX + MOV (0x8b) ...) + if (origBytes[1] == 0x48 && + (origBytes[2] >= 0x88 && origBytes[2] <= 0x8b)) { + COPY_CODES(3); + int len = CountModRmSib(origBytes); + if (len < 0) { + // no way to support this yet. + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + COPY_CODES(len); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x80 && origBytes[1] == 0x3d) { + origBytes += 2; + + // cmp byte ptr [rip-relative address], imm8 + // We'll compute the absolute address and do the cmp in r11 + + // push r11 (to save the old value) + tramp.WriteByte(0x49); + tramp.WriteByte(0x53); + + uintptr_t absAddr = origBytes.ReadDisp32AsAbsolute(); + + // mov r11, absolute address + tramp.WriteByte(0x49); + tramp.WriteByte(0xbb); + tramp.WritePointer(absAddr); + + // cmp byte ptr [r11],... + tramp.WriteByte(0x41); + tramp.WriteByte(0x80); + tramp.WriteByte(0x3b); + + // ...imm8 + COPY_CODES(1); + + // pop r11 (doesn't affect the flags from the cmp) + tramp.WriteByte(0x49); + tramp.WriteByte(0x5b); + } else if (*origBytes == 0x90) { + // nop + COPY_CODES(1); + } else if ((*origBytes & 0xf8) == 0xb8) { + // MOV r32, imm32 + COPY_CODES(5); + } else if (*origBytes == 0x33) { + // xor r32, r/m32 + COPY_CODES(2); + } else if (*origBytes == 0xf6) { + // test r/m8, imm8 (used by ntdll on Windows 10 x64) + // (no flags are affected by near jmp since there is no task switch, + // so it is ok for a jmp to be written immediately after a test) + BYTE subOpcode = 0; + int nModRmSibBytes = CountModRmSib(origBytes + 1, &subOpcode); + if (nModRmSibBytes < 0 || subOpcode != 0) { + // Unsupported + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + COPY_CODES(2 + nModRmSibBytes); + } else if (*origBytes == 0x85) { + // test r/m32, r32 + int nModRmSibBytes = CountModRmSib(origBytes + 1); + if (nModRmSibBytes < 0) { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + COPY_CODES(1 + nModRmSibBytes); + } else if (*origBytes == 0xd1 && (origBytes[1] & kMaskMod) == kModReg) { + // bit shifts/rotates : (SA|SH|RO|RC)(R|L) r32 + // (e.g. 0xd1 0xe0 is SAL, 0xd1 0xc8 is ROR) + COPY_CODES(2); + } else if (*origBytes == 0x83 && (origBytes[1] & kMaskMod) == kModReg) { + // ADD|OR|ADC|SBB|AND|SUB|XOR|CMP r, imm8 + COPY_CODES(3); + } else if (*origBytes == 0xc3) { + // ret + COPY_CODES(1); + } else if (*origBytes == 0xcc) { + // int 3 + COPY_CODES(1); + } else if (*origBytes == 0xe8 || *origBytes == 0xe9) { + // CALL (0xe8) or JMP (0xe9) 32bit offset + foundJmp = *origBytes == 0xe9; + ++origBytes; + + if (!GenerateJump(tramp, origBytes.ReadDisp32AsAbsolute(), + foundJmp ? JumpType::Jmp : JumpType::Call)) { + return; + } + } else if (*origBytes >= 0x73 && *origBytes <= 0x75) { + // 73 cb JAE rel8 + // 74 cb JE rel8 + // 75 cb JNE rel8 + const JumpType kJumpTypes[] = {JumpType::Jae, JumpType::Je, + JumpType::Jne}; + auto jumpType = kJumpTypes[*origBytes - 0x73]; + uint8_t offset = origBytes[1]; + + origBytes += 2; + + if (!GenerateJump(tramp, origBytes.OffsetToAbsolute(offset), + jumpType)) { + return; + } + } else if (*origBytes == 0xff) { + uint8_t mod = origBytes[1] & kMaskMod; + uint8_t reg = (origBytes[1] & kMaskReg) >> kRegFieldShift; + uint8_t rm = origBytes[1] & kMaskRm; + if (mod == kModReg && (reg == 0 || reg == 1 || reg == 2 || reg == 6)) { + // INC|DEC|CALL|PUSH r64 + COPY_CODES(2); + } else if (mod == kModNoRegDisp && reg == 2 && + rm == kRmNoRegDispDisp32) { + // FF 15 CALL [disp32] + origBytes += 2; + if (!GenerateJump(tramp, origBytes.ChasePointerFromDisp(), + JumpType::Call)) { + return; + } + } else if (reg == 4) { + // FF /4 (Opcode=ff, REG=4): JMP r/m + if (mod == kModNoRegDisp && rm == kRmNoRegDispDisp32) { + // FF 25 JMP [disp32] + foundJmp = true; + + origBytes += 2; + + uintptr_t jmpDest = origBytes.ChasePointerFromDisp(); + + if (!GenerateJump(tramp, jmpDest, JumpType::Jmp)) { + return; + } + } else { + // JMP r/m except JMP [disp32] + int len = CountModRmSib(origBytes + 1); + if (len < 0) { + // RIP-relative not yet supported + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + + COPY_CODES(len + 1); + + foundJmp = true; + } + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x83 && (origBytes[1] & 0xf8) == 0x60) { + // and [r+d], imm8 + COPY_CODES(5); + } else if (*origBytes == 0xc6) { + // mov [r+d], imm8 + int len = CountModRmSib(origBytes + 1); + if (len < 0) { + // RIP-relative not yet supported + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + COPY_CODES(len + 2); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } +#elif defined(_M_ARM64) + + // The number of bytes required to facilitate a detour depends on the + // proximity of the hook function to the target function. In the best case, + // we can branch within +/- 128MB of the current location, requiring only + // 4 bytes. In the worst case, we need 16 bytes to load an absolute address + // into a register and then branch to it. + const uint32_t bytesRequiredFromDecode = + (mFlags.value() & DetourFlags::eTestOnlyForceShortPatch) + ? 4 + : kWorstCaseBytesRequired; + + while (origBytes.GetOffset() < bytesRequiredFromDecode) { + uintptr_t curPC = origBytes.GetCurrentAbsolute(); + uint32_t curInst = origBytes.ReadNextInstruction(); + + Result<arm64::LoadOrBranch, arm64::PCRelCheckError> pcRelInfo = + arm64::CheckForPCRel(curPC, curInst); + if (pcRelInfo.isErr()) { + if (pcRelInfo.unwrapErr() == + arm64::PCRelCheckError::InstructionNotPCRel) { + // Instruction is not PC-relative, we can just copy it verbatim + tramp.WriteInstruction(curInst); + continue; + } + + // At this point we have determined that there is no decoder available + // for the current, PC-relative, instruction. + + // origBytes is now pointing one instruction past the one that we + // need the trampoline to jump back to. + if (!origBytes.BackUpOneInstruction()) { + return; + } + + break; + } + + // We need to load an absolute address into a particular register + tramp.WriteLoadLiteral(pcRelInfo.inspect().mAbsAddress, + pcRelInfo.inspect().mDestReg); + } + +#else +# error "Unknown processor type" +#endif + + if (origBytes.GetOffset() > 100) { + // printf ("Too big!"); + return; + } + +#if defined(_M_IX86) + if (pJmp32 >= 0) { + // Jump directly to the original target of the jump instead of jumping to + // the original function. Adjust jump target displacement to jump location + // in the trampoline. + tramp.AdjustDisp32AtOffset(pJmp32 + 1, origBytes.GetBaseAddress()); + } else { + tramp.WriteByte(0xe9); // jmp + tramp.WriteDisp32(origBytes.GetAddress()); + } +#elif defined(_M_X64) + // If we found a Jmp, we don't need to add another instruction. However, + // if we found a _conditional_ jump or a CALL (or no control operations + // at all) then we still need to run the rest of aOriginalFunction. + if (!foundJmp) { + if (!GenerateJump(tramp, origBytes.GetAddress(), JumpType::Jmp)) { + return; + } + } +#elif defined(_M_ARM64) + // Let's find out how many bytes we have available to us for patching + uint32_t numBytesForPatching = tramp.GetCurrentExecutableCodeLen(); + + if (!numBytesForPatching) { + // There's nothing we can do + return; + } + + if (tramp.IsNull()) { + // Recursive case + HMODULE targetModule = nullptr; + + if (numBytesForPatching < kWorstCaseBytesRequired) { + if (!::GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast<LPCWSTR>(origBytes.GetBaseAddress()), + &targetModule)) { + return; + } + } + + Maybe<TrampPoolT> maybeTrampPool = DoReserve(targetModule); + MOZ_ASSERT(maybeTrampPool); + if (!maybeTrampPool) { + return; + } + + Maybe<Trampoline<MMPolicyT>> maybeRealTramp( + maybeTrampPool.ref().GetNextTrampoline()); + if (!maybeRealTramp) { + return; + } + + origBytes.Rewind(); + CreateTrampoline(origBytes, maybeTrampPool.ptr(), maybeRealTramp.ref(), + aDest, aOutTramp); + return; + } + + // Write the branch from the trampoline back to the original code + + tramp.WriteLoadLiteral(origBytes.GetAddress(), 16); + tramp.WriteInstruction(arm64::BuildUnconditionalBranchToRegister(16)); +#else +# error "Unsupported processor architecture" +#endif + + // The trampoline is now complete. + void* trampPtr = tramp.EndExecutableCode(); + if (!trampPtr) { + return; + } + +#ifdef _M_X64 + if constexpr (MMPolicyT::kSupportsUnwindInfo) { + DebugOnly<bool> unwindInfoAdded = tramp.AddUnwindInfo( + origBytes.GetBaseAddress(), origBytes.GetOffset()); + MOZ_ASSERT(unwindInfoAdded); + } +#endif // _M_X64 + + WritableTargetFunction<MMPolicyT> target(origBytes.Promote()); + if (!target) { + return; + } + + do { + // Now patch the original function. + // When we're instructed to apply a non-default patch, apply it and exit. + // If non-default patching fails, bail out, no fallback. + // Otherwise, we go straight to the default patch. + +#if defined(_M_X64) + if (use10BytePatch) { + if (!Apply10BytePatch(aTrampPool, trampPtr, target, aDest)) { + return; + } + break; + } +#elif defined(_M_ARM64) + if (numBytesForPatching < kWorstCaseBytesRequired) { + if (!Apply4BytePatch(aTrampPool, trampPtr, target, aDest)) { + return; + } + break; + } +#endif + + PrimitiveT::ApplyDefaultPatch(target, aDest); + } while (false); + + // Output the trampoline, thus signalling that this call was a success. This + // must happen before our patched function can be reached from another + // thread, so before we commit the target code (bug 1838286). + *aOutTramp = trampPtr; + + if (!target.Commit()) { + *aOutTramp = nullptr; + } + } +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_PatcherDetour_h diff --git a/toolkit/xre/dllservices/mozglue/interceptor/PatcherNopSpace.h b/toolkit/xre/dllservices/mozglue/interceptor/PatcherNopSpace.h new file mode 100644 index 0000000000..deee87e0f8 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/PatcherNopSpace.h @@ -0,0 +1,205 @@ +/* -*- 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/. */ + +#ifndef mozilla_interceptor_PatcherNopSpace_h +#define mozilla_interceptor_PatcherNopSpace_h + +#if defined(_M_IX86) + +# include "mozilla/interceptor/PatcherBase.h" + +namespace mozilla { +namespace interceptor { + +template <typename VMPolicy> +class WindowsDllNopSpacePatcher final : public WindowsDllPatcherBase<VMPolicy> { + typedef typename VMPolicy::MMPolicyT MMPolicyT; + + // For remembering the addresses of functions we've patched. + mozilla::Vector<void*> mPatchedFns; + + public: + template <typename... Args> + explicit WindowsDllNopSpacePatcher(Args&&... aArgs) + : WindowsDllPatcherBase<VMPolicy>(std::forward<Args>(aArgs)...) {} + + ~WindowsDllNopSpacePatcher() { Clear(); } + + WindowsDllNopSpacePatcher(const WindowsDllNopSpacePatcher&) = delete; + WindowsDllNopSpacePatcher(WindowsDllNopSpacePatcher&&) = delete; + WindowsDllNopSpacePatcher& operator=(const WindowsDllNopSpacePatcher&) = + delete; + WindowsDllNopSpacePatcher& operator=(WindowsDllNopSpacePatcher&&) = delete; + + void Clear() { + // Restore the mov edi, edi to the beginning of each function we patched. + + for (auto&& ptr : mPatchedFns) { + WritableTargetFunction<MMPolicyT> fn( + this->mVMPolicy, reinterpret_cast<uintptr_t>(ptr), sizeof(uint16_t)); + if (!fn) { + continue; + } + + // mov edi, edi + fn.CommitAndWriteShort(0xff8b); + } + + mPatchedFns.clear(); + } + + /** + * NVIDIA Optimus drivers utilize Microsoft Detours 2.x to patch functions + * in our address space. There is a bug in Detours 2.x that causes it to + * patch at the wrong address when attempting to detour code that is already + * NOP space patched. This function is an effort to detect the presence of + * this NVIDIA code in our address space and disable NOP space patching if it + * is. We also check AppInit_DLLs since this is the mechanism that the Optimus + * drivers use to inject into our process. + */ + static bool IsCompatible() { + // These DLLs are known to have bad interactions with this style of patching + const wchar_t* kIncompatibleDLLs[] = {L"detoured.dll", L"_etoured.dll", + L"nvd3d9wrap.dll", L"nvdxgiwrap.dll"}; + // See if the infringing DLLs are already loaded + for (unsigned int i = 0; i < mozilla::ArrayLength(kIncompatibleDLLs); ++i) { + if (GetModuleHandleW(kIncompatibleDLLs[i])) { + return false; + } + } + if (GetModuleHandleW(L"user32.dll")) { + // user32 is loaded but the infringing DLLs are not, assume we're safe to + // proceed. + return true; + } + // If user32 has not loaded yet, check AppInit_DLLs to ensure that Optimus + // won't be loaded once user32 is initialized. + HKEY hkey = NULL; + if (!RegOpenKeyExW( + HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Windows", 0, + KEY_QUERY_VALUE, &hkey)) { + nsAutoRegKey key(hkey); + DWORD numBytes = 0; + const wchar_t kAppInitDLLs[] = L"AppInit_DLLs"; + // Query for required buffer size + LONG status = RegQueryValueExW(hkey, kAppInitDLLs, nullptr, nullptr, + nullptr, &numBytes); + mozilla::UniquePtr<wchar_t[]> data; + if (!status) { + // Allocate the buffer and query for the actual data + data = mozilla::MakeUnique<wchar_t[]>((numBytes + 1) / sizeof(wchar_t)); + status = RegQueryValueExW(hkey, kAppInitDLLs, nullptr, nullptr, + (LPBYTE)data.get(), &numBytes); + } + if (!status) { + // For each token, split up the filename components and then check the + // name of the file. + const wchar_t kDelimiters[] = L", "; + wchar_t* tokenContext = nullptr; + wchar_t* token = wcstok_s(data.get(), kDelimiters, &tokenContext); + while (token) { + wchar_t fname[_MAX_FNAME] = {0}; + if (!_wsplitpath_s(token, nullptr, 0, nullptr, 0, fname, + mozilla::ArrayLength(fname), nullptr, 0)) { + // nvinit.dll is responsible for bootstrapping the DLL injection, so + // that is the library that we check for here + const wchar_t kNvInitName[] = L"nvinit"; + if (!_wcsnicmp(fname, kNvInitName, + mozilla::ArrayLength(kNvInitName))) { + return false; + } + } + token = wcstok_s(nullptr, kDelimiters, &tokenContext); + } + } + } + return true; + } + + bool AddHook(FARPROC aTargetFn, intptr_t aHookDest, void** aOrigFunc) { + if (!IsCompatible()) { +# if defined(MOZILLA_INTERNAL_API) + NS_WARNING("NOP space patching is unavailable for compatibility reasons"); +# endif + return false; + } + + MOZ_ASSERT(aTargetFn); + if (!aTargetFn) { + return false; + } + + ReadOnlyTargetFunction<MMPolicyT> readOnlyTargetFn( + this->ResolveRedirectedAddress(aTargetFn)); + + if (!WriteHook(readOnlyTargetFn, aHookDest, aOrigFunc)) { + return false; + } + + return mPatchedFns.append( + reinterpret_cast<void*>(readOnlyTargetFn.GetBaseAddress())); + } + + bool WriteHook(const ReadOnlyTargetFunction<MMPolicyT>& aFn, + intptr_t aHookDest, void** aOrigFunc) { + // Ensure we can read and write starting at fn - 5 (for the long jmp we're + // going to write) and ending at fn + 2 (for the short jmp up to the long + // jmp). These bytes may span two pages with different protection. + WritableTargetFunction<MMPolicyT> writableFn(aFn.Promote(7, -5)); + if (!writableFn) { + return false; + } + + // Check that the 5 bytes before the function are NOP's or INT 3's, + const uint8_t nopOrBp[] = {0x90, 0xCC}; + if (!writableFn.template VerifyValuesAreOneOf<uint8_t, 5>(nopOrBp)) { + return false; + } + + // ... and that the first 2 bytes of the function are mov(edi, edi). + // There are two ways to encode the same thing: + // + // 0x89 0xff == mov r/m, r + // 0x8b 0xff == mov r, r/m + // + // where "r" is register and "r/m" is register or memory. + // Windows seems to use 0x8B 0xFF. We include 0x89 0xFF out of paranoia. + + // (These look backwards because little-endian) + const uint16_t possibleEncodings[] = {0xFF8B, 0xFF89}; + if (!writableFn.template VerifyValuesAreOneOf<uint16_t, 1>( + possibleEncodings, 5)) { + return false; + } + + // Write a long jump into the space above the function. + writableFn.WriteByte(0xe9); // jmp + if (!writableFn) { + return false; + } + + writableFn.WriteDisp32(aHookDest); // target + if (!writableFn) { + return false; + } + + // Set aOrigFunc here, because after this point, aHookDest might be called, + // and aHookDest might use the aOrigFunc pointer. + *aOrigFunc = reinterpret_cast<void*>(writableFn.GetCurrentAddress() + + sizeof(uint16_t)); + + // Short jump up into our long jump. + return writableFn.CommitAndWriteShort(0xF9EB); // jmp $-5 + } +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // defined(_M_IX86) + +#endif // mozilla_interceptor_PatcherNopSpace_h diff --git a/toolkit/xre/dllservices/mozglue/interceptor/RangeMap.h b/toolkit/xre/dllservices/mozglue/interceptor/RangeMap.h new file mode 100644 index 0000000000..d45d031613 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/RangeMap.h @@ -0,0 +1,142 @@ +/* -*- 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/. */ + +#ifndef mozilla_interceptor_RangeMap_h +#define mozilla_interceptor_RangeMap_h + +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/mozalloc.h" +#include "mozilla/Span.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Vector.h" + +#include <algorithm> + +namespace mozilla { +namespace interceptor { + +/** + * This class maintains a vector of VMSharingPolicyUnique objects, sorted on + * the memory range that is used for reserving each object. + * + * This is used by VMSharingPolicyShared for creating and looking up VM regions + * that are within proximity of the applicable range. + * + * VMSharingPolicyUnique objects managed by this class are reused whenever + * possible. If no range is required, we just return the first available + * policy. + * + * If no range is required and no policies have yet been allocated, we create + * a new one with a null range as a default. + */ +template <typename MMPolicyT> +class RangeMap final { + private: + /** + * This class is used as the comparison key for sorting and insertion. + */ + class Range { + public: + constexpr Range() : mBase(0), mLimit(0) {} + + explicit Range(const Maybe<Span<const uint8_t>>& aBounds) + : mBase(aBounds ? reinterpret_cast<const uintptr_t>( + MMPolicyT::GetLowerBound(aBounds.ref())) + : 0), + mLimit(aBounds ? reinterpret_cast<const uintptr_t>( + MMPolicyT::GetUpperBoundIncl(aBounds.ref())) + : 0) {} + + Range& operator=(const Range&) = default; + Range(const Range&) = default; + Range(Range&&) = default; + Range& operator=(Range&&) = default; + + bool operator<(const Range& aOther) const { + return mBase < aOther.mBase || + (mBase == aOther.mBase && mLimit < aOther.mLimit); + } + + bool Contains(const Range& aOther) const { + return mBase <= aOther.mBase && mLimit >= aOther.mLimit; + } + + private: + uintptr_t mBase; + uintptr_t mLimit; + }; + + class PolicyInfo final : public Range { + public: + explicit PolicyInfo(const Range& aRange) + : Range(aRange), + mPolicy(MakeUnique<VMSharingPolicyUnique<MMPolicyT>>()) {} + + PolicyInfo(const PolicyInfo&) = delete; + PolicyInfo& operator=(const PolicyInfo&) = delete; + + PolicyInfo(PolicyInfo&& aOther) = default; + PolicyInfo& operator=(PolicyInfo&& aOther) = default; + + VMSharingPolicyUnique<MMPolicyT>* GetPolicy() { return mPolicy.get(); } + + private: + UniquePtr<VMSharingPolicyUnique<MMPolicyT>> mPolicy; + }; + + using VectorType = Vector<PolicyInfo, 0, InfallibleAllocPolicy>; + + public: + constexpr RangeMap() : mPolicies(nullptr) {} + + VMSharingPolicyUnique<MMPolicyT>* GetPolicy( + const Maybe<Span<const uint8_t>>& aBounds) { + Range testRange(aBounds); + + if (!mPolicies) { + mPolicies = new VectorType(); + } + + // If no bounds are specified, we just use the first available policy + if (!aBounds) { + if (mPolicies->empty()) { + if (!mPolicies->append(PolicyInfo(testRange))) { + return nullptr; + } + } + + return GetFirstPolicy(); + } + + // mPolicies is sorted, so we search + auto itr = + std::lower_bound(mPolicies->begin(), mPolicies->end(), testRange); + if (itr != mPolicies->end() && itr->Contains(testRange)) { + return itr->GetPolicy(); + } + + itr = mPolicies->insert(itr, PolicyInfo(testRange)); + + MOZ_ASSERT(std::is_sorted(mPolicies->begin(), mPolicies->end())); + + return itr->GetPolicy(); + } + + private: + VMSharingPolicyUnique<MMPolicyT>* GetFirstPolicy() { + MOZ_RELEASE_ASSERT(mPolicies && !mPolicies->empty()); + return mPolicies->begin()->GetPolicy(); + } + + private: + VectorType* mPolicies; +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_RangeMap_h diff --git a/toolkit/xre/dllservices/mozglue/interceptor/TargetFunction.h b/toolkit/xre/dllservices/mozglue/interceptor/TargetFunction.h new file mode 100644 index 0000000000..b79867edcd --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/TargetFunction.h @@ -0,0 +1,1000 @@ +/* -*- 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/. */ + +#ifndef mozilla_interceptor_TargetFunction_h +#define mozilla_interceptor_TargetFunction_h + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/BinarySearch.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/Maybe.h" + +#include "mozilla/Types.h" +#include "mozilla/Unused.h" +#include "mozilla/Vector.h" + +#include <memory> +#include <type_traits> + +namespace mozilla { +namespace interceptor { + +#if defined(_M_IX86) + +template <typename T> +bool CommitAndWriteShortInternal(const T& aMMPolicy, void* aDest, + uint16_t aValue); + +template <> +inline bool CommitAndWriteShortInternal<MMPolicyInProcess>( + const MMPolicyInProcess& aMMPolicy, void* aDest, uint16_t aValue) { + return aMMPolicy.WriteAtomic(aDest, aValue); +} + +template <> +inline bool CommitAndWriteShortInternal<MMPolicyOutOfProcess>( + const MMPolicyOutOfProcess& aMMPolicy, void* aDest, uint16_t aValue) { + return aMMPolicy.Write(aDest, &aValue, sizeof(uint16_t)); +} + +#endif // defined(_M_IX86) + +// Forward declaration +template <typename MMPolicy> +class ReadOnlyTargetFunction; + +template <typename MMPolicy> +class MOZ_STACK_CLASS WritableTargetFunction final { + class AutoProtect final { + using ProtectParams = std::tuple<uintptr_t, uint32_t>; + + public: + explicit AutoProtect(const MMPolicy& aMMPolicy) : mMMPolicy(aMMPolicy) {} + + AutoProtect(const MMPolicy& aMMPolicy, uintptr_t aAddr, size_t aNumBytes, + uint32_t aNewProt) + : mMMPolicy(aMMPolicy) { + const uint32_t pageSize = mMMPolicy.GetPageSize(); + const uintptr_t limit = aAddr + aNumBytes - 1; + const uintptr_t limitPageNum = limit / pageSize; + const uintptr_t basePageNum = aAddr / pageSize; + const uintptr_t numPagesToChange = limitPageNum - basePageNum + 1; + + // We'll use the base address of the page instead of aAddr + uintptr_t curAddr = basePageNum * pageSize; + + // Now change the protection on each page + for (uintptr_t curPage = 0; curPage < numPagesToChange; + ++curPage, curAddr += pageSize) { + uint32_t prevProt; + if (!aMMPolicy.Protect(reinterpret_cast<void*>(curAddr), pageSize, + aNewProt, &prevProt)) { + Clear(); + return; + } + + // Save the previous protection for curAddr so that we can revert this + // in the destructor. + if (!mProtects.append(std::make_tuple(curAddr, prevProt))) { + Clear(); + return; + } + } + } + + AutoProtect(AutoProtect&& aOther) + : mMMPolicy(aOther.mMMPolicy), mProtects(std::move(aOther.mProtects)) { + aOther.mProtects.clear(); + } + + ~AutoProtect() { Clear(); } + + explicit operator bool() const { return !mProtects.empty(); } + + AutoProtect(const AutoProtect&) = delete; + AutoProtect& operator=(const AutoProtect&) = delete; + AutoProtect& operator=(AutoProtect&&) = delete; + + private: + void Clear() { + const uint32_t pageSize = mMMPolicy.GetPageSize(); + for (auto&& entry : mProtects) { + uint32_t prevProt; + DebugOnly<bool> ok = + mMMPolicy.Protect(reinterpret_cast<void*>(std::get<0>(entry)), + pageSize, std::get<1>(entry), &prevProt); + MOZ_ASSERT(ok); + } + + mProtects.clear(); + } + + private: + const MMPolicy& mMMPolicy; + // We include two entries of inline storage as that is most common in the + // worst case. + Vector<ProtectParams, 2> mProtects; + }; + + public: + /** + * Used to initialize an invalid WritableTargetFunction, thus signalling an + * error. + */ + explicit WritableTargetFunction(const MMPolicy& aMMPolicy) + : mMMPolicy(aMMPolicy), + mFunc(0), + mNumBytes(0), + mOffset(0), + mStartWriteOffset(0), + mAccumulatedStatus(false), + mProtect(aMMPolicy) {} + + WritableTargetFunction(const MMPolicy& aMMPolicy, uintptr_t aFunc, + size_t aNumBytes) + : mMMPolicy(aMMPolicy), + mFunc(aFunc), + mNumBytes(aNumBytes), + mOffset(0), + mStartWriteOffset(0), + mAccumulatedStatus(true), + mProtect(aMMPolicy, aFunc, aNumBytes, PAGE_EXECUTE_READWRITE) {} + + WritableTargetFunction(WritableTargetFunction&& aOther) + : mMMPolicy(aOther.mMMPolicy), + mFunc(aOther.mFunc), + mNumBytes(aOther.mNumBytes), + mOffset(aOther.mOffset), + mStartWriteOffset(aOther.mStartWriteOffset), + mLocalBytes(std::move(aOther.mLocalBytes)), + mAccumulatedStatus(aOther.mAccumulatedStatus), + mProtect(std::move(aOther.mProtect)) { + aOther.mAccumulatedStatus = false; + } + + ~WritableTargetFunction() { + MOZ_ASSERT(mLocalBytes.empty(), "Did you forget to call Commit?"); + } + + WritableTargetFunction(const WritableTargetFunction&) = delete; + WritableTargetFunction& operator=(const WritableTargetFunction&) = delete; + WritableTargetFunction& operator=(WritableTargetFunction&&) = delete; + + /** + * @return true if data was successfully committed. + */ + bool Commit() { + if (!(*this)) { + return false; + } + + if (mLocalBytes.empty()) { + // Nothing to commit, treat like success + return true; + } + + bool ok = + mMMPolicy.Write(reinterpret_cast<void*>(mFunc + mStartWriteOffset), + mLocalBytes.begin(), mLocalBytes.length()); + if (!ok) { + return false; + } + + mMMPolicy.FlushInstructionCache(); + + mStartWriteOffset += mLocalBytes.length(); + + mLocalBytes.clear(); + return true; + } + + explicit operator bool() const { return mProtect && mAccumulatedStatus; } + + void WriteByte(const uint8_t& aValue) { + if (!mLocalBytes.append(aValue)) { + mAccumulatedStatus = false; + return; + } + + mOffset += sizeof(uint8_t); + } + + Maybe<uint8_t> ReadByte() { + // Reading is only permitted prior to any writing + MOZ_ASSERT(mOffset == mStartWriteOffset); + if (mOffset > mStartWriteOffset) { + mAccumulatedStatus = false; + return Nothing(); + } + + uint8_t value; + if (!mMMPolicy.Read(&value, reinterpret_cast<const void*>(mFunc + mOffset), + sizeof(uint8_t))) { + mAccumulatedStatus = false; + return Nothing(); + } + + mOffset += sizeof(uint8_t); + mStartWriteOffset += sizeof(uint8_t); + return Some(value); + } + + Maybe<uintptr_t> ReadEncodedPtr() { + // Reading is only permitted prior to any writing + MOZ_ASSERT(mOffset == mStartWriteOffset); + if (mOffset > mStartWriteOffset) { + mAccumulatedStatus = false; + return Nothing(); + } + + uintptr_t value; + if (!mMMPolicy.Read(&value, reinterpret_cast<const void*>(mFunc + mOffset), + sizeof(uintptr_t))) { + mAccumulatedStatus = false; + return Nothing(); + } + + mOffset += sizeof(uintptr_t); + mStartWriteOffset += sizeof(uintptr_t); + return Some(ReadOnlyTargetFunction<MMPolicy>::DecodePtr(value)); + } + + Maybe<uint32_t> ReadLong() { + // Reading is only permitted prior to any writing + MOZ_ASSERT(mOffset == mStartWriteOffset); + if (mOffset > mStartWriteOffset) { + mAccumulatedStatus = false; + return Nothing(); + } + + uint32_t value; + if (!mMMPolicy.Read(&value, reinterpret_cast<const void*>(mFunc + mOffset), + sizeof(uint32_t))) { + mAccumulatedStatus = false; + return Nothing(); + } + + mOffset += sizeof(uint32_t); + mStartWriteOffset += sizeof(uint32_t); + return Some(value); + } + + void WriteShort(const uint16_t& aValue) { + if (!mLocalBytes.append(reinterpret_cast<const uint8_t*>(&aValue), + sizeof(uint16_t))) { + mAccumulatedStatus = false; + return; + } + + mOffset += sizeof(uint16_t); + } + +#if defined(_M_IX86) + public: + /** + * Commits any dirty writes, and then writes a short, atomically if possible. + * This call may succeed in both inproc and outproc cases, but atomicity + * is only guaranteed in the inproc case. + */ + bool CommitAndWriteShort(const uint16_t aValue) { + // First, commit everything that has been written until now + if (!Commit()) { + return false; + } + + // Now immediately write the short, atomically if inproc + bool ok = CommitAndWriteShortInternal( + mMMPolicy, reinterpret_cast<void*>(mFunc + mStartWriteOffset), aValue); + if (!ok) { + return false; + } + + mMMPolicy.FlushInstructionCache(); + mStartWriteOffset += sizeof(uint16_t); + return true; + } +#endif // defined(_M_IX86) + + void WriteDisp32(const uintptr_t aAbsTarget) { + intptr_t diff = static_cast<intptr_t>(aAbsTarget) - + static_cast<intptr_t>(mFunc + mOffset + sizeof(int32_t)); + + CheckedInt<int32_t> checkedDisp(diff); + MOZ_ASSERT(checkedDisp.isValid()); + if (!checkedDisp.isValid()) { + mAccumulatedStatus = false; + return; + } + + int32_t disp = checkedDisp.value(); + if (!mLocalBytes.append(reinterpret_cast<uint8_t*>(&disp), + sizeof(int32_t))) { + mAccumulatedStatus = false; + return; + } + + mOffset += sizeof(int32_t); + } + +#if defined(_M_X64) || defined(_M_ARM64) + void WriteLong(const uint32_t aValue) { + if (!mLocalBytes.append(reinterpret_cast<const uint8_t*>(&aValue), + sizeof(uint32_t))) { + mAccumulatedStatus = false; + return; + } + + mOffset += sizeof(uint32_t); + } +#endif // defined(_M_X64) + + void WritePointer(const uintptr_t aAbsTarget) { + if (!mLocalBytes.append(reinterpret_cast<const uint8_t*>(&aAbsTarget), + sizeof(uintptr_t))) { + mAccumulatedStatus = false; + return; + } + + mOffset += sizeof(uintptr_t); + } + + /** + * @param aValues N-sized array of type T that specifies the set of values + * that are permissible in the first M bytes of the target + * function at aOffset. + * @return true if M values of type T in the function are members of the + * set specified by aValues. + */ + template <typename T, size_t M, size_t N> + bool VerifyValuesAreOneOf(const T (&aValues)[N], const uint8_t aOffset = 0) { + T buf[M]; + if (!mMMPolicy.Read( + buf, reinterpret_cast<const void*>(mFunc + mOffset + aOffset), + M * sizeof(T))) { + return false; + } + + for (auto&& fnValue : buf) { + bool match = false; + for (auto&& testValue : aValues) { + match |= (fnValue == testValue); + } + + if (!match) { + return false; + } + } + + return true; + } + + uintptr_t GetCurrentAddress() const { return mFunc + mOffset; } + + private: + const MMPolicy& mMMPolicy; + const uintptr_t mFunc; + const size_t mNumBytes; + uint32_t mOffset; + uint32_t mStartWriteOffset; + + // In an ideal world, we'd only read 5 bytes on 32-bit and 13 bytes on 64-bit, + // to match the minimum bytes that we need to write in in order to patch the + // target function. Since the actual opcodes will often require us to pull in + // extra bytes above that minimum, we set the inline storage to be larger than + // those minima in an effort to give the Vector extra wiggle room before it + // needs to touch the heap. +#if defined(_M_IX86) + static const size_t kInlineStorage = 16; +#elif defined(_M_X64) || defined(_M_ARM64) + static const size_t kInlineStorage = 32; +#endif + Vector<uint8_t, kInlineStorage> mLocalBytes; + bool mAccumulatedStatus; + AutoProtect mProtect; +}; + +template <typename MMPolicy> +class ReadOnlyTargetBytes { + public: + ReadOnlyTargetBytes(const MMPolicy& aMMPolicy, const void* aBase) + : mMMPolicy(aMMPolicy), mBase(reinterpret_cast<const uint8_t*>(aBase)) {} + + ReadOnlyTargetBytes(ReadOnlyTargetBytes&& aOther) + : mMMPolicy(aOther.mMMPolicy), mBase(aOther.mBase) {} + + ReadOnlyTargetBytes(const ReadOnlyTargetBytes& aOther, + const uint32_t aOffsetFromOther = 0) + : mMMPolicy(aOther.mMMPolicy), mBase(aOther.mBase + aOffsetFromOther) {} + + void EnsureLimit(uint32_t aDesiredLimit) { + // In the out-proc case we use this function to read the target function's + // bytes in the other process into a local buffer. We don't need that for + // the in-process case because we already have direct access to our target + // function's bytes. + } + + uint32_t TryEnsureLimit(uint32_t aDesiredLimit) { + // Same as EnsureLimit above. We don't need to ensure for the in-process. + return aDesiredLimit; + } + + bool IsValidAtOffset(const int8_t aOffset) const { + if (!aOffset) { + return true; + } + + uintptr_t base = reinterpret_cast<uintptr_t>(mBase); + uintptr_t adjusted = base + aOffset; + uint32_t pageSize = mMMPolicy.GetPageSize(); + + // If |adjusted| is within the same page as |mBase|, we're still valid + if ((base / pageSize) == (adjusted / pageSize)) { + return true; + } + + // Otherwise, let's query |adjusted| + return mMMPolicy.IsPageAccessible(adjusted); + } + + /** + * This returns a pointer to a *potentially local copy* of the target + * function's bytes. The returned pointer should not be used for any + * pointer arithmetic relating to the target function. + */ + const uint8_t* GetLocalBytes() const { return mBase; } + + /** + * This returns a pointer to the target function's bytes. The returned pointer + * may possibly belong to another process, so while it should be used for + * pointer arithmetic, it *must not* be dereferenced. + */ + uintptr_t GetBase() const { return reinterpret_cast<uintptr_t>(mBase); } + + const MMPolicy& GetMMPolicy() const { return mMMPolicy; } + + ReadOnlyTargetBytes& operator=(const ReadOnlyTargetBytes&) = delete; + ReadOnlyTargetBytes& operator=(ReadOnlyTargetBytes&&) = delete; + + private: + const MMPolicy& mMMPolicy; + uint8_t const* const mBase; +}; + +template <> +class ReadOnlyTargetBytes<MMPolicyOutOfProcess> { + public: + ReadOnlyTargetBytes(const MMPolicyOutOfProcess& aMMPolicy, const void* aBase) + : mMMPolicy(aMMPolicy), mBase(reinterpret_cast<const uint8_t*>(aBase)) {} + + ReadOnlyTargetBytes(ReadOnlyTargetBytes&& aOther) + : mMMPolicy(aOther.mMMPolicy), + mLocalBytes(std::move(aOther.mLocalBytes)), + mBase(aOther.mBase) {} + + ReadOnlyTargetBytes(const ReadOnlyTargetBytes& aOther) + : mMMPolicy(aOther.mMMPolicy), mBase(aOther.mBase) { + Unused << mLocalBytes.appendAll(aOther.mLocalBytes); + } + + ReadOnlyTargetBytes(const ReadOnlyTargetBytes& aOther, + const uint32_t aOffsetFromOther) + : mMMPolicy(aOther.mMMPolicy), mBase(aOther.mBase + aOffsetFromOther) { + if (aOffsetFromOther >= aOther.mLocalBytes.length()) { + return; + } + + Unused << mLocalBytes.append(aOther.mLocalBytes.begin() + aOffsetFromOther, + aOther.mLocalBytes.end()); + } + + void EnsureLimit(uint32_t aDesiredLimit) { + size_t prevSize = mLocalBytes.length(); + if (aDesiredLimit < prevSize) { + return; + } + + size_t newSize = aDesiredLimit + 1; + if (newSize < kInlineStorage) { + // Always try to read as much memory as we can at once + newSize = kInlineStorage; + } + + bool resizeOk = mLocalBytes.resize(newSize); + MOZ_RELEASE_ASSERT(resizeOk); + + bool ok = mMMPolicy.Read(&mLocalBytes[prevSize], mBase + prevSize, + newSize - prevSize); + if (ok) { + return; + } + + // We couldn't pull more bytes than needed (which may happen if those extra + // bytes are not accessible). In this case, we try just to get the bare + // minimum. + newSize = aDesiredLimit + 1; + resizeOk = mLocalBytes.resize(newSize); + MOZ_RELEASE_ASSERT(resizeOk); + + ok = mMMPolicy.Read(&mLocalBytes[prevSize], mBase + prevSize, + newSize - prevSize); + MOZ_RELEASE_ASSERT(ok); + } + + // This function tries to ensure as many bytes as possible up to + // |aDesiredLimit| bytes, returning how many bytes were actually ensured. + // As EnsureLimit does, we allocate an extra byte in local to make sure + // mLocalBytes always has at least one byte even though the target memory + // was inaccessible at all. + uint32_t TryEnsureLimit(uint32_t aDesiredLimit) { + size_t prevSize = mLocalBytes.length(); + if (aDesiredLimit < prevSize) { + return aDesiredLimit; + } + + size_t newSize = aDesiredLimit; + if (newSize < kInlineStorage) { + // Always try to read as much memory as we can at once + newSize = kInlineStorage; + } + + bool resizeOk = mLocalBytes.resize(newSize); + MOZ_RELEASE_ASSERT(resizeOk); + + size_t bytesRead = mMMPolicy.TryRead(&mLocalBytes[prevSize], + mBase + prevSize, newSize - prevSize); + + newSize = prevSize + bytesRead; + + resizeOk = mLocalBytes.resize(newSize + 1); + MOZ_RELEASE_ASSERT(resizeOk); + + mLocalBytes[newSize] = 0; + return newSize; + } + + bool IsValidAtOffset(const int8_t aOffset) const { + if (!aOffset) { + return true; + } + + uintptr_t base = reinterpret_cast<uintptr_t>(mBase); + uintptr_t adjusted = base + aOffset; + uint32_t pageSize = mMMPolicy.GetPageSize(); + + // If |adjusted| is within the same page as |mBase|, we're still valid + if ((base / pageSize) == (adjusted / pageSize)) { + return true; + } + + // Otherwise, let's query |adjusted| + return mMMPolicy.IsPageAccessible(adjusted); + } + + /** + * This returns a pointer to a *potentially local copy* of the target + * function's bytes. The returned pointer should not be used for any + * pointer arithmetic relating to the target function. + */ + const uint8_t* GetLocalBytes() const { + if (mLocalBytes.empty()) { + return nullptr; + } + + return mLocalBytes.begin(); + } + + /** + * This returns a pointer to the target function's bytes. The returned pointer + * may possibly belong to another process, so while it should be used for + * pointer arithmetic, it *must not* be dereferenced. + */ + uintptr_t GetBase() const { return reinterpret_cast<uintptr_t>(mBase); } + + const MMPolicyOutOfProcess& GetMMPolicy() const { return mMMPolicy; } + + ReadOnlyTargetBytes& operator=(const ReadOnlyTargetBytes&) = delete; + ReadOnlyTargetBytes& operator=(ReadOnlyTargetBytes&&) = delete; + + private: + // In an ideal world, we'd only read 5 bytes on 32-bit and 13 bytes on 64-bit, + // to match the minimum bytes that we need to write in in order to patch the + // target function. Since the actual opcodes will often require us to pull in + // extra bytes above that minimum, we set the inline storage to be larger than + // those minima in an effort to give the Vector extra wiggle room before it + // needs to touch the heap. +#if defined(_M_IX86) + static const size_t kInlineStorage = 16; +#elif defined(_M_X64) || defined(_M_ARM64) + static const size_t kInlineStorage = 32; +#endif + + const MMPolicyOutOfProcess& mMMPolicy; + Vector<uint8_t, kInlineStorage> mLocalBytes; + uint8_t const* const mBase; +}; + +template <typename MMPolicy> +class TargetBytesPtr { + public: + typedef TargetBytesPtr<MMPolicy> Type; + + static Type Make(const MMPolicy& aMMPolicy, const void* aFunc) { + return TargetBytesPtr(aMMPolicy, aFunc); + } + + static Type CopyFromOffset(const TargetBytesPtr& aOther, + const uint32_t aOffsetFromOther) { + return TargetBytesPtr(aOther, aOffsetFromOther); + } + + ReadOnlyTargetBytes<MMPolicy>* operator->() { return &mTargetBytes; } + + TargetBytesPtr(TargetBytesPtr&& aOther) + : mTargetBytes(std::move(aOther.mTargetBytes)) {} + + TargetBytesPtr(const TargetBytesPtr& aOther) + : mTargetBytes(aOther.mTargetBytes) {} + + TargetBytesPtr& operator=(const TargetBytesPtr&) = delete; + TargetBytesPtr& operator=(TargetBytesPtr&&) = delete; + + private: + TargetBytesPtr(const MMPolicy& aMMPolicy, const void* aFunc) + : mTargetBytes(aMMPolicy, aFunc) {} + + TargetBytesPtr(const TargetBytesPtr& aOther, const uint32_t aOffsetFromOther) + : mTargetBytes(aOther.mTargetBytes, aOffsetFromOther) {} + + ReadOnlyTargetBytes<MMPolicy> mTargetBytes; +}; + +template <> +class TargetBytesPtr<MMPolicyOutOfProcess> { + public: + typedef std::shared_ptr<ReadOnlyTargetBytes<MMPolicyOutOfProcess>> Type; + + static Type Make(const MMPolicyOutOfProcess& aMMPolicy, const void* aFunc) { + return std::make_shared<ReadOnlyTargetBytes<MMPolicyOutOfProcess>>( + aMMPolicy, aFunc); + } + + static Type CopyFromOffset(const Type& aOther, + const uint32_t aOffsetFromOther) { + return std::make_shared<ReadOnlyTargetBytes<MMPolicyOutOfProcess>>( + *aOther, aOffsetFromOther); + } +}; + +template <typename MMPolicy> +class MOZ_STACK_CLASS ReadOnlyTargetFunction final { + public: + ReadOnlyTargetFunction(const MMPolicy& aMMPolicy, const void* aFunc) + : mTargetBytes(TargetBytesPtr<MMPolicy>::Make(aMMPolicy, aFunc)), + mOffset(0) {} + + ReadOnlyTargetFunction(const MMPolicy& aMMPolicy, FARPROC aFunc) + : mTargetBytes(TargetBytesPtr<MMPolicy>::Make( + aMMPolicy, reinterpret_cast<const void*>(aFunc))), + mOffset(0) {} + + ReadOnlyTargetFunction(const MMPolicy& aMMPolicy, uintptr_t aFunc) + : mTargetBytes(TargetBytesPtr<MMPolicy>::Make( + aMMPolicy, reinterpret_cast<const void*>(aFunc))), + mOffset(0) {} + + ReadOnlyTargetFunction(ReadOnlyTargetFunction&& aOther) + : mTargetBytes(std::move(aOther.mTargetBytes)), mOffset(aOther.mOffset) {} + + ReadOnlyTargetFunction& operator=(const ReadOnlyTargetFunction&) = delete; + ReadOnlyTargetFunction& operator=(ReadOnlyTargetFunction&&) = delete; + + ~ReadOnlyTargetFunction() = default; + + ReadOnlyTargetFunction operator+(const uint32_t aOffset) const { + return ReadOnlyTargetFunction(*this, mOffset + aOffset); + } + + uintptr_t GetBaseAddress() const { return mTargetBytes->GetBase(); } + + uintptr_t GetAddress() const { return mTargetBytes->GetBase() + mOffset; } + + uintptr_t AsEncodedPtr() const { + return EncodePtr( + reinterpret_cast<void*>(mTargetBytes->GetBase() + mOffset)); + } + + static uintptr_t EncodePtr(void* aPtr) { + return reinterpret_cast<uintptr_t>(::EncodePointer(aPtr)); + } + + static uintptr_t DecodePtr(uintptr_t aEncodedPtr) { + return reinterpret_cast<uintptr_t>( + ::DecodePointer(reinterpret_cast<PVOID>(aEncodedPtr))); + } + + bool IsValidAtOffset(const int8_t aOffset) const { + return mTargetBytes->IsValidAtOffset(aOffset); + } + +#if defined(_M_ARM64) + + uint32_t ReadNextInstruction() { + mTargetBytes->EnsureLimit(mOffset + sizeof(uint32_t)); + uint32_t instruction = *reinterpret_cast<const uint32_t*>( + mTargetBytes->GetLocalBytes() + mOffset); + mOffset += sizeof(uint32_t); + return instruction; + } + + bool BackUpOneInstruction() { + if (mOffset < sizeof(uint32_t)) { + return false; + } + + mOffset -= sizeof(uint32_t); + return true; + } + +#else + + uint8_t const& operator*() const { + mTargetBytes->EnsureLimit(mOffset); + return *(mTargetBytes->GetLocalBytes() + mOffset); + } + + uint8_t const& operator[](uint32_t aIndex) const { + mTargetBytes->EnsureLimit(mOffset + aIndex); + return *(mTargetBytes->GetLocalBytes() + mOffset + aIndex); + } + + ReadOnlyTargetFunction& operator++() { + ++mOffset; + return *this; + } + + ReadOnlyTargetFunction& operator+=(uint32_t aDelta) { + mOffset += aDelta; + return *this; + } + + uintptr_t ReadDisp32AsAbsolute() { + mTargetBytes->EnsureLimit(mOffset + sizeof(int32_t)); + int32_t disp = *reinterpret_cast<const int32_t*>( + mTargetBytes->GetLocalBytes() + mOffset); + uintptr_t result = + mTargetBytes->GetBase() + mOffset + sizeof(int32_t) + disp; + mOffset += sizeof(int32_t); + return result; + } + + bool IsRelativeShortJump(uintptr_t* aOutTarget) { + if ((*this)[0] == 0xeb) { + int8_t offset = static_cast<int8_t>((*this)[1]); + *aOutTarget = GetAddress() + 2 + offset; + return true; + } + return false; + } + +# if defined(_M_X64) + // Currently this function is used only in x64. + bool IsRelativeNearJump(uintptr_t* aOutTarget) { + if ((*this)[0] == 0xe9) { + *aOutTarget = (*this + 1).ReadDisp32AsAbsolute(); + return true; + } + return false; + } +# endif // defined(_M_X64) + + bool IsIndirectNearJump(uintptr_t* aOutTarget) { + if ((*this)[0] == 0xff && (*this)[1] == 0x25) { +# if defined(_M_X64) + *aOutTarget = (*this + 2).ChasePointerFromDisp(); +# else + *aOutTarget = (*this + 2).template ChasePointer<uintptr_t*>(); +# endif // defined(_M_X64) + return true; + } +# if defined(_M_X64) + else if ((*this)[0] == 0x48 && (*this)[1] == 0xff && (*this)[2] == 0x25) { + // According to Intel SDM, JMP does not have REX.W except JMP m16:64, + // but CPU can execute JMP r/m32 with REX.W. We handle it just in case. + *aOutTarget = (*this + 3).ChasePointerFromDisp(); + return true; + } +# endif // defined(_M_X64) + return false; + } + +#endif // defined(_M_ARM64) + + void Rewind() { mOffset = 0; } + + uint32_t GetOffset() const { return mOffset; } + + uintptr_t OffsetToAbsolute(const uint8_t aOffset) const { + return mTargetBytes->GetBase() + mOffset + aOffset; + } + + uintptr_t GetCurrentAbsolute() const { return OffsetToAbsolute(0); } + + /** + * This method promotes the code referenced by this object to be writable. + * + * @param aLen The length of the function's code to make writable. If set + * to zero, this object's current offset is used as the length. + * @param aOffset The result's base address will be offset from this + * object's base address by |aOffset| bytes. This value may be + * negative. + */ + WritableTargetFunction<MMPolicy> Promote(const uint32_t aLen = 0, + const int8_t aOffset = 0) const { + const uint32_t effectiveLength = aLen ? aLen : mOffset; + MOZ_RELEASE_ASSERT(effectiveLength, + "Cannot Promote a zero-length function"); + + if (!mTargetBytes->IsValidAtOffset(aOffset)) { + return WritableTargetFunction<MMPolicy>(mTargetBytes->GetMMPolicy()); + } + + WritableTargetFunction<MMPolicy> result(mTargetBytes->GetMMPolicy(), + mTargetBytes->GetBase() + aOffset, + effectiveLength); + + return result; + } + + private: + template <typename T> + struct ChasePointerHelper { + template <typename MMPolicy_> + static T Result(const MMPolicy_&, T aValue) { + return aValue; + } + }; + + template <typename T> + struct ChasePointerHelper<T*> { + template <typename MMPolicy_> + static auto Result(const MMPolicy_& aPolicy, T* aValue) { + ReadOnlyTargetFunction<MMPolicy_> ptr(aPolicy, aValue); + return ptr.template ChasePointer<T>(); + } + }; + + public: + // Keep chasing pointers until T is not a pointer type anymore + template <typename T> + auto ChasePointer() { + mTargetBytes->EnsureLimit(mOffset + sizeof(T)); + const std::remove_cv_t<T> result = + *reinterpret_cast<const std::remove_cv_t<T>*>( + mTargetBytes->GetLocalBytes() + mOffset); + return ChasePointerHelper<std::remove_cv_t<T>>::Result( + mTargetBytes->GetMMPolicy(), result); + } + + uintptr_t ChasePointerFromDisp() { + uintptr_t ptrFromDisp = ReadDisp32AsAbsolute(); + ReadOnlyTargetFunction<MMPolicy> ptr( + mTargetBytes->GetMMPolicy(), + reinterpret_cast<const void*>(ptrFromDisp)); + return ptr.template ChasePointer<uintptr_t>(); + } + + private: + ReadOnlyTargetFunction(const ReadOnlyTargetFunction& aOther) + : mTargetBytes(aOther.mTargetBytes), mOffset(aOther.mOffset) {} + + ReadOnlyTargetFunction(const ReadOnlyTargetFunction& aOther, + const uint32_t aOffsetFromOther) + : mTargetBytes(TargetBytesPtr<MMPolicy>::CopyFromOffset( + aOther.mTargetBytes, aOffsetFromOther)), + mOffset(0) {} + + private: + mutable typename TargetBytesPtr<MMPolicy>::Type mTargetBytes; + uint32_t mOffset; +}; + +template <typename MMPolicy, typename T> +class MOZ_STACK_CLASS TargetObject { + mutable typename TargetBytesPtr<MMPolicy>::Type mTargetBytes; + + TargetObject(const MMPolicy& aMMPolicy, const void* aBaseAddress) + : mTargetBytes(TargetBytesPtr<MMPolicy>::Make(aMMPolicy, aBaseAddress)) { + mTargetBytes->EnsureLimit(sizeof(T)); + } + + public: + explicit TargetObject(const MMPolicy& aMMPolicy) + : mTargetBytes(TargetBytesPtr<MMPolicy>::Make(aMMPolicy, nullptr)) {} + + TargetObject(const MMPolicy& aMMPolicy, uintptr_t aBaseAddress) + : TargetObject(aMMPolicy, reinterpret_cast<const void*>(aBaseAddress)) {} + + TargetObject(const TargetObject&) = delete; + TargetObject(TargetObject&&) = delete; + TargetObject& operator=(const TargetObject&) = delete; + TargetObject& operator=(TargetObject&&) = delete; + + explicit operator bool() const { + return mTargetBytes->GetBase() && mTargetBytes->GetLocalBytes(); + } + + const T* operator->() const { + return reinterpret_cast<const T*>(mTargetBytes->GetLocalBytes()); + } + + const T* GetLocalBase() const { + return reinterpret_cast<const T*>(mTargetBytes->GetLocalBytes()); + } +}; + +template <typename MMPolicy, typename T> +class MOZ_STACK_CLASS TargetObjectArray { + mutable typename TargetBytesPtr<MMPolicy>::Type mTargetBytes; + size_t mNumOfItems; + + TargetObjectArray(const MMPolicy& aMMPolicy, const void* aBaseAddress, + size_t aNumOfItems) + : mTargetBytes(TargetBytesPtr<MMPolicy>::Make(aMMPolicy, aBaseAddress)), + mNumOfItems(aNumOfItems) { + uint32_t itemsRead = + mTargetBytes->TryEnsureLimit(sizeof(T) * mNumOfItems) / sizeof(T); + // itemsRead may be bigger than the requested amount because of buffering, + // but mNumOfItems should not include extra bytes of buffering. + if (itemsRead < mNumOfItems) { + mNumOfItems = itemsRead; + } + } + + const T* GetLocalBase() const { + return reinterpret_cast<const T*>(mTargetBytes->GetLocalBytes()); + } + + public: + explicit TargetObjectArray(const MMPolicy& aMMPolicy) + : mTargetBytes(TargetBytesPtr<MMPolicy>::Make(aMMPolicy, nullptr)), + mNumOfItems(0) {} + + TargetObjectArray(const MMPolicy& aMMPolicy, uintptr_t aBaseAddress, + size_t aNumOfItems) + : TargetObjectArray(aMMPolicy, + reinterpret_cast<const void*>(aBaseAddress), + aNumOfItems) {} + + TargetObjectArray(const TargetObjectArray&) = delete; + TargetObjectArray(TargetObjectArray&&) = delete; + TargetObjectArray& operator=(const TargetObjectArray&) = delete; + TargetObjectArray& operator=(TargetObjectArray&&) = delete; + + explicit operator bool() const { + return mTargetBytes->GetBase() && mNumOfItems; + } + + const T* operator[](size_t aIndex) const { + if (aIndex >= mNumOfItems) { + return nullptr; + } + + return &GetLocalBase()[aIndex]; + } + + template <typename Comparator> + bool BinarySearchIf(const Comparator& aCompare, + size_t* aMatchOrInsertionPoint) const { + return mozilla::BinarySearchIf(GetLocalBase(), 0, mNumOfItems, aCompare, + aMatchOrInsertionPoint); + } +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_TargetFunction_h diff --git a/toolkit/xre/dllservices/mozglue/interceptor/Trampoline.h b/toolkit/xre/dllservices/mozglue/interceptor/Trampoline.h new file mode 100644 index 0000000000..befbd47215 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/Trampoline.h @@ -0,0 +1,773 @@ +/* -*- 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/. */ + +#ifndef mozilla_interceptor_Trampoline_h +#define mozilla_interceptor_Trampoline_h + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/Maybe.h" +#include "mozilla/Types.h" +#include "mozilla/WindowsProcessMitigations.h" +#include "mozilla/WindowsUnwindInfo.h" + +namespace mozilla { +namespace interceptor { + +template <typename MMPolicy> +class MOZ_STACK_CLASS Trampoline final { + public: + Trampoline(const MMPolicy* aMMPolicy, uint8_t* const aLocalBase, + const uintptr_t aRemoteBase, const uint32_t aChunkSize) + : mMMPolicy(aMMPolicy), + mPrevLocalProt(0), + mLocalBase(aLocalBase), + mRemoteBase(aRemoteBase), + mOffset(0), + mExeOffset(0), +#ifdef _M_X64 + mCopyCodesEndOffset(0), + mExeEndOffset(0), +#endif // _M_X64 + mMaxOffset(aChunkSize), + mAccumulatedStatus(true) { + if (!::VirtualProtect(aLocalBase, aChunkSize, + MMPolicy::GetTrampWriteProtFlags(), + &mPrevLocalProt)) { + mPrevLocalProt = 0; + } + } + + Trampoline(Trampoline&& aOther) + : mMMPolicy(aOther.mMMPolicy), + mPrevLocalProt(aOther.mPrevLocalProt), + mLocalBase(aOther.mLocalBase), + mRemoteBase(aOther.mRemoteBase), + mOffset(aOther.mOffset), + mExeOffset(aOther.mExeOffset), +#ifdef _M_X64 + mCopyCodesEndOffset(aOther.mCopyCodesEndOffset), + mExeEndOffset(aOther.mExeEndOffset), +#endif // _M_X64 + mMaxOffset(aOther.mMaxOffset), + mAccumulatedStatus(aOther.mAccumulatedStatus) { + aOther.mPrevLocalProt = 0; + aOther.mAccumulatedStatus = false; + } + + MOZ_IMPLICIT Trampoline(decltype(nullptr)) + : mMMPolicy(nullptr), + mPrevLocalProt(0), + mLocalBase(nullptr), + mRemoteBase(0), + mOffset(0), + mExeOffset(0), +#ifdef _M_X64 + mCopyCodesEndOffset(0), + mExeEndOffset(0), +#endif // _M_X64 + mMaxOffset(0), + mAccumulatedStatus(false) { + } + + Trampoline(const Trampoline&) = delete; + Trampoline& operator=(const Trampoline&) = delete; + + Trampoline& operator=(Trampoline&& aOther) { + Clear(); + + mMMPolicy = aOther.mMMPolicy; + mPrevLocalProt = aOther.mPrevLocalProt; + mLocalBase = aOther.mLocalBase; + mRemoteBase = aOther.mRemoteBase; + mOffset = aOther.mOffset; + mExeOffset = aOther.mExeOffset; +#ifdef _M_X64 + mCopyCodesEndOffset = aOther.mCopyCodesEndOffset; + mExeEndOffset = aOther.mExeEndOffset; +#endif // _M_X64 + mMaxOffset = aOther.mMaxOffset; + mAccumulatedStatus = aOther.mAccumulatedStatus; + + aOther.mPrevLocalProt = 0; + aOther.mAccumulatedStatus = false; + + return *this; + } + + ~Trampoline() { Clear(); } + + explicit operator bool() const { + return IsNull() || + (mLocalBase && mRemoteBase && mPrevLocalProt && mAccumulatedStatus); + } + + bool IsNull() const { return !mMMPolicy; } + +#if defined(_M_ARM64) + + void WriteInstruction(uint32_t aInstruction) { + const uint32_t kDelta = sizeof(uint32_t); + + if (!mMMPolicy) { + // Null tramp, just track offset + mOffset += kDelta; + return; + } + + if (mOffset + kDelta > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + *reinterpret_cast<uint32_t*>(mLocalBase + mOffset) = aInstruction; + mOffset += kDelta; + } + + void WriteLoadLiteral(const uintptr_t aAddress, const uint8_t aReg) { + const uint32_t kDelta = sizeof(uint32_t) + sizeof(uintptr_t); + + if (!mMMPolicy) { + // Null tramp, just track offset + mOffset += kDelta; + return; + } + + // We grow the literal pool from the *end* of the tramp, + // so we need to ensure that there is enough room for both an instruction + // and a pointer + if (mOffset + kDelta > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + mMaxOffset -= sizeof(uintptr_t); + *reinterpret_cast<uintptr_t*>(mLocalBase + mMaxOffset) = aAddress; + + CheckedInt<intptr_t> pc(GetCurrentRemoteAddress()); + if (!pc.isValid()) { + mAccumulatedStatus = false; + return; + } + + CheckedInt<intptr_t> literal(reinterpret_cast<uintptr_t>(mLocalBase) + + mMaxOffset); + if (!literal.isValid()) { + mAccumulatedStatus = false; + return; + } + + CheckedInt<intptr_t> ptrOffset = (literal - pc); + if (!ptrOffset.isValid()) { + mAccumulatedStatus = false; + return; + } + + // ptrOffset must be properly aligned + MOZ_ASSERT((ptrOffset.value() % 4) == 0); + ptrOffset /= 4; + + CheckedInt<int32_t> offset(ptrOffset.value()); + if (!offset.isValid()) { + mAccumulatedStatus = false; + return; + } + + // Ensure that offset falls within the range of a signed 19-bit value + if (offset.value() < -0x40000 || offset.value() > 0x3FFFF) { + mAccumulatedStatus = false; + return; + } + + const int32_t kimm19Mask = 0x7FFFF; + int32_t masked = offset.value() & kimm19Mask; + + MOZ_ASSERT(aReg < 32); + uint32_t loadInstr = 0x58000000 | (masked << 5) | aReg; + WriteInstruction(loadInstr); + } + +#else + + void WriteByte(uint8_t aValue) { + const uint32_t kDelta = sizeof(uint8_t); + + if (!mMMPolicy) { + // Null tramp, just track offset + mOffset += kDelta; + return; + } + + if (mOffset >= mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + *(mLocalBase + mOffset) = aValue; + ++mOffset; + } + + void WriteInteger(int32_t aValue) { + const uint32_t kDelta = sizeof(int32_t); + + if (!mMMPolicy) { + // Null tramp, just track offset + mOffset += kDelta; + return; + } + + if (mOffset + kDelta > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + *reinterpret_cast<int32_t*>(mLocalBase + mOffset) = aValue; + mOffset += kDelta; + } + + void WriteDisp32(uintptr_t aAbsTarget) { + const uint32_t kDelta = sizeof(int32_t); + + if (!mMMPolicy) { + // Null tramp, just track offset + mOffset += kDelta; + return; + } + + if (mOffset + kDelta > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + // This needs to be computed from the remote location + intptr_t remoteTrampPosition = static_cast<intptr_t>(mRemoteBase + mOffset); + + intptr_t diff = + static_cast<intptr_t>(aAbsTarget) - (remoteTrampPosition + kDelta); + + CheckedInt<int32_t> checkedDisp(diff); + MOZ_ASSERT(checkedDisp.isValid()); + if (!checkedDisp.isValid()) { + mAccumulatedStatus = false; + return; + } + + int32_t disp = checkedDisp.value(); + *reinterpret_cast<int32_t*>(mLocalBase + mOffset) = disp; + mOffset += kDelta; + } + + void WriteBytes(void* aAddr, size_t aSize) { + if (!mMMPolicy) { + // Null tramp, just track offset + mOffset += aSize; + return; + } + + if (mOffset + aSize > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + std::memcpy(reinterpret_cast<void*>(mLocalBase + mOffset), aAddr, aSize); + mOffset += aSize; + } + +#endif + + void WritePointer(uintptr_t aValue) { + const uint32_t kDelta = sizeof(uintptr_t); + + if (!mMMPolicy) { + // Null tramp, just track offset + mOffset += kDelta; + return; + } + + if (mOffset + kDelta > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + *reinterpret_cast<uintptr_t*>(mLocalBase + mOffset) = aValue; + mOffset += kDelta; + } + + void WriteEncodedPointer(void* aValue) { + uintptr_t encoded = ReadOnlyTargetFunction<MMPolicy>::EncodePtr(aValue); + WritePointer(encoded); + } + + Maybe<uintptr_t> ReadPointer() { + if (mOffset + sizeof(uintptr_t) > mMaxOffset) { + mAccumulatedStatus = false; + return Nothing(); + } + + auto result = Some(*reinterpret_cast<uintptr_t*>(mLocalBase + mOffset)); + mOffset += sizeof(uintptr_t); + return std::move(result); + } + + Maybe<uintptr_t> ReadEncodedPointer() { + Maybe<uintptr_t> encoded(ReadPointer()); + if (!encoded) { + return encoded; + } + + return Some(ReadOnlyTargetFunction<MMPolicy>::DecodePtr(encoded.value())); + } + +#if defined(_M_IX86) + // 32-bit only + void AdjustDisp32AtOffset(uint32_t aOffset, uintptr_t aAbsTarget) { + uint32_t effectiveOffset = mExeOffset + aOffset; + + if (effectiveOffset + sizeof(int32_t) > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + intptr_t diff = static_cast<intptr_t>(aAbsTarget) - + static_cast<intptr_t>(mRemoteBase + mExeOffset); + *reinterpret_cast<int32_t*>(mLocalBase + effectiveOffset) += diff; + } +#endif // defined(_M_IX86) + + void CopyFrom(uintptr_t aOrigBytes, uint32_t aNumBytes) { + if (!mMMPolicy) { + // Null tramp, just track offset + mOffset += aNumBytes; + return; + } + + if (!mMMPolicy || mOffset + aNumBytes > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + if (!mMMPolicy->Read(mLocalBase + mOffset, + reinterpret_cast<void*>(aOrigBytes), aNumBytes)) { + mAccumulatedStatus = false; + return; + } + + mOffset += aNumBytes; + } + + void CopyCodes(uintptr_t aOrigBytes, uint32_t aNumBytes) { +#ifdef _M_X64 + if (mOffset == mCopyCodesEndOffset) { + mCopyCodesEndOffset += aNumBytes; + } +#endif // _M_X64 + CopyFrom(aOrigBytes, aNumBytes); + } + + void Rewind() { mOffset = 0; } + + uintptr_t GetCurrentRemoteAddress() const { return mRemoteBase + mOffset; } + + void StartExecutableCode() { + MOZ_ASSERT(!mExeOffset); + mExeOffset = mOffset; +#ifdef _M_X64 + mCopyCodesEndOffset = mOffset; +#endif // _M_X64 + } + + void* EndExecutableCode() { + if (!mAccumulatedStatus || !mMMPolicy) { + return nullptr; + } + +#ifdef _M_X64 + mExeEndOffset = mOffset; +#endif // _M_X64 + + // This must always return the start address the executable code + // *in the target process* + return reinterpret_cast<void*>(mRemoteBase + mExeOffset); + } + + uint32_t GetCurrentExecutableCodeLen() const { return mOffset - mExeOffset; } + +#ifdef _M_X64 + + void Align(uint32_t aAlignment) { + // aAlignment should be a power of 2 + MOZ_ASSERT(!(aAlignment & (aAlignment - 1))); + + uint32_t alignedOffset = (mOffset + aAlignment - 1) & ~(aAlignment - 1); + if (alignedOffset > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + mOffset = alignedOffset; + } + + // We assume that all instructions that are part of the prologue are left + // intact by detouring code, i.e. that they are copied using CopyCodes. This + // is not true for calls and jumps for example, but calls and jumps cannot be + // part of the prologue. This assumption allows us to copy unwind information + // as-is, because unwind information only refers to instructions within the + // prologue. + bool AddUnwindInfo(uintptr_t aOrigFuncAddr, uintptr_t aOrigFuncStopOffset) { + if constexpr (!MMPolicy::kSupportsUnwindInfo) { + return false; + } + + if (!mMMPolicy) { + return false; + } + + uint32_t origFuncOffsetFromBeginAddr = 0; + uint32_t origFuncOffsetToEndAddr = 0; + uintptr_t origImageBase = 0; + auto unwindInfoData = + mMMPolicy->LookupUnwindInfo(aOrigFuncAddr, &origFuncOffsetFromBeginAddr, + &origFuncOffsetToEndAddr, &origImageBase); + if (!unwindInfoData) { + // If the original function does not have unwind info, there is nothing + // more to do. + return true; + } + + // We do not support hooking at a location that isn't the beginning of a + // function. + MOZ_ASSERT(origFuncOffsetFromBeginAddr == 0); + if (origFuncOffsetFromBeginAddr != 0) { + return false; + } + + IterableUnwindInfo unwindInfoIt(unwindInfoData.get()); + auto& unwindInfo = unwindInfoIt.Info(); + + // The prologue should contain only instructions that we detour using + // CopyCodes. If not, there is most likely a mismatch between the unwind + // information and the actual code we are detouring, so we stop here. This + // is a best-effort safeguard intended to detect situations where e.g. + // third-party injected code would have altered the function we are + // detouring. + if (mCopyCodesEndOffset < aOrigFuncStopOffset && + unwindInfo.size_of_prolog > mCopyCodesEndOffset) { + return false; + } + + // According to the documentation, the array is sorted by descending order + // of offset in the prologue. Let's double check this assumption if in + // debug. This also checks that the full unwind information isn't + // ill-formed, thanks to all the MOZ_ASSERT in iteration code. +# ifdef DEBUG + uint8_t previousOffset = 0xFF; + for (const auto& unwindCode : unwindInfoIt) { + MOZ_ASSERT(unwindCode.offset_in_prolog <= previousOffset); + previousOffset = unwindCode.offset_in_prolog; + } +# endif // DEBUG + + // We skip entries that are not part of the code we have detoured. + // This code relies on the array being sorted by descending order of offset + // in the prolog. + uint8_t firstRelevantCode = 0; + uint8_t countOfCodes = 0; + auto it = unwindInfoIt.begin(); + for (; it != unwindInfoIt.end(); ++it) { + const auto& unwindCode = *it; + if (unwindCode.offset_in_prolog <= aOrigFuncStopOffset) { + // Found a relevant entry + firstRelevantCode = it.Index(); + countOfCodes = unwindInfo.count_of_codes - firstRelevantCode; + break; + } + } + + // Check that we encountered no ill-formed unwind codes. + if (!it.IsValid() && !it.IsAtEnd()) { + return false; + } + + // We do not support chained unwind info. We should add support for chained + // unwind info if we ever reach this assert. Since we hook functions at + // their start address, this should not happen. + if (unwindInfo.flags & UNW_FLAG_CHAININFO) { + MOZ_ASSERT( + false, + "Tried to detour at a location with chained unwind information"); + return false; + } + + // We do not support exception handler info either. This could be a problem + // if we detour code that does not belong to the prologue and contains a + // call instruction, as this handler would then not be found if unwinding + // from callees. The following assert checks that this does not happen. + // + // Our current assumption is that all the functions we hook either have no + // associated exception handlers, or it is __GSHandlerCheck. This handler + // is the most commonly found, for example it is present in LdrLoadDll, + // SendMessageTimeoutW, GetWindowInfo. It is added to functions that use + // stack buffers, in order to mitigate stack buffer overflows. We explain + // below why it is not a problem that we do not preserve __GSHandlerCheck + // information when we detour code. + // + // Preserving exception handler information would raise two challenges: + // + // (1) if the exception handler was not written in a generic way, it may + // behave differently when called for our detoured code compared to + // what it would do if called from the original location of the code; + // (2) the exception handler can be followed by handler-specific data, + // which we cannot copy because we do not know its size. + // + // __GSHandlerCheck checks that the stack cookie value wasn't overwritten + // before continuing to unwind and call further handlers. That is a + // security feature that we want to preserve. However, since these + // functions allocate stack space and write the stack cookie as part of + // their prologue, the 13 bytes that we detour are necessarily part of + // their prologue, which must contain at least the following instructions: + // + // 48 81 ec XX XX XX XX sub rsp, 0xXXXXXXXX + // 48 8b 05 XX XX XX XX mov rax, qword ptr [rip+__security_cookie] + // 48 33 c4 xor rax, rsp + // 48 89 84 24 XX XX XX XX mov qword ptr [RSP + 0xXXXXXXXX],RAX + // + // As a consequence, code associated with __GSHandlerCheck will necessarily + // satisfy (aOrigFuncStopOffset <= unwindInfo.size_of_prolog), and it is OK + // to not preserve handler info in that case. +# ifdef DEBUG + if (aOrigFuncStopOffset > unwindInfo.size_of_prolog) { + MOZ_ASSERT(!(unwindInfo.flags & (UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER))); + } +# endif // DEBUG + + // The unwind info must be DWORD-aligned + Align(sizeof(uint32_t)); + if (!mAccumulatedStatus) { + return false; + } + uintptr_t unwindInfoOffset = mOffset; + + unwindInfo.flags &= + ~(UNW_FLAG_CHAININFO | UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER); + unwindInfo.count_of_codes = countOfCodes; + if (aOrigFuncStopOffset < unwindInfo.size_of_prolog) { + unwindInfo.size_of_prolog = aOrigFuncStopOffset; + } + + WriteBytes(reinterpret_cast<void*>(&unwindInfo), + offsetof(UnwindInfo, unwind_code)); + if (!mAccumulatedStatus) { + return false; + } + + WriteBytes( + reinterpret_cast<void*>(&unwindInfo.unwind_code[firstRelevantCode]), + countOfCodes * sizeof(UnwindCode)); + if (!mAccumulatedStatus) { + return false; + } + + // The function table must be DWORD-aligned + Align(sizeof(uint32_t)); + if (!mAccumulatedStatus) { + return false; + } + uintptr_t functionTableOffset = mOffset; + + WriteInteger(mExeOffset); + if (!mAccumulatedStatus) { + return false; + } + + WriteInteger(mExeEndOffset); + if (!mAccumulatedStatus) { + return false; + } + + WriteInteger(unwindInfoOffset); + if (!mAccumulatedStatus) { + return false; + } + + return mMMPolicy->AddFunctionTable(mRemoteBase + functionTableOffset, 1, + mRemoteBase); + } + +#endif // _M_X64 + + Trampoline<MMPolicy>& operator--() { + MOZ_ASSERT(mOffset); + --mOffset; + return *this; + } + + private: + void Clear() { + if (!mLocalBase || !mPrevLocalProt) { + return; + } + + DebugOnly<bool> ok = !!::VirtualProtect(mLocalBase, mMaxOffset, + mPrevLocalProt, &mPrevLocalProt); + MOZ_ASSERT(ok); + + mLocalBase = nullptr; + mRemoteBase = 0; + mPrevLocalProt = 0; + mAccumulatedStatus = false; + } + + private: + const MMPolicy* mMMPolicy; + DWORD mPrevLocalProt; + uint8_t* mLocalBase; + uintptr_t mRemoteBase; + uint32_t mOffset; + uint32_t mExeOffset; +#ifdef _M_X64 + uint32_t mCopyCodesEndOffset; + uint32_t mExeEndOffset; +#endif // _M_X64 + uint32_t mMaxOffset; + bool mAccumulatedStatus; +}; + +template <typename MMPolicy> +class MOZ_STACK_CLASS TrampolineCollection final { + public: + class MOZ_STACK_CLASS TrampolineIterator final { + public: + Trampoline<MMPolicy> operator*() { + uint32_t offset = mCurTramp * mCollection.mTrampSize; + return Trampoline<MMPolicy>( + &mCollection.mMMPolicy, mCollection.mLocalBase + offset, + mCollection.mRemoteBase + offset, mCollection.mTrampSize); + } + + TrampolineIterator& operator++() { + ++mCurTramp; + return *this; + } + + bool operator!=(const TrampolineIterator& aOther) const { + return mCurTramp != aOther.mCurTramp; + } + + private: + explicit TrampolineIterator( + const TrampolineCollection<MMPolicy>& aCollection, + const uint32_t aCurTramp = 0) + : mCollection(aCollection), mCurTramp(aCurTramp) {} + + const TrampolineCollection<MMPolicy>& mCollection; + uint32_t mCurTramp; + + friend class TrampolineCollection<MMPolicy>; + }; + + explicit TrampolineCollection(const MMPolicy& aMMPolicy) + : mMMPolicy(aMMPolicy), + mLocalBase(0), + mRemoteBase(0), + mTrampSize(0), + mNumTramps(0), + mPrevProt(0), + mCS(nullptr) {} + + TrampolineCollection(const MMPolicy& aMMPolicy, uint8_t* const aLocalBase, + const uintptr_t aRemoteBase, const uint32_t aTrampSize, + const uint32_t aNumTramps) + : mMMPolicy(aMMPolicy), + mLocalBase(aLocalBase), + mRemoteBase(aRemoteBase), + mTrampSize(aTrampSize), + mNumTramps(aNumTramps), + mPrevProt(0), + mCS(nullptr) { + if (!aNumTramps) { + return; + } + + BOOL ok = mMMPolicy.Protect(aLocalBase, aNumTramps * aTrampSize, + PAGE_EXECUTE_READWRITE, &mPrevProt); + if (!ok) { + // When destroying a sandboxed process that uses + // MITIGATION_DYNAMIC_CODE_DISABLE, we won't be allowed to write to our + // executable memory so we just do nothing. If we fail to get access + // to memory for any other reason, we still don't want to crash but we + // do assert. + MOZ_ASSERT(IsDynamicCodeDisabled()); + mNumTramps = 0; + mPrevProt = 0; + } + } + + ~TrampolineCollection() { + if (!mPrevProt) { + return; + } + + mMMPolicy.Protect(mLocalBase, mNumTramps * mTrampSize, mPrevProt, + &mPrevProt); + + if (mCS) { + ::LeaveCriticalSection(mCS); + } + } + + void Lock(CRITICAL_SECTION& aCS) { + if (!mPrevProt || mCS) { + return; + } + + mCS = &aCS; + ::EnterCriticalSection(&aCS); + } + + TrampolineIterator begin() const { + if (!mPrevProt) { + return end(); + } + + return TrampolineIterator(*this); + } + + TrampolineIterator end() const { + return TrampolineIterator(*this, mNumTramps); + } + + TrampolineCollection(const TrampolineCollection&) = delete; + TrampolineCollection& operator=(const TrampolineCollection&) = delete; + TrampolineCollection& operator=(TrampolineCollection&&) = delete; + + TrampolineCollection(TrampolineCollection&& aOther) + : mMMPolicy(aOther.mMMPolicy), + mLocalBase(aOther.mLocalBase), + mRemoteBase(aOther.mRemoteBase), + mTrampSize(aOther.mTrampSize), + mNumTramps(aOther.mNumTramps), + mPrevProt(aOther.mPrevProt), + mCS(aOther.mCS) { + aOther.mPrevProt = 0; + aOther.mCS = nullptr; + } + + private: + const MMPolicy& mMMPolicy; + uint8_t* const mLocalBase; + const uintptr_t mRemoteBase; + const uint32_t mTrampSize; + uint32_t mNumTramps; + uint32_t mPrevProt; + CRITICAL_SECTION* mCS; + + friend class TrampolineIterator; +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_Trampoline_h diff --git a/toolkit/xre/dllservices/mozglue/interceptor/VMSharingPolicies.h b/toolkit/xre/dllservices/mozglue/interceptor/VMSharingPolicies.h new file mode 100644 index 0000000000..8f93f5c1ad --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/VMSharingPolicies.h @@ -0,0 +1,285 @@ +/* -*- 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/. */ + +#ifndef mozilla_interceptor_VMSharingPolicies_h +#define mozilla_interceptor_VMSharingPolicies_h + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/Types.h" + +namespace mozilla { +namespace interceptor { + +/** + * This class is an abstraction of a reservation of virtual address space that + * has been obtained from a VMSharingPolicy via the policy's |Reserve| method. + * + * TrampolinePool allows us to obtain a trampoline without needing to concern + * ourselves with the underlying implementation of the VM sharing policy. + * + * For example, VMSharingPolicyShared delegates to VMSharingPolicyUnique, but + * also requires taking a lock before doing so. By invoking |GetNextTrampoline| + * on a TrampolinePool, the caller does not need to concern themselves with + * that detail. + * + * We accompolish this with a recursive implementation that provides an inner + * TrampolinePool that is provided by the delegated VMSharingPolicy. + */ +template <typename VMPolicyT, typename InnerT> +class MOZ_STACK_CLASS TrampolinePool final { + public: + TrampolinePool(TrampolinePool&& aOther) = default; + + TrampolinePool(VMPolicyT& aVMPolicy, InnerT&& aInner) + : mVMPolicy(aVMPolicy), mInner(std::move(aInner)) {} + + TrampolinePool& operator=(TrampolinePool&& aOther) = delete; + TrampolinePool(const TrampolinePool&) = delete; + TrampolinePool& operator=(const TrampolinePool&) = delete; + + using MMPolicyT = typename VMPolicyT::MMPolicyT; + + Maybe<Trampoline<MMPolicyT>> GetNextTrampoline() { + return mVMPolicy.GetNextTrampoline(mInner); + } + +#if defined(_M_X64) + bool IsInLowest2GB() const { + return mVMPolicy.IsTrampolineSpaceInLowest2GB(mInner); + } +#endif // defined(_M_X64) + + private: + VMPolicyT& mVMPolicy; + InnerT mInner; +}; + +/** + * This specialization is the base case for TrampolinePool, and is used by + * VMSharingPolicyUnique (since that policy does not delegate anything). + */ +template <typename VMPolicyT> +class MOZ_STACK_CLASS TrampolinePool<VMPolicyT, decltype(nullptr)> final { + public: + explicit TrampolinePool(VMPolicyT& aVMPolicy) : mVMPolicy(aVMPolicy) {} + + TrampolinePool(TrampolinePool&& aOther) = default; + + TrampolinePool& operator=(TrampolinePool&& aOther) = delete; + TrampolinePool(const TrampolinePool&) = delete; + TrampolinePool& operator=(const TrampolinePool&) = delete; + + using MMPolicyT = typename VMPolicyT::MMPolicyT; + + Maybe<Trampoline<MMPolicyT>> GetNextTrampoline() { + return mVMPolicy.GetNextTrampoline(); + } + +#if defined(_M_X64) + bool IsInLowest2GB() const { + return mVMPolicy.IsTrampolineSpaceInLowest2GB(); + } +#endif // defined(_M_X64) + + private: + VMPolicyT& mVMPolicy; +}; + +template <typename MMPolicy> +class VMSharingPolicyUnique : public MMPolicy { + using ThisType = VMSharingPolicyUnique<MMPolicy>; + + public: + using PoolType = TrampolinePool<ThisType, decltype(nullptr)>; + + template <typename... Args> + explicit VMSharingPolicyUnique(Args&&... aArgs) + : MMPolicy(std::forward<Args>(aArgs)...), mNextChunkIndex(0) {} + + Maybe<PoolType> Reserve(const uintptr_t aPivotAddr, + const uint32_t aMaxDistanceFromPivot) { + // Win32 allocates VM addresses at a 64KiB granularity, so we might as well + // utilize that entire 64KiB reservation. + uint32_t len = MMPolicy::GetAllocGranularity(); + + Maybe<Span<const uint8_t>> maybeBounds = MMPolicy::SpanFromPivotAndDistance( + len, aPivotAddr, aMaxDistanceFromPivot); + + return Reserve(len, maybeBounds); + } + + Maybe<PoolType> Reserve(const uint32_t aSize, + const Maybe<Span<const uint8_t>>& aBounds) { + uint32_t bytesReserved = MMPolicy::Reserve(aSize, aBounds); + if (!bytesReserved) { + return Nothing(); + } + + return Some(PoolType(*this)); + } + + TrampolineCollection<MMPolicy> Items() const { + return TrampolineCollection<MMPolicy>(*this, this->GetLocalView(), + this->GetRemoteView(), kChunkSize, + mNextChunkIndex); + } + + void Clear() { mNextChunkIndex = 0; } + + ~VMSharingPolicyUnique() = default; + + VMSharingPolicyUnique(const VMSharingPolicyUnique&) = delete; + VMSharingPolicyUnique& operator=(const VMSharingPolicyUnique&) = delete; + + VMSharingPolicyUnique(VMSharingPolicyUnique&& aOther) + : MMPolicy(std::move(aOther)), mNextChunkIndex(aOther.mNextChunkIndex) { + aOther.mNextChunkIndex = 0; + } + + VMSharingPolicyUnique& operator=(VMSharingPolicyUnique&& aOther) { + static_cast<MMPolicy&>(*this) = std::move(aOther); + mNextChunkIndex = aOther.mNextChunkIndex; + aOther.mNextChunkIndex = 0; + return *this; + } + + protected: + // In VMSharingPolicyUnique we do not implement the overload that accepts + // an inner trampoline pool, as this policy is expected to be the + // implementation of the base case. + Maybe<Trampoline<MMPolicy>> GetNextTrampoline() { + uint32_t offset = mNextChunkIndex * kChunkSize; + if (!this->MaybeCommitNextPage(offset, kChunkSize)) { + return Nothing(); + } + + Trampoline<MMPolicy> result(this, this->GetLocalView() + offset, + this->GetRemoteView() + offset, kChunkSize); + if (!!result) { + ++mNextChunkIndex; + } + + return Some(std::move(result)); + } + + private: + uint32_t mNextChunkIndex; + static const uint32_t kChunkSize = 128; + + template <typename VMPolicyT, typename FriendT> + friend class TrampolinePool; +}; + +} // namespace interceptor +} // namespace mozilla + +// We don't include RangeMap.h until this point because it depends on the +// TrampolinePool definitions from above. +#include "mozilla/interceptor/RangeMap.h" + +namespace mozilla { +namespace interceptor { + +// We only support this policy for in-proc MMPolicy. +class MOZ_TRIVIAL_CTOR_DTOR VMSharingPolicyShared : public MMPolicyInProcess { + typedef VMSharingPolicyUnique<MMPolicyInProcess> UniquePolicyT; + typedef VMSharingPolicyShared ThisType; + + public: + using PoolType = TrampolinePool<ThisType, UniquePolicyT::PoolType>; + using MMPolicyT = MMPolicyInProcess; + + constexpr VMSharingPolicyShared() {} + + bool ShouldUnhookUponDestruction() const { return false; } + + Maybe<PoolType> Reserve(const uintptr_t aPivotAddr, + const uint32_t aMaxDistanceFromPivot) { + // Win32 allocates VM addresses at a 64KiB granularity, so we might as well + // utilize that entire 64KiB reservation. + uint32_t len = this->GetAllocGranularity(); + + Maybe<Span<const uint8_t>> maybeBounds = + MMPolicyInProcess::SpanFromPivotAndDistance(len, aPivotAddr, + aMaxDistanceFromPivot); + + AutoCriticalSection lock(GetCS()); + VMSharingPolicyUnique<MMPolicyT>* uniquePol = sVMMap.GetPolicy(maybeBounds); + MOZ_ASSERT(uniquePol); + if (!uniquePol) { + return Nothing(); + } + + Maybe<UniquePolicyT::PoolType> maybeUnique = + uniquePol->Reserve(len, maybeBounds); + if (!maybeUnique) { + return Nothing(); + } + + return Some(PoolType(*this, std::move(maybeUnique.ref()))); + } + + TrampolineCollection<MMPolicyInProcess> Items() const { + // Since ShouldUnhookUponDestruction returns false, this can be empty + return TrampolineCollection<MMPolicyInProcess>(*this); + } + + void Clear() { + // This must be a no-op for shared VM policy; we can't have one interceptor + // wiping out trampolines for all interceptors in the process. + } + + VMSharingPolicyShared(const VMSharingPolicyShared&) = delete; + VMSharingPolicyShared(VMSharingPolicyShared&&) = delete; + VMSharingPolicyShared& operator=(const VMSharingPolicyShared&) = delete; + VMSharingPolicyShared& operator=(VMSharingPolicyShared&&) = delete; + + private: + static CRITICAL_SECTION* GetCS() { + static const bool isAlloc = []() -> bool { + DWORD flags = 0; +#if defined(RELEASE_OR_BETA) + flags |= CRITICAL_SECTION_NO_DEBUG_INFO; +#endif // defined(RELEASE_OR_BETA) + ::InitializeCriticalSectionEx(&sCS, 4000, flags); + return true; + }(); + Unused << isAlloc; + + return &sCS; + } + + // In VMSharingPolicyShared, we only implement the overload that accepts + // a VMSharingPolicyUnique trampoline pool as |aInner|, since we require the + // former policy to wrap the latter. + Maybe<Trampoline<MMPolicyInProcess>> GetNextTrampoline( + UniquePolicyT::PoolType& aInner) { + AutoCriticalSection lock(GetCS()); + return aInner.GetNextTrampoline(); + } + +#if defined(_M_X64) + bool IsTrampolineSpaceInLowest2GB( + const UniquePolicyT::PoolType& aInner) const { + AutoCriticalSection lock(GetCS()); + return aInner.IsInLowest2GB(); + } +#endif // defined(_M_X64) + + private: + template <typename VMPolicyT, typename InnerT> + friend class TrampolinePool; + + inline static RangeMap<MMPolicyInProcess> sVMMap; + inline static CRITICAL_SECTION sCS; +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_VMSharingPolicies_h diff --git a/toolkit/xre/dllservices/mozglue/interceptor/moz.build b/toolkit/xre/dllservices/mozglue/interceptor/moz.build new file mode 100644 index 0000000000..561e33b147 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/moz.build @@ -0,0 +1,26 @@ +# -*- 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/. + +EXPORTS.mozilla.interceptor += [ + "Arm64.h", + "MMPolicies.h", + "PatcherBase.h", + "PatcherDetour.h", + "PatcherNopSpace.h", + "RangeMap.h", + "TargetFunction.h", + "Trampoline.h", + "VMSharingPolicies.h", +] + +if CONFIG["CPU_ARCH"] == "aarch64": + Library("interceptor") + + FINAL_LIBRARY = "mozglue" + + UNIFIED_SOURCES += [ + "Arm64.cpp", + ] diff --git a/toolkit/xre/dllservices/mozglue/moz.build b/toolkit/xre/dllservices/mozglue/moz.build new file mode 100644 index 0000000000..72b39a6b8a --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/moz.build @@ -0,0 +1,80 @@ +# -*- 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/. + +Library("dllservices_mozglue") + +if CONFIG["MOZ_WIDGET_TOOLKIT"]: + SOURCES += [ + # This file contains a |using namespace mozilla;| statement + "WindowsDllBlocklist.cpp", + ] + + UNIFIED_SOURCES += [ + "Authenticode.cpp", + "LoaderObserver.cpp", + "ModuleLoadFrame.cpp", + "WindowsBCryptInitialization.cpp", + "WindowsFallbackLoaderAPI.cpp", + ] + +if not CONFIG["JS_STANDALONE"]: + UNIFIED_SOURCES += [ + "CacheNtDllThunk.cpp", + ] + +OS_LIBS += [ + "crypt32", + "ntdll", + "version", + "wintrust", +] + +DELAYLOAD_DLLS += [ + "crypt32.dll", + "wintrust.dll", +] + +EXPORTS += [ + "nsWindowsDllInterceptor.h", +] + +EXPORTS.mozilla += [ + "Authenticode.h", + "CacheNtDllThunk.h", + "LoaderAPIInterfaces.h", + "ModuleLoadInfo.h", + "WindowsBCryptInitialization.h", + "WindowsDllBlocklist.h", + "WindowsDllBlocklistCommon.h", + "WindowsDllBlocklistInfo.h", +] + +EXPORTS.mozilla.glue += [ + "SharedSection.h", + "WindowsDllServices.h", +] + +# Generate DLL Blocklists +blocklist_header_types = ["A11y", "Launcher", "Legacy", "Test"] +blocklist_file_leaf_tpl = "WindowsDllBlocklist{0}Defs.h" +blocklist_files = tuple( + [blocklist_file_leaf_tpl.format(type) for type in blocklist_header_types] +) + +GeneratedFile( + *blocklist_files, + script="gen_dll_blocklist_defs.py", + entry_point="gen_blocklists", + inputs=["WindowsDllBlocklistDefs.in"] +) + +EXPORTS.mozilla += ["!" + hdr for hdr in blocklist_files] + +DIRS += [ + "interceptor", +] + +FINAL_LIBRARY = "mozglue" diff --git a/toolkit/xre/dllservices/mozglue/nsWindowsDllInterceptor.h b/toolkit/xre/dllservices/mozglue/nsWindowsDllInterceptor.h new file mode 100644 index 0000000000..1211b49526 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/nsWindowsDllInterceptor.h @@ -0,0 +1,819 @@ +/* -*- 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/. */ + +#ifndef NS_WINDOWS_DLL_INTERCEPTOR_H_ +#define NS_WINDOWS_DLL_INTERCEPTOR_H_ + +#include <wchar.h> +#include <windows.h> +#include <winternl.h> + +#include <utility> + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/NativeNt.h" + +#include "mozilla/Types.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Vector.h" +#include "mozilla/interceptor/MMPolicies.h" +#include "mozilla/interceptor/PatcherDetour.h" +#include "mozilla/interceptor/PatcherNopSpace.h" +#include "mozilla/interceptor/VMSharingPolicies.h" +#include "nsWindowsHelpers.h" + +/* + * Simple function interception. + * + * We have two separate mechanisms for intercepting a function: We can use the + * built-in nop space, if it exists, or we can create a detour. + * + * Using the built-in nop space works as follows: On x86-32, DLL functions + * begin with a two-byte nop (mov edi, edi) and are preceeded by five bytes of + * NOP instructions. + * + * When we detect a function with this prelude, we do the following: + * + * 1. Write a long jump to our interceptor function into the five bytes of NOPs + * before the function. + * + * 2. Write a short jump -5 into the two-byte nop at the beginning of the + * function. + * + * This mechanism is nice because it's thread-safe. It's even safe to do if + * another thread is currently running the function we're modifying! + * + * When the WindowsDllNopSpacePatcher is destroyed, we overwrite the short jump + * but not the long jump, so re-intercepting the same function won't work, + * because its prelude won't match. + * + * + * Unfortunately nop space patching doesn't work on functions which don't have + * this magic prelude (and in particular, x86-64 never has the prelude). So + * when we can't use the built-in nop space, we fall back to using a detour, + * which works as follows: + * + * 1. Save first N bytes of OrigFunction to trampoline, where N is a + * number of bytes >= 5 that are instruction aligned. + * + * 2. Replace first 5 bytes of OrigFunction with a jump to the Hook + * function. + * + * 3. After N bytes of the trampoline, add a jump to OrigFunction+N to + * continue original program flow. + * + * 4. Hook function needs to call the trampoline during its execution, + * to invoke the original function (so address of trampoline is + * returned). + * + * When the WindowsDllDetourPatcher object is destructed, OrigFunction is + * patched again to jump directly to the trampoline instead of going through + * the hook function. As such, re-intercepting the same function won't work, as + * jump instructions are not supported. + * + * Note that this is not thread-safe. Sad day. + * + */ + +#if defined(_M_IX86) && defined(__clang__) && __has_declspec_attribute(guard) +// On x86, nop-space patches return to the second instruction of their target. +// This is a deliberate violation of Control Flow Guard, so disable the check. +# define INTERCEPTOR_DISABLE_CFGUARD __declspec(guard(nocf)) +#else +# define INTERCEPTOR_DISABLE_CFGUARD /* nothing */ +#endif + +namespace mozilla { +namespace interceptor { + +template <typename T> +struct OriginalFunctionPtrTraits; + +template <typename R, typename... Args> +struct OriginalFunctionPtrTraits<R (*)(Args...)> { + using ReturnType = R; +}; + +#if defined(_M_IX86) +template <typename R, typename... Args> +struct OriginalFunctionPtrTraits<R(__stdcall*)(Args...)> { + using ReturnType = R; +}; + +template <typename R, typename... Args> +struct OriginalFunctionPtrTraits<R(__fastcall*)(Args...)> { + using ReturnType = R; +}; +#endif // defined(_M_IX86) + +template <typename InterceptorT, typename FuncPtrT> +class FuncHook final { + public: + using ThisType = FuncHook<InterceptorT, FuncPtrT>; + using ReturnType = typename OriginalFunctionPtrTraits<FuncPtrT>::ReturnType; + + constexpr FuncHook() : mOrigFunc(nullptr), mInitOnce(INIT_ONCE_STATIC_INIT) {} + + ~FuncHook() = default; + + bool Set(InterceptorT& aInterceptor, const char* aName, FuncPtrT aHookDest) { + LPVOID addHookOk = nullptr; + InitOnceContext ctx(this, &aInterceptor, aName, aHookDest, false); + + return ::InitOnceExecuteOnce(&mInitOnce, &InitOnceCallback, &ctx, + &addHookOk) && + addHookOk; + } + + bool SetDetour(InterceptorT& aInterceptor, const char* aName, + FuncPtrT aHookDest) { + LPVOID addHookOk = nullptr; + InitOnceContext ctx(this, &aInterceptor, aName, aHookDest, true); + + return ::InitOnceExecuteOnce(&mInitOnce, &InitOnceCallback, &ctx, + &addHookOk) && + addHookOk; + } + + explicit operator bool() const { return !!mOrigFunc; } + + template <typename... ArgsType> + INTERCEPTOR_DISABLE_CFGUARD ReturnType operator()(ArgsType&&... aArgs) const { + return mOrigFunc(std::forward<ArgsType>(aArgs)...); + } + + FuncPtrT GetStub() const { return mOrigFunc; } + + // One-time init stuff cannot be moved or copied + FuncHook(const FuncHook&) = delete; + FuncHook(FuncHook&&) = delete; + FuncHook& operator=(const FuncHook&) = delete; + FuncHook& operator=(FuncHook&& aOther) = delete; + + private: + struct MOZ_RAII InitOnceContext final { + InitOnceContext(ThisType* aHook, InterceptorT* aInterceptor, + const char* aName, FuncPtrT aHookDest, bool aForceDetour) + : mHook(aHook), + mInterceptor(aInterceptor), + mName(aName), + mHookDest(reinterpret_cast<void*>(aHookDest)), + mForceDetour(aForceDetour) {} + + ThisType* mHook; + InterceptorT* mInterceptor; + const char* mName; + void* mHookDest; + bool mForceDetour; + }; + + private: + bool Apply(InterceptorT* aInterceptor, const char* aName, void* aHookDest) { + return aInterceptor->AddHook(aName, reinterpret_cast<intptr_t>(aHookDest), + reinterpret_cast<void**>(&mOrigFunc)); + } + + bool ApplyDetour(InterceptorT* aInterceptor, const char* aName, + void* aHookDest) { + return aInterceptor->AddDetour(aName, reinterpret_cast<intptr_t>(aHookDest), + reinterpret_cast<void**>(&mOrigFunc)); + } + + static BOOL CALLBACK InitOnceCallback(PINIT_ONCE aInitOnce, PVOID aParam, + PVOID* aOutContext) { + MOZ_ASSERT(aOutContext); + + bool result; + auto ctx = reinterpret_cast<InitOnceContext*>(aParam); + if (ctx->mForceDetour) { + result = ctx->mHook->ApplyDetour(ctx->mInterceptor, ctx->mName, + ctx->mHookDest); + } else { + result = ctx->mHook->Apply(ctx->mInterceptor, ctx->mName, ctx->mHookDest); + } + + *aOutContext = + result ? reinterpret_cast<PVOID>(1U << INIT_ONCE_CTX_RESERVED_BITS) + : nullptr; + return TRUE; + } + + private: + FuncPtrT mOrigFunc; + INIT_ONCE mInitOnce; +}; + +template <typename InterceptorT, typename FuncPtrT> +class MOZ_ONLY_USED_TO_AVOID_STATIC_CONSTRUCTORS FuncHookCrossProcess final { + public: + using ThisType = FuncHookCrossProcess<InterceptorT, FuncPtrT>; + using ReturnType = typename OriginalFunctionPtrTraits<FuncPtrT>::ReturnType; + +#if defined(DEBUG) + FuncHookCrossProcess() {} +#endif // defined(DEBUG) + + bool Set(nt::CrossExecTransferManager& aTransferMgr, + InterceptorT& aInterceptor, const char* aName, FuncPtrT aHookDest) { + FuncPtrT origFunc; + if (!aInterceptor.AddHook(aName, reinterpret_cast<intptr_t>(aHookDest), + reinterpret_cast<void**>(&origFunc))) { + return false; + } + + return CopyStubToChildProcess(aTransferMgr, aInterceptor, origFunc); + } + + bool SetDetour(nt::CrossExecTransferManager& aTransferMgr, + InterceptorT& aInterceptor, const char* aName, + FuncPtrT aHookDest) { + FuncPtrT origFunc; + if (!aInterceptor.AddDetour(aName, reinterpret_cast<intptr_t>(aHookDest), + reinterpret_cast<void**>(&origFunc))) { + return false; + } + + return CopyStubToChildProcess(aTransferMgr, aInterceptor, origFunc); + } + + explicit operator bool() const { return !!mOrigFunc; } + + /** + * NB: This operator is only meaningful when invoked in the target process! + */ + template <typename... ArgsType> + ReturnType operator()(ArgsType&&... aArgs) const { + return mOrigFunc(std::forward<ArgsType>(aArgs)...); + } + +#if defined(DEBUG) + FuncHookCrossProcess(const FuncHookCrossProcess&) = delete; + FuncHookCrossProcess(FuncHookCrossProcess&&) = delete; + FuncHookCrossProcess& operator=(const FuncHookCrossProcess&) = delete; + FuncHookCrossProcess& operator=(FuncHookCrossProcess&& aOther) = delete; +#endif // defined(DEBUG) + + private: + bool CopyStubToChildProcess(nt::CrossExecTransferManager& aTransferMgr, + InterceptorT& aInterceptor, FuncPtrT aStub) { + LauncherVoidResult writeResult = + aTransferMgr.Transfer(&mOrigFunc, &aStub, sizeof(FuncPtrT)); + if (writeResult.isErr()) { +#ifdef MOZ_USE_LAUNCHER_ERROR + const mozilla::WindowsError& err = writeResult.inspectErr().mError; +#else + const mozilla::WindowsError& err = writeResult.inspectErr(); +#endif + aInterceptor.SetLastDetourError(FUNCHOOKCROSSPROCESS_COPYSTUB_ERROR, + err.AsHResult()); + return false; + } + return true; + } + + private: + FuncPtrT mOrigFunc; +}; + +template <typename MMPolicyT, typename InterceptorT> +struct TypeResolver; + +template <typename InterceptorT> +struct TypeResolver<mozilla::interceptor::MMPolicyInProcess, InterceptorT> { + template <typename FuncPtrT> + using FuncHookType = FuncHook<InterceptorT, FuncPtrT>; +}; + +template <typename InterceptorT> +struct TypeResolver<mozilla::interceptor::MMPolicyOutOfProcess, InterceptorT> { + template <typename FuncPtrT> + using FuncHookType = FuncHookCrossProcess<InterceptorT, FuncPtrT>; +}; + +template <typename VMPolicy = mozilla::interceptor::VMSharingPolicyShared> +class WindowsDllInterceptor final + : public TypeResolver<typename VMPolicy::MMPolicyT, + WindowsDllInterceptor<VMPolicy>> { + typedef WindowsDllInterceptor<VMPolicy> ThisType; + + interceptor::WindowsDllDetourPatcher<VMPolicy> mDetourPatcher; +#if defined(_M_IX86) + interceptor::WindowsDllNopSpacePatcher<typename VMPolicy::MMPolicyT> + mNopSpacePatcher; +#endif // defined(_M_IX86) + + HMODULE mModule; + + public: + template <typename... Args> + explicit WindowsDllInterceptor(Args&&... aArgs) + : mDetourPatcher(std::forward<Args>(aArgs)...) +#if defined(_M_IX86) + , + mNopSpacePatcher(std::forward<Args>(aArgs)...) +#endif // defined(_M_IX86) + , + mModule(nullptr) { + } + + WindowsDllInterceptor(const WindowsDllInterceptor&) = delete; + WindowsDllInterceptor(WindowsDllInterceptor&&) = delete; + WindowsDllInterceptor& operator=(const WindowsDllInterceptor&) = delete; + WindowsDllInterceptor& operator=(WindowsDllInterceptor&&) = delete; + + ~WindowsDllInterceptor() { Clear(); } + + template <size_t N> + void Init(const char (&aModuleName)[N]) { + wchar_t moduleName[N]; + + for (size_t i = 0; i < N; ++i) { + MOZ_ASSERT(!(aModuleName[i] & 0x80), + "Use wide-character overload for non-ASCII module names"); + moduleName[i] = aModuleName[i]; + } + + Init(moduleName); + } + + void Init(const wchar_t* aModuleName) { + if (mModule) { + return; + } + + mModule = ::LoadLibraryW(aModuleName); + } + + /** Force a specific configuration for testing purposes. NOT to be used in + production code! **/ + void TestOnlyDetourInit(const wchar_t* aModuleName, DetourFlags aFlags) { + Init(aModuleName); + mDetourPatcher.Init(aFlags); + } + + void Clear() { + if (!mModule) { + return; + } + +#if defined(_M_IX86) + mNopSpacePatcher.Clear(); +#endif // defined(_M_IX86) + mDetourPatcher.Clear(); + + // NB: We intentionally leak mModule + } + +#if defined(NIGHTLY_BUILD) + const Maybe<DetourError>& GetLastDetourError() const { + return mDetourPatcher.GetLastDetourError(); + } +#endif // defined(NIGHTLY_BUILD) + template <typename... Args> + void SetLastDetourError(Args&&... aArgs) { + return mDetourPatcher.SetLastDetourError(std::forward<Args>(aArgs)...); + } + + constexpr static uint32_t GetWorstCaseRequiredBytesToPatch() { + return WindowsDllDetourPatcherPrimitive< + typename VMPolicy::MMPolicyT>::GetWorstCaseRequiredBytesToPatch(); + } + + private: + /** + * Hook/detour the method aName from the DLL we set in Init so that it calls + * aHookDest instead. Returns the original method pointer in aOrigFunc + * and returns true if successful. + * + * IMPORTANT: If you use this method, please add your case to the + * TestDllInterceptor in order to detect future failures. Even if this + * succeeds now, updates to the hooked DLL could cause it to fail in + * the future. + */ + bool AddHook(const char* aName, intptr_t aHookDest, void** aOrigFunc) { + // Use a nop space patch if possible, otherwise fall back to a detour. + // This should be the preferred method for adding hooks. + if (!mModule) { + mDetourPatcher.SetLastDetourError(DetourResultCode::INTERCEPTOR_MOD_NULL); + return false; + } + + if (!mDetourPatcher.IsPageAccessible( + nt::PEHeaders::HModuleToBaseAddr<uintptr_t>(mModule))) { + mDetourPatcher.SetLastDetourError( + DetourResultCode::INTERCEPTOR_MOD_INACCESSIBLE); + return false; + } + + FARPROC proc = mDetourPatcher.GetProcAddress(mModule, aName); + if (!proc) { + mDetourPatcher.SetLastDetourError( + DetourResultCode::INTERCEPTOR_PROC_NULL); + return false; + } + + if (!mDetourPatcher.IsPageAccessible(reinterpret_cast<uintptr_t>(proc))) { + mDetourPatcher.SetLastDetourError( + DetourResultCode::INTERCEPTOR_PROC_INACCESSIBLE); + return false; + } + +#if defined(_M_IX86) + if (mNopSpacePatcher.AddHook(proc, aHookDest, aOrigFunc)) { + return true; + } +#endif // defined(_M_IX86) + + return AddDetour(proc, aHookDest, aOrigFunc); + } + + /** + * Detour the method aName from the DLL we set in Init so that it calls + * aHookDest instead. Returns the original method pointer in aOrigFunc + * and returns true if successful. + * + * IMPORTANT: If you use this method, please add your case to the + * TestDllInterceptor in order to detect future failures. Even if this + * succeeds now, updates to the detoured DLL could cause it to fail in + * the future. + */ + bool AddDetour(const char* aName, intptr_t aHookDest, void** aOrigFunc) { + // Generally, code should not call this method directly. Use AddHook unless + // there is a specific need to avoid nop space patches. + if (!mModule) { + mDetourPatcher.SetLastDetourError(DetourResultCode::INTERCEPTOR_MOD_NULL); + return false; + } + + if (!mDetourPatcher.IsPageAccessible( + nt::PEHeaders::HModuleToBaseAddr<uintptr_t>(mModule))) { + mDetourPatcher.SetLastDetourError( + DetourResultCode::INTERCEPTOR_MOD_INACCESSIBLE); + return false; + } + + FARPROC proc = mDetourPatcher.GetProcAddress(mModule, aName); + if (!proc) { + mDetourPatcher.SetLastDetourError( + DetourResultCode::INTERCEPTOR_PROC_NULL); + return false; + } + + if (!mDetourPatcher.IsPageAccessible(reinterpret_cast<uintptr_t>(proc))) { + mDetourPatcher.SetLastDetourError( + DetourResultCode::INTERCEPTOR_PROC_INACCESSIBLE); + return false; + } + + return AddDetour(proc, aHookDest, aOrigFunc); + } + + bool AddDetour(FARPROC aProc, intptr_t aHookDest, void** aOrigFunc) { + MOZ_ASSERT(mModule && aProc); + + if (!mDetourPatcher.Initialized()) { + DetourFlags flags = DetourFlags::eDefault; +#if defined(_M_X64) + // NTDLL hooks should attempt to use a 10-byte patch because some + // injected DLLs do the same and interfere with our stuff. + bool needs10BytePatch = (mModule == ::GetModuleHandleW(L"ntdll.dll")); + + bool isWin8Or81 = IsWin8OrLater() && (!IsWin10OrLater()); + bool isWin8 = IsWin8OrLater() && (!IsWin8Point1OrLater()); + + bool isKernel32Dll = (mModule == ::GetModuleHandleW(L"kernel32.dll")); + + bool isDuplicateHandle = (reinterpret_cast<void*>(aProc) == + reinterpret_cast<void*>(&::DuplicateHandle)); + + // CloseHandle on Windows 8/8.1 only accomodates 10-byte patches. + needs10BytePatch |= isWin8Or81 && isKernel32Dll && + (reinterpret_cast<void*>(aProc) == + reinterpret_cast<void*>(&CloseHandle)); + + // CreateFileA and DuplicateHandle on Windows 8 require 10-byte patches. + needs10BytePatch |= isWin8 && isKernel32Dll && + ((reinterpret_cast<void*>(aProc) == + reinterpret_cast<void*>(&::CreateFileA)) || + isDuplicateHandle); + + if (needs10BytePatch) { + flags |= DetourFlags::eEnable10BytePatch; + } + + if (isWin8 && isDuplicateHandle) { + // Because we can't detour Win8's KERNELBASE!DuplicateHandle, + // we detour kernel32!DuplicateHandle (See bug 1659398). + flags |= DetourFlags::eDontResolveRedirection; + } +#endif // defined(_M_X64) + + mDetourPatcher.Init(flags); + } + + return mDetourPatcher.AddHook(aProc, aHookDest, aOrigFunc); + } + + private: + template <typename InterceptorT, typename FuncPtrT> + friend class FuncHook; + + template <typename InterceptorT, typename FuncPtrT> + friend class FuncHookCrossProcess; +}; + +/** + * IAT patching is intended for use when we only want to intercept a function + * call originating from a specific module. + */ +class WindowsIATPatcher final { + public: + template <typename FuncPtrT> + using FuncHookType = FuncHook<WindowsIATPatcher, FuncPtrT>; + + private: + static bool CheckASCII(const char* aInStr) { + while (*aInStr) { + if (*aInStr & 0x80) { + return false; + } + ++aInStr; + } + return true; + } + + static bool AddHook(HMODULE aFromModule, const char* aToModuleName, + const char* aTargetFnName, void* aHookDest, + Atomic<void*>* aOutOrigFunc) { + if (!aFromModule || !aToModuleName || !aTargetFnName || !aOutOrigFunc) { + return false; + } + + // PE Spec requires ASCII names for imported module names + const bool isModuleNameAscii = CheckASCII(aToModuleName); + MOZ_ASSERT(isModuleNameAscii); + if (!isModuleNameAscii) { + return false; + } + + // PE Spec requires ASCII names for imported function names + const bool isTargetFnNameAscii = CheckASCII(aTargetFnName); + MOZ_ASSERT(isTargetFnNameAscii); + if (!isTargetFnNameAscii) { + return false; + } + + nt::PEHeaders headers(aFromModule); + if (!headers) { + return false; + } + + PIMAGE_IMPORT_DESCRIPTOR impDesc = + headers.GetImportDescriptor(aToModuleName); + if (!nt::PEHeaders::IsValid(impDesc)) { + // Either aFromModule does not import aToModuleName at load-time, or + // aToModuleName is a (currently unsupported) delay-load import. + return false; + } + + // Resolve the import name table (INT). + auto firstINTThunk = headers.template RVAToPtr<PIMAGE_THUNK_DATA>( + impDesc->OriginalFirstThunk); + if (!nt::PEHeaders::IsValid(firstINTThunk)) { + return false; + } + + Maybe<ptrdiff_t> thunkIndex; + + // Scan the INT for the location of the thunk for the function named + // 'aTargetFnName'. + for (PIMAGE_THUNK_DATA curINTThunk = firstINTThunk; + nt::PEHeaders::IsValid(curINTThunk); ++curINTThunk) { + if (IMAGE_SNAP_BY_ORDINAL(curINTThunk->u1.Ordinal)) { + // Currently not supporting import by ordinal; this isn't hard to add, + // but we won't bother unless necessary. + continue; + } + + PIMAGE_IMPORT_BY_NAME curThunkFnName = + headers.template RVAToPtr<PIMAGE_IMPORT_BY_NAME>( + curINTThunk->u1.AddressOfData); + MOZ_ASSERT(curThunkFnName); + if (!curThunkFnName) { + // Looks like we have a bad name descriptor. Try to continue. + continue; + } + + // Function name checks MUST be case-sensitive! + if (!strcmp(aTargetFnName, curThunkFnName->Name)) { + // We found the thunk. Save the index of this thunk, as the IAT thunk + // is located at the same index in that table as in the INT. + thunkIndex = Some(curINTThunk - firstINTThunk); + break; + } + } + + if (thunkIndex.isNothing()) { + // We never found a thunk for that function. Perhaps it's not imported? + return false; + } + + if (thunkIndex.value() < 0) { + // That's just wrong. + return false; + } + + auto firstIATThunk = + headers.template RVAToPtr<PIMAGE_THUNK_DATA>(impDesc->FirstThunk); + if (!nt::PEHeaders::IsValid(firstIATThunk)) { + return false; + } + + // Resolve the IAT thunk for the function we want + PIMAGE_THUNK_DATA targetThunk = &firstIATThunk[thunkIndex.value()]; + if (!nt::PEHeaders::IsValid(targetThunk)) { + return false; + } + + auto fnPtr = reinterpret_cast<Atomic<void*>*>(&targetThunk->u1.Function); + + // Now we can just change out its pointer with our hook function. + AutoVirtualProtect prot(fnPtr, sizeof(void*), PAGE_EXECUTE_READWRITE); + if (!prot) { + return false; + } + + // We do the exchange this way to ensure that *aOutOrigFunc is always valid + // once the atomic exchange has taken place. + void* tmp; + + do { + tmp = *fnPtr; + *aOutOrigFunc = tmp; + } while (!fnPtr->compareExchange(tmp, aHookDest)); + + return true; + } + + template <typename InterceptorT, typename FuncPtrT> + friend class FuncHook; +}; + +template <typename FuncPtrT> +class MOZ_ONLY_USED_TO_AVOID_STATIC_CONSTRUCTORS + FuncHook<WindowsIATPatcher, FuncPtrT> + final { + public: + using ThisType = FuncHook<WindowsIATPatcher, FuncPtrT>; + using ReturnType = typename OriginalFunctionPtrTraits<FuncPtrT>::ReturnType; + + constexpr FuncHook() + : mInitOnce(INIT_ONCE_STATIC_INIT), + mFromModule(nullptr), + mOrigFunc(nullptr) {} + +#if defined(DEBUG) + ~FuncHook() = default; +#endif // defined(DEBUG) + + bool Set(const wchar_t* aFromModuleName, const char* aToModuleName, + const char* aFnName, FuncPtrT aHookDest) { + nsModuleHandle fromModule(::LoadLibraryW(aFromModuleName)); + if (!fromModule) { + return false; + } + + return Set(fromModule, aToModuleName, aFnName, aHookDest); + } + + // We offer this overload in case the client wants finer-grained control over + // loading aFromModule. + bool Set(nsModuleHandle& aFromModule, const char* aToModuleName, + const char* aFnName, FuncPtrT aHookDest) { + LPVOID addHookOk = nullptr; + InitOnceContext ctx(this, aFromModule, aToModuleName, aFnName, aHookDest); + + bool result = ::InitOnceExecuteOnce(&mInitOnce, &InitOnceCallback, &ctx, + &addHookOk) && + addHookOk; + if (!result) { + return result; + } + + // If we successfully set the hook then we must retain a strong reference + // to the module that we modified. + mFromModule = aFromModule.disown(); + return result; + } + + explicit operator bool() const { return !!mOrigFunc; } + + template <typename... ArgsType> + ReturnType operator()(ArgsType&&... aArgs) const { + return mOrigFunc(std::forward<ArgsType>(aArgs)...); + } + + FuncPtrT GetStub() const { return mOrigFunc; } + +#if defined(DEBUG) + // One-time init stuff cannot be moved or copied + FuncHook(const FuncHook&) = delete; + FuncHook(FuncHook&&) = delete; + FuncHook& operator=(const FuncHook&) = delete; + FuncHook& operator=(FuncHook&& aOther) = delete; +#endif // defined(DEBUG) + + private: + struct MOZ_RAII InitOnceContext final { + InitOnceContext(ThisType* aHook, const nsModuleHandle& aFromModule, + const char* aToModuleName, const char* aFnName, + FuncPtrT aHookDest) + : mHook(aHook), + mFromModule(aFromModule), + mToModuleName(aToModuleName), + mFnName(aFnName), + mHookDest(reinterpret_cast<void*>(aHookDest)) {} + + ThisType* mHook; + const nsModuleHandle& mFromModule; + const char* mToModuleName; + const char* mFnName; + void* mHookDest; + }; + + private: + bool Apply(const nsModuleHandle& aFromModule, const char* aToModuleName, + const char* aFnName, void* aHookDest) { + return WindowsIATPatcher::AddHook( + aFromModule, aToModuleName, aFnName, aHookDest, + reinterpret_cast<Atomic<void*>*>(&mOrigFunc)); + } + + static BOOL CALLBACK InitOnceCallback(PINIT_ONCE aInitOnce, PVOID aParam, + PVOID* aOutContext) { + MOZ_ASSERT(aOutContext); + + auto ctx = reinterpret_cast<InitOnceContext*>(aParam); + bool result = ctx->mHook->Apply(ctx->mFromModule, ctx->mToModuleName, + ctx->mFnName, ctx->mHookDest); + + *aOutContext = + result ? reinterpret_cast<PVOID>(1U << INIT_ONCE_CTX_RESERVED_BITS) + : nullptr; + return TRUE; + } + + private: + INIT_ONCE mInitOnce; + HMODULE mFromModule; // never freed + FuncPtrT mOrigFunc; +}; + +/** + * This class applies an irreversible patch to jump to a target function + * without backing up the original function. + */ +class WindowsDllEntryPointInterceptor final { + using DllMainFn = BOOL(WINAPI*)(HINSTANCE, DWORD, LPVOID); + using MMPolicyT = MMPolicyInProcessEarlyStage; + + MMPolicyT mMMPolicy; + + public: + explicit WindowsDllEntryPointInterceptor( + const MMPolicyT::Kernel32Exports& aK32Exports) + : mMMPolicy(aK32Exports) {} + + bool Set(const nt::PEHeaders& aHeaders, DllMainFn aDestination) { + if (!aHeaders) { + return false; + } + + WindowsDllDetourPatcherPrimitive<MMPolicyT> patcher; + return patcher.AddIrreversibleHook( + mMMPolicy, aHeaders.GetEntryPoint(), + reinterpret_cast<uintptr_t>(aDestination)); + } +}; + +} // namespace interceptor + +using WindowsDllInterceptor = interceptor::WindowsDllInterceptor<>; + +using CrossProcessDllInterceptor = interceptor::WindowsDllInterceptor< + mozilla::interceptor::VMSharingPolicyUnique< + mozilla::interceptor::MMPolicyOutOfProcess>>; + +using WindowsIATPatcher = interceptor::WindowsIATPatcher; + +} // namespace mozilla + +#endif /* NS_WINDOWS_DLL_INTERCEPTOR_H_ */ diff --git a/toolkit/xre/dllservices/tests/AssemblyPayloads.h b/toolkit/xre/dllservices/tests/AssemblyPayloads.h new file mode 100644 index 0000000000..4dcbcc5fbf --- /dev/null +++ b/toolkit/xre/dllservices/tests/AssemblyPayloads.h @@ -0,0 +1,238 @@ +/* -*- 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 +// -- it will not detour DetouredCallJumper. We need to do this to point our +// hooking code to the dynamic code, because our hooking API works with an +// exported function name. +__attribute__((naked)) __declspec(dllexport noinline) void DetouredCallJumper( + uintptr_t aCallee) { + // Ideally we would want this to be: + // jmp qword ptr [rip + offset gDetouredCall] + // Unfortunately, it is unclear how to do that with inline assembly, so we + // use a zero offset and patch it before the test. + asm volatile("jmpq *0(%rip)"); +} +# 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/TestDllBlocklistAssumptions.cpp b/toolkit/xre/dllservices/tests/TestDllBlocklistAssumptions.cpp new file mode 100644 index 0000000000..17c68f26dc --- /dev/null +++ b/toolkit/xre/dllservices/tests/TestDllBlocklistAssumptions.cpp @@ -0,0 +1,344 @@ +/* -*- 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 <tlhelp32.h> + +#include <stdio.h> + +#include "mozilla/Assertions.h" +#include "mozilla/NativeNt.h" +#include "mozilla/Vector.h" + +#include "nsWindowsDllInterceptor.h" + +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); + +using namespace mozilla; + +static WindowsDllInterceptor NtdllIntercept; + +static WindowsDllInterceptor::FuncHookType<decltype(&::NtMapViewOfSection)> + stub_NtMapViewOfSection; + +static constexpr auto kDllUsedForLoadLibraryTests = L"dbghelp.dll"; + +class MappedViewsInfoCollector { + public: + struct MappedViewInfo { + PVOID BaseAddress; + HANDLE aSection; + PUBLIC_OBJECT_BASIC_INFORMATION obiSection; + MEMORY_BASIC_INFORMATION mbiVirtualMemory; + }; + + MappedViewsInfoCollector() = default; + ~MappedViewsInfoCollector() = default; + + const MappedViewInfo* begin() const { return mMappedViewsInfo.begin(); } + const MappedViewInfo* end() const { return mMappedViewsInfo.end(); } + + const MappedViewInfo* GetInfo(PVOID aBaseAddress) const { + for (const auto& mappedView : mMappedViewsInfo) { + if (mappedView.BaseAddress == aBaseAddress) { + return &mappedView; + } + } + return nullptr; + } + + void Add(PVOID aBaseAddress, HANDLE aSection) { + auto existingMappedViewInfo = GetInfo(aBaseAddress); + if (existingMappedViewInfo) { + MOZ_RELEASE_ASSERT(existingMappedViewInfo->BaseAddress == aBaseAddress); + MOZ_RELEASE_ASSERT(existingMappedViewInfo->aSection == aSection); + return; + } + + MappedViewInfo mappedViewInfo{aBaseAddress, aSection}; + MOZ_RELEASE_ASSERT(NT_SUCCESS(::NtQueryObject( + aSection, ObjectBasicInformation, &mappedViewInfo.obiSection, + sizeof(mappedViewInfo.obiSection), nullptr))); + MOZ_RELEASE_ASSERT(NT_SUCCESS(::NtQueryVirtualMemory( + ::GetCurrentProcess(), aBaseAddress, MemoryBasicInformation, + &mappedViewInfo.mbiVirtualMemory, + sizeof(mappedViewInfo.mbiVirtualMemory), nullptr))); + MOZ_RELEASE_ASSERT(mMappedViewsInfo.append(std::move(mappedViewInfo))); + } + + void Reset() { MOZ_RELEASE_ASSERT(mMappedViewsInfo.resize(0)); } + + private: + Vector<MappedViewInfo> mMappedViewsInfo; +}; + +static bool sIsTestRunning = false; +static MappedViewsInfoCollector sMappedViewsInfoCollector; + +NTSTATUS NTAPI patched_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) { + NTSTATUS result = stub_NtMapViewOfSection( + aSection, aProcess, aBaseAddress, aZeroBits, aCommitSize, aSectionOffset, + aViewSize, aInheritDisposition, aAllocationType, aProtectionFlags); + if (sIsTestRunning && NT_SUCCESS(result)) { + MOZ_RELEASE_ASSERT(aBaseAddress); + sMappedViewsInfoCollector.Add(*aBaseAddress, aSection); + } + return result; +} + +bool InitializeTests() { + NtdllIntercept.Init(L"ntdll.dll"); + + bool success = stub_NtMapViewOfSection.Set( + NtdllIntercept, "NtMapViewOfSection", patched_NtMapViewOfSection); + if (!success) { + printf( + "TEST-UNEXPECTED-FAIL | TestDllBlocklistAssumptions | " + "Failed to hook NtMapViewOfSection.\n"); + fflush(stdout); + return false; + } + + printf( + "TEST-PASS | TestDllBlocklistAssumptions | " + "Successfully hooked NtMapViewOfSection.\n"); + fflush(stdout); + return true; +} + +bool CheckMappedViewAssumptions(const char* aTestDescription, PVOID baseAddress, + bool aExpectExecutableSection, + bool aExpectImageTypeMemory) { + auto mappedViewInfo = sMappedViewsInfoCollector.GetInfo(baseAddress); + if (!mappedViewInfo) { + printf( + "TEST-UNEXPECTED-FAIL | TestDllBlocklistAssumptions | " + "Failed to find mapped view information while testing %s.\n", + aTestDescription); + fflush(stdout); + return false; + } + + MOZ_RELEASE_ASSERT(mappedViewInfo->BaseAddress == baseAddress); + + if (aExpectExecutableSection != + bool(mappedViewInfo->obiSection.GrantedAccess & SECTION_MAP_EXECUTE)) { + printf( + "TEST-UNEXPECTED-FAIL | TestDllBlocklistAssumptions | " + "Mismatch in assumptions regarding section executablity while testing " + "%s.\n", + aTestDescription); + fflush(stdout); + return false; + } + + if (aExpectImageTypeMemory != + bool(mappedViewInfo->mbiVirtualMemory.Type & MEM_IMAGE)) { + printf( + "TEST-UNEXPECTED-FAIL | TestDllBlocklistAssumptions | " + "Mismatch in assumptions regarding virtual memory type while testing " + "%s.\n", + aTestDescription); + fflush(stdout); + return false; + } + return true; +} + +// Assumptions used to block normal DLL loads: +// - section handle was granted SECTION_MAP_EXECUTE access; +// - virtual memory is tagged as MEM_IMAGE type. +bool TestDllLoad() { + sMappedViewsInfoCollector.Reset(); + + auto module = ::LoadLibraryW(kDllUsedForLoadLibraryTests); + if (!module) { + printf( + "TEST-UNEXPECTED-FAIL | TestDllBlocklistAssumptions | " + "Call to LoadLibraryW failed with error %lu.\n", + ::GetLastError()); + fflush(stdout); + return false; + } + + auto baseAddress = nt::PEHeaders::HModuleToBaseAddr<PVOID>(module); + bool result = + CheckMappedViewAssumptions("LoadLibraryW", baseAddress, true, true); + FreeLibrary(module); + + if (result) { + printf( + "TEST-PASS | TestDllBlocklistAssumptions | " + "DLL loading works as expected.\n"); + fflush(stdout); + } + return result; +} + +// Assumptions used to avoid blocking DLL loads when loading as data file: +// - section handle was *not* granted SECTION_MAP_EXECUTE access; +// - virtual memory is *not* tagged as MEM_IMAGE type. +bool TestDllLoadAsDataFile() { + sMappedViewsInfoCollector.Reset(); + + auto module = ::LoadLibraryExW(kDllUsedForLoadLibraryTests, nullptr, + LOAD_LIBRARY_AS_DATAFILE); + if (!module) { + printf( + "TEST-UNEXPECTED-FAIL | TestDllBlocklistAssumptions | " + "Call to LoadLibraryExW failed with error %lu.\n", + ::GetLastError()); + fflush(stdout); + return false; + } + + auto baseAddress = nt::PEHeaders::HModuleToBaseAddr<PVOID>(module); + bool result = CheckMappedViewAssumptions( + "LoadLibraryExW(LOAD_LIBRARY_AS_DATAFILE)", baseAddress, false, false); + FreeLibrary(module); + + if (result) { + printf( + "TEST-PASS | TestDllBlocklistAssumptions | " + "DLL loading as data file works as expected.\n"); + fflush(stdout); + } + return result; +} + +// Assumptions used to avoid blocking DLL loads when loading as image resource: +// - section handle was *not* granted SECTION_MAP_EXECUTE access; +// - virtual memory is tagged as MEM_IMAGE type, however. +bool TestDllLoadAsImageResource() { + sMappedViewsInfoCollector.Reset(); + + auto module = ::LoadLibraryExW(kDllUsedForLoadLibraryTests, nullptr, + LOAD_LIBRARY_AS_IMAGE_RESOURCE); + if (!module) { + printf( + "TEST-UNEXPECTED-FAIL | TestDllBlocklistAssumptions | " + "Call to LoadLibraryExW failed with error %lu.\n", + ::GetLastError()); + fflush(stdout); + return false; + } + + auto baseAddress = nt::PEHeaders::HModuleToBaseAddr<PVOID>(module); + bool result = CheckMappedViewAssumptions( + "LoadLibraryExW(LOAD_LIBRARY_AS_IMAGE_RESOURCE)", baseAddress, false, + true); + FreeLibrary(module); + + if (result) { + printf( + "TEST-PASS | TestDllBlocklistAssumptions | " + "DLL loading as image resource works as expected.\n"); + fflush(stdout); + } + return result; +} + +// Assumptions used to avoid crashing when using Thread32Next (bug 1733532): +// - section handle was *not* granted SECTION_MAP_EXECUTE access; +// - virtual memory is *not* tagged as MEM_IMAGE type. +bool TestThreadIteration() { + sMappedViewsInfoCollector.Reset(); + + HANDLE snapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + if (snapshot == INVALID_HANDLE_VALUE) { + printf( + "TEST-UNEXPECTED-FAIL | TestDllBlocklistAssumptions | " + "Call toCreateToolhelp32Snapshot failed with error %lu.\n", + ::GetLastError()); + fflush(stdout); + return false; + } + + THREADENTRY32 entry{}; + entry.dwSize = sizeof(entry); + if (!::Thread32First(snapshot, &entry)) { + printf( + "TEST-UNEXPECTED-FAIL | TestDllBlocklistAssumptions | " + "Call to Thread32First failed with error %lu.\n", + ::GetLastError()); + fflush(stdout); + return false; + } + + while (::Thread32Next(snapshot, &entry)) { + } + auto error = GetLastError(); + if (error != ERROR_NO_MORE_FILES) { + printf( + "TEST-UNEXPECTED-FAIL | TestDllBlocklistAssumptions | " + "Call to Thread32Next failed with unexpected error %lu.\n", + error); + fflush(stdout); + return false; + } + + uint32_t count = 0; + for (const auto& mappedViewInfo : sMappedViewsInfoCollector) { + if (!CheckMappedViewAssumptions("Thread32Next", mappedViewInfo.BaseAddress, + false, false)) { + return false; + } + ++count; + } + + if (!count) { + printf( + "TEST-UNEXPECTED-FAIL | TestDllBlocklistAssumptions | " + "Unexpectedly found no mappings after iterating threads with " + "Thread32Next.\n"); + fflush(stdout); + return false; + } + + printf( + "TEST-PASS | TestDllBlocklistAssumptions | " + "Iterating threads with Thread32Next works as expected.\n"); + fflush(stdout); + return true; +} + +extern "C" int wmain(int argc, wchar_t* argv[]) { + LARGE_INTEGER start; + QueryPerformanceCounter(&start); + + sIsTestRunning = true; + if (InitializeTests() && TestDllLoad() && TestDllLoadAsDataFile() && + TestDllLoadAsImageResource() && TestThreadIteration()) { + sIsTestRunning = false; + + printf("TEST-PASS | TestDllBlocklistAssumptions | 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; + } + + sIsTestRunning = false; + return 1; +} diff --git a/toolkit/xre/dllservices/tests/TestDllInterceptor.cpp b/toolkit/xre/dllservices/tests/TestDllInterceptor.cpp new file mode 100644 index 0000000000..2b70c9bcd4 --- /dev/null +++ b/toolkit/xre/dllservices/tests/TestDllInterceptor.cpp @@ -0,0 +1,1475 @@ +/* -*- 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 <bcrypt.h> +#pragma comment(lib, "bcrypt.lib") + +#include "AssemblyPayloads.h" +#include "mozilla/DynamicallyLinkedFunctionPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WindowsVersion.h" +#include "nsWindowsDllInterceptor.h" +#include "nsWindowsHelpers.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) + +#define RtlGenRandom SystemFunction036 +extern "C" BOOLEAN NTAPI RtlGenRandom(PVOID aRandomBuffer, + ULONG aRandomBufferLength); + +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 = std::tuple<Args...>, size_t... Indices> +decltype(auto) Apply(OrigFuncT& aFunc, ArgTuple&& aArgs, + std::index_sequence<Indices...>) { + return std::apply(aFunc, 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 = std::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 = std::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[]; + +#ifdef _M_X64 + +// To check that unwind information propagates from hooked functions to their +// stubs, we need to find the real address where the detoured code lives. +class RedirectionResolver : public interceptor::WindowsDllPatcherBase< + interceptor::VMSharingPolicyShared> { + public: + uintptr_t ResolveRedirectedAddressForTest(FARPROC aFunc) { + bool isWin8 = IsWin8OrLater() && (!IsWin8Point1OrLater()); + + bool isDuplicateHandle = (reinterpret_cast<void*>(aFunc) == + reinterpret_cast<void*>(&::DuplicateHandle)); + + // We need to reproduce the behavior of WindowsDllInterceptor::AddDetour + // with respect to redirection, including the corner case for bug 1659398. + if (isWin8 && isDuplicateHandle) { + return reinterpret_cast<uintptr_t>(aFunc); + } + + return ResolveRedirectedAddress(aFunc).GetAddress(); + } +}; + +#endif // _M_X64 + +// 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)); + + HMODULE module = ::LoadLibraryW(dllW); + FARPROC funcAddr = ::GetProcAddress(module, func); + if (!funcAddr) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Failed to find %s from " + "%s\n", + func, dll); + fflush(stdout); + return false; + } + +#ifdef _M_X64 + + // Resolve what is the actual address of the code that will be detoured, as + // that's the code we want to compare with when we check for unwind + // information. Do that *before* detouring, although the address will only be + // used after detouring. + RedirectionResolver resolver; + auto detouredCodeAddr = resolver.ResolveRedirectedAddressForTest(funcAddr); + +#endif // _M_X64 + + 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); + +#ifdef _M_X64 + + // Check that unwind information has been added if and only if it was + // present for the original detoured code. + uintptr_t funcImageBase = 0; + auto funcEntry = + RtlLookupFunctionEntry(detouredCodeAddr, &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; + } + + // Test the DLL function we just hooked. + 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_HELPER(dll, func, pred, comp) \ + TestHook<decltype(&func)>(dll, #func, \ + &Predicates<decltype(&func)>::pred<comp>) + +#define TEST_HOOK(dll, func, pred, comp) TEST_HOOK_HELPER(dll, 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) { + // We first adjust the detoured call jumper from: + // ff 25 00 00 00 00 jmp qword ptr [rip + 0] + // to: + // ff 25 XX XX XX XX jmp qword ptr [rip + offset gDetouredCall] + uint8_t bytes[6]{0xff, 0x25, 0, 0, 0, 0}; + if (0 != memcmp(bytes, reinterpret_cast<void*>(DetouredCallJumper), + sizeof bytes)) { + return nullptr; + } + + DWORD oldProtect{}; + if (!VirtualProtect(reinterpret_cast<void*>(DetouredCallJumper), sizeof bytes, + PAGE_READWRITE, &oldProtect)) { + return nullptr; + } + + *reinterpret_cast<uint32_t*>(&bytes[2]) = static_cast<uint32_t>( + reinterpret_cast<uintptr_t>(&gDetouredCall) - + (reinterpret_cast<uintptr_t>(DetouredCallJumper) + sizeof bytes)); + memcpy(reinterpret_cast<void*>(DetouredCallJumper), bytes, sizeof bytes); + + if (!VirtualProtect(reinterpret_cast<void*>(DetouredCallJumper), sizeof bytes, + oldProtect, &oldProtect)) { + return nullptr; + } + + 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)); + + 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; + } + + 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) && + TEST_HOOK("bcrypt.dll", BCryptGenRandom, Equals, + static_cast<NTSTATUS>(STATUS_INVALID_HANDLE)) && + TEST_HOOK("advapi32.dll", RtlGenRandom, Equals, TRUE) && +#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..938f63a524 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDLLBlocklist.cpp @@ -0,0 +1,475 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> +#include <winternl.h> + +#include <process.h> + +#include <array> +#include <functional> + +#include "gtest/gtest.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Char16.h" +#include "mozilla/gtest/MozAssertions.h" +#include "mozilla/Services.h" +#include "mozilla/WinDllServices.h" +#include "mozilla/WindowsStackCookie.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIThread.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsUnicharUtils.h" +#include "nsWindowsHelpers.h" + +static nsString GetFullPath(const nsAString& aLeaf) { + nsCOMPtr<nsIFile> f; + + EXPECT_TRUE(NS_SUCCEEDED( + NS_GetSpecialDirectory(NS_OS_CURRENT_WORKING_DIR, getter_AddRefs(f)))); + + EXPECT_NS_SUCCEEDED(f->Append(aLeaf)); + + bool exists; + EXPECT_TRUE(NS_SUCCEEDED(f->Exists(&exists)) && exists); + + nsString ret; + EXPECT_NS_SUCCEEDED(f->GetPath(ret)); + return ret; +} + +void FlushMainThreadLoop() { + nsCOMPtr<nsIThread> mainThread; + nsresult rv = NS_GetMainThread(getter_AddRefs(mainThread)); + ASSERT_NS_SUCCEEDED(rv); + + rv = NS_OK; + bool processed = true; + while (processed && NS_SUCCEEDED(rv)) { + rv = mainThread->ProcessNextEvent(false, &processed); + } +} + +class TestDLLLoadObserver : public nsIObserver { + public: + using DLLFilter = std::function<bool(const char16_t*)>; + + explicit TestDLLLoadObserver(DLLFilter dllFilter) + : mDllFilter(std::move(dllFilter)), + mMainThreadNotificationsCount(0), + mNonMainThreadNotificationsCount(0){}; + + NS_DECL_THREADSAFE_ISUPPORTS + + NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) override { + if (!mDllFilter(aData)) { + return NS_OK; + } + if (0 == strcmp(aTopic, mozilla::DllServices::kTopicDllLoadedMainThread)) { + ++mMainThreadNotificationsCount; + } else { + EXPECT_TRUE( + 0 == + strcmp(aTopic, mozilla::DllServices::kTopicDllLoadedNonMainThread)); + ++mNonMainThreadNotificationsCount; + } + return NS_OK; + } + + void Init() { + nsCOMPtr<nsIObserverService> obsServ( + mozilla::services::GetObserverService()); + + EXPECT_TRUE(obsServ); + + obsServ->AddObserver(this, mozilla::DllServices::kTopicDllLoadedMainThread, + false); + obsServ->AddObserver( + this, mozilla::DllServices::kTopicDllLoadedNonMainThread, false); + } + + void Exit() { + // Observe only gets called if/when we flush the main thread loop. + FlushMainThreadLoop(); + + nsCOMPtr<nsIObserverService> obsServ( + mozilla::services::GetObserverService()); + + EXPECT_TRUE(obsServ); + + obsServ->RemoveObserver(this, + mozilla::DllServices::kTopicDllLoadedMainThread); + obsServ->RemoveObserver(this, + mozilla::DllServices::kTopicDllLoadedNonMainThread); + } + + int MainThreadNotificationsCount() { return mMainThreadNotificationsCount; } + + int NonMainThreadNotificationsCount() { + return mNonMainThreadNotificationsCount; + } + + private: + virtual ~TestDLLLoadObserver() = default; + + DLLFilter mDllFilter; + int mMainThreadNotificationsCount; + int mNonMainThreadNotificationsCount; +}; + +NS_IMPL_ISUPPORTS(TestDLLLoadObserver, nsIObserver) + +TEST(TestDllBlocklist, BlockDllByName) +{ + // The DLL name has capital letters, so this also tests that the comparison + // is case-insensitive. + constexpr auto kLeafName = u"TestDllBlocklist_MatchByName.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + + EXPECT_TRUE(!hDll); + EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get())); + + hDll.own(::LoadLibraryExW(dllPath.get(), nullptr, LOAD_LIBRARY_AS_DATAFILE)); + // Mapped as MEM_MAPPED + PAGE_READONLY + EXPECT_TRUE(hDll); +} + +TEST(TestDllBlocklist, BlockDllByVersion) +{ + constexpr auto kLeafName = u"TestDllBlocklist_MatchByVersion.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + + EXPECT_TRUE(!hDll); + EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get())); + + hDll.own( + ::LoadLibraryExW(dllPath.get(), nullptr, LOAD_LIBRARY_AS_IMAGE_RESOURCE)); + // Mapped as MEM_IMAGE + PAGE_READONLY + EXPECT_TRUE(hDll); +} + +TEST(TestDllBlocklist, AllowDllByVersion) +{ + constexpr auto kLeafName = u"TestDllBlocklist_AllowByVersion.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + + EXPECT_TRUE(!!hDll); + EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get())); +} + +TEST(TestDllBlocklist, GPUProcessOnly_AllowInMainProcess) +{ + constexpr auto kLeafName = u"TestDllBlocklist_GPUProcessOnly.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + + EXPECT_TRUE(!!hDll); + EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get())); +} + +TEST(TestDllBlocklist, SocketProcessOnly_AllowInMainProcess) +{ + constexpr auto kLeafName = u"TestDllBlocklist_SocketProcessOnly.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + + EXPECT_TRUE(!!hDll); + EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get())); +} + +TEST(TestDllBlocklist, UtilityProcessOnly_AllowInMainProcess) +{ + constexpr auto kLeafName = u"TestDllBlocklist_UtilityProcessOnly.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + + EXPECT_TRUE(!!hDll); + EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get())); +} + +TEST(TestDllBlocklist, GMPluginProcessOnly_AllowInMainProcess) +{ + constexpr auto kLeafName = u"TestDllBlocklist_GMPluginProcessOnly.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + + EXPECT_TRUE(!!hDll); + EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get())); +} + +// This DLL has two entries; it's blocked for unversioned (i.e. DLLs that +// have no version information) everywhere and blocked for versions 5.5.5.5 and +// earlier only in the GPU process. Since the version we're trying to load +// is 5.5.5.5, it should load in the main process. +TEST(TestDllBlocklist, MultipleEntriesDifferentProcesses_AllowInMainProcess) +{ + constexpr auto kLeafName = + u"TestDllBlocklist_MultipleEntries_DifferentProcesses.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + + EXPECT_TRUE(!!hDll); + EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get())); +} + +TEST(TestDllBlocklist, MultipleEntriesSameProcessBackward_Block) +{ + // One entry matches by version and many others do not, so + // we should block. + constexpr auto kLeafName = + u"TestDllBlocklist_MultipleEntries_SameProcessBackward.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + + EXPECT_TRUE(!hDll); + EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get())); + + hDll.own( + ::LoadLibraryExW(dllPath.get(), nullptr, LOAD_LIBRARY_AS_IMAGE_RESOURCE)); + // Mapped as MEM_IMAGE + PAGE_READONLY + EXPECT_TRUE(hDll); +} + +TEST(TestDllBlocklist, MultipleEntriesSameProcessForward_Block) +{ + // One entry matches by version and many others do not, so + // we should block. + constexpr auto kLeafName = + u"TestDllBlocklist_MultipleEntries_SameProcessForward.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + + EXPECT_TRUE(!hDll); + EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get())); + + hDll.own( + ::LoadLibraryExW(dllPath.get(), nullptr, LOAD_LIBRARY_AS_IMAGE_RESOURCE)); + // Mapped as MEM_IMAGE + PAGE_READONLY + EXPECT_TRUE(hDll); +} + +#if defined(MOZ_LAUNCHER_PROCESS) +// RedirectToNoOpEntryPoint needs the launcher process. +// This test will fail in debug x64 if we mistakenly reintroduce stack buffers +// in patched_NtMapViewOfSection (see bug 1733532). +TEST(TestDllBlocklist, NoOpEntryPoint) +{ + // DllMain of this dll has MOZ_RELEASE_ASSERT. This test makes sure we load + // the module successfully without running DllMain. + constexpr auto kLeafName = u"TestDllBlocklist_NoOpEntryPoint.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + +# if defined(MOZ_ASAN) + // With ASAN, the test uses mozglue's blocklist where + // REDIRECT_TO_NOOP_ENTRYPOINT is ignored. So LoadLibraryW + // is expected to fail. + EXPECT_TRUE(!hDll); + EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get())); +# else + EXPECT_TRUE(!!hDll); + EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get())); +# endif +} + +// User blocklist needs the launcher process. +// This test will fail in debug x64 if we mistakenly reintroduce stack buffers +// in patched_NtMapViewOfSection (see bug 1733532). +TEST(TestDllBlocklist, UserBlocked) +{ + constexpr auto kLeafName = u"TestDllBlocklist_UserBlocked.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + +// With ASAN, the test uses mozglue's blocklist where +// the user blocklist is not used. +# if !defined(MOZ_ASAN) + EXPECT_TRUE(!hDll); + EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get())); +# endif + hDll.own(::LoadLibraryExW(dllPath.get(), nullptr, LOAD_LIBRARY_AS_DATAFILE)); + // Mapped as MEM_MAPPED + PAGE_READONLY + EXPECT_TRUE(hDll); +} +#endif // defined(MOZ_LAUNCHER_PROCESS) + +#define DLL_BLOCKLIST_ENTRY(name, ...) {name, __VA_ARGS__}, +#define DLL_BLOCKLIST_STRING_TYPE const char* +#include "mozilla/WindowsDllBlocklistLegacyDefs.h" + +TEST(TestDllBlocklist, BlocklistIntegrity) +{ + DECLARE_POINTER_TO_FIRST_DLL_BLOCKLIST_ENTRY(pFirst); + DECLARE_POINTER_TO_LAST_DLL_BLOCKLIST_ENTRY(pLast); + + EXPECT_FALSE(pLast->mName || pLast->mMaxVersion || pLast->mFlags); + + for (size_t i = 0; i < mozilla::ArrayLength(gWindowsDllBlocklist) - 1; ++i) { + auto pEntry = pFirst + i; + + // Validate name + EXPECT_TRUE(!!pEntry->mName); + EXPECT_GT(strlen(pEntry->mName), 3U); + + // Check the filename for valid characters. + for (auto pch = pEntry->mName; *pch != 0; ++pch) { + EXPECT_FALSE(*pch >= 'A' && *pch <= 'Z'); + } + } +} + +TEST(TestDllBlocklist, BlockThreadWithLoadLibraryEntryPoint) +{ + // Only supported on Nightly +#if defined(NIGHTLY_BUILD) + using ThreadProc = unsigned(__stdcall*)(void*); + + constexpr auto kLeafNameW = u"TestDllBlocklist_MatchByVersion.dll"_ns; + + nsString fullPathW = GetFullPath(kLeafNameW); + EXPECT_FALSE(fullPathW.IsEmpty()); + + nsAutoHandle threadW(reinterpret_cast<HANDLE>( + _beginthreadex(nullptr, 0, reinterpret_cast<ThreadProc>(&::LoadLibraryW), + (void*)fullPathW.get(), 0, nullptr))); + + EXPECT_TRUE(!!threadW); + EXPECT_EQ(::WaitForSingleObject(threadW, INFINITE), WAIT_OBJECT_0); + +# if !defined(MOZ_ASAN) + // ASAN builds under Windows 11 can have unexpected thread exit codes. + // See bug 1798796 + DWORD exitCode; + EXPECT_TRUE(::GetExitCodeThread(threadW, &exitCode) && !exitCode); +# endif // !defined(MOZ_ASAN) + EXPECT_TRUE(!::GetModuleHandleW(kLeafNameW.get())); + + const NS_LossyConvertUTF16toASCII fullPathA(fullPathW); + EXPECT_FALSE(fullPathA.IsEmpty()); + + nsAutoHandle threadA(reinterpret_cast<HANDLE>( + _beginthreadex(nullptr, 0, reinterpret_cast<ThreadProc>(&::LoadLibraryA), + (void*)fullPathA.get(), 0, nullptr))); + + EXPECT_TRUE(!!threadA); + EXPECT_EQ(::WaitForSingleObject(threadA, INFINITE), WAIT_OBJECT_0); +# if !defined(MOZ_ASAN) + // ASAN builds under Windows 11 can have unexpected thread exit codes. + // See bug 1798796 + EXPECT_TRUE(::GetExitCodeThread(threadA, &exitCode) && !exitCode); +# endif // !defined(MOZ_ASAN) + EXPECT_TRUE(!::GetModuleHandleW(kLeafNameW.get())); +#endif // defined(NIGHTLY_BUILD) +} + +constexpr auto kSingleNotificationDll1Loads = 4; +constexpr auto kSingleNotificationDll2Loads = 3; +using DllFullPathsArray = + std::array<nsString, + kSingleNotificationDll1Loads + kSingleNotificationDll2Loads>; + +DWORD __stdcall LoadSingleNotificationModules(LPVOID aThreadParameter) { + auto* dllFullPaths = reinterpret_cast<DllFullPathsArray*>(aThreadParameter); + + for (const auto& dllFullPath : *dllFullPaths) { + nsModuleHandle hDll(::LoadLibraryW(dllFullPath.get())); + EXPECT_TRUE(!hDll); + EXPECT_TRUE(!::GetModuleHandleW(dllFullPath.get())); + } + + return 0; +} + +// The next test is only relevant if we hook LdrLoadDll, so we reflect the +// hooking condition from browser/app/winlauncher/DllBlocklistInit.cpp. +#if !defined(MOZ_ASAN) && !defined(_M_ARM64) + +// This test relies on the fact that blocked DLL loads generate a DLL load +// notification. +TEST(TestDllBlocklist, SingleNotification) +{ + // We will block-load the two DLLs multiple times, with variations on case. + std::array<nsLiteralString, kSingleNotificationDll1Loads> dll1Variations{ + u"TestDllBlocklist_SingleNotification1.dll"_ns, + u"TestDllBlocklist_SingleNotification1.dll"_ns, + u"testdllblocklist_singlenotification1.dll"_ns, + u"TeStDlLbLoCkLiSt_SiNgLeNoTiFiCaTiOn1.DlL"_ns, + }; + std::array<nsLiteralString, kSingleNotificationDll2Loads> dll2Variations{ + u"TestDllBlocklist_SingleNotification2.dll"_ns, + u"testdllblocklist_singlenotification2.dll"_ns, + u"TESTDLLBLOCKLIST_SINGLENOTIFICATION2.dll"_ns, + }; + DllFullPathsArray dllFullPaths; + size_t i = 0; + for (const auto& dllName : dll1Variations) { + dllFullPaths[i] = GetFullPath(dllName); + ++i; + } + for (const auto& dllName : dll2Variations) { + dllFullPaths[i] = GetFullPath(dllName); + ++i; + } + + // Register our observer. + TestDLLLoadObserver::DLLFilter dllFilter( + [](const char16_t* aLoadedPath) -> bool { + nsDependentString loadedPath(aLoadedPath); + return StringEndsWith(loadedPath, + u"\\testdllblocklist_singlenotification1.dll"_ns, + nsCaseInsensitiveStringComparator) || + StringEndsWith(loadedPath, + u"\\testdllblocklist_singlenotification2.dll"_ns, + nsCaseInsensitiveStringComparator); + }); + RefPtr<TestDLLLoadObserver> obs(new TestDLLLoadObserver(dllFilter)); + obs->Init(); + + // Load DllServices. This is required for notifications to get dispatched. + RefPtr<mozilla::DllServices> dllSvc(mozilla::DllServices::Get()); + EXPECT_TRUE(dllSvc); + + // Block-load the two DLLs multiple times on the main thread. + LoadSingleNotificationModules(reinterpret_cast<void*>(&dllFullPaths)); + + // Block-load the two DLLs multiple times on a different thread. + HANDLE thread = + ::CreateThread(nullptr, 0, LoadSingleNotificationModules, + reinterpret_cast<void*>(&dllFullPaths), 0, nullptr); + EXPECT_EQ(::WaitForSingleObject(thread, 5000), WAIT_OBJECT_0); + + // Unregister our observer and flush the main thread loop. + obs->Exit(); + + // Check how many notifications we received + EXPECT_EQ(obs->MainThreadNotificationsCount(), 2); + EXPECT_EQ(obs->NonMainThreadNotificationsCount(), + kSingleNotificationDll1Loads + kSingleNotificationDll2Loads); +} + +#endif // !defined(MOZ_ASAN) && !defined(_M_ARM64) 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_GMPluginProcessOnly/TestDllBlocklist_GMPluginProcessOnly.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_GMPluginProcessOnly/TestDllBlocklist_GMPluginProcessOnly.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_GMPluginProcessOnly/TestDllBlocklist_GMPluginProcessOnly.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_GMPluginProcessOnly/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_GMPluginProcessOnly/moz.build new file mode 100644 index 0000000000..a5c89c2b8e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_GMPluginProcessOnly/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_GMPluginProcessOnly") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_GMPluginProcessOnly.cpp", +] + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_GMPluginProcessOnly.dll"] diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_GPUProcessOnly/TestDllBlocklist_GPUProcessOnly.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_GPUProcessOnly/TestDllBlocklist_GPUProcessOnly.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_GPUProcessOnly/TestDllBlocklist_GPUProcessOnly.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_GPUProcessOnly/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_GPUProcessOnly/moz.build new file mode 100644 index 0000000000..748e9cf22c --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_GPUProcessOnly/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_GPUProcessOnly") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_GPUProcessOnly.cpp", +] + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_GPUProcessOnly.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_MultipleEntries_DifferentProcesses/TestDllBlocklist_MultipleEntries_DifferentProcesses.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_DifferentProcesses/TestDllBlocklist_MultipleEntries_DifferentProcesses.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_DifferentProcesses/TestDllBlocklist_MultipleEntries_DifferentProcesses.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_MultipleEntries_DifferentProcesses/TestDllBlocklist_MultipleEntries_DifferentProcesses.rc b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_DifferentProcesses/TestDllBlocklist_MultipleEntries_DifferentProcesses.rc new file mode 100644 index 0000000000..2b5b81406e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_DifferentProcesses/TestDllBlocklist_MultipleEntries_DifferentProcesses.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_MultipleEntries_DifferentProcesses.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_MultipleEntries_DifferentProcesses/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_DifferentProcesses/moz.build new file mode 100644 index 0000000000..cf2f745a28 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_DifferentProcesses/moz.build @@ -0,0 +1,19 @@ +# -*- 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_MultipleEntries_DifferentProcesses") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_MultipleEntries_DifferentProcesses.cpp", +] + +RCFILE = "TestDllBlocklist_MultipleEntries_DifferentProcesses.rc" + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += [ + "!TestDllBlocklist_MultipleEntries_DifferentProcesses.dll" + ] diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_SameProcessBackward/TestDllBlocklist_MultipleEntries_SameProcessBackward.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_SameProcessBackward/TestDllBlocklist_MultipleEntries_SameProcessBackward.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_SameProcessBackward/TestDllBlocklist_MultipleEntries_SameProcessBackward.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_MultipleEntries_SameProcessBackward/TestDllBlocklist_MultipleEntries_SameProcessBackward.rc b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_SameProcessBackward/TestDllBlocklist_MultipleEntries_SameProcessBackward.rc new file mode 100644 index 0000000000..0b2431c343 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_SameProcessBackward/TestDllBlocklist_MultipleEntries_SameProcessBackward.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_MultipleEntries_SameProcessBackward.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_MultipleEntries_SameProcessBackward/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_SameProcessBackward/moz.build new file mode 100644 index 0000000000..21ab229245 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_SameProcessBackward/moz.build @@ -0,0 +1,19 @@ +# -*- 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_MultipleEntries_SameProcessBackward") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_MultipleEntries_SameProcessBackward.cpp", +] + +RCFILE = "TestDllBlocklist_MultipleEntries_SameProcessBackward.rc" + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += [ + "!TestDllBlocklist_MultipleEntries_SameProcessBackward.dll" + ] diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_SameProcessForward/TestDllBlocklist_MultipleEntries_SameProcessForward.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_SameProcessForward/TestDllBlocklist_MultipleEntries_SameProcessForward.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_SameProcessForward/TestDllBlocklist_MultipleEntries_SameProcessForward.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_MultipleEntries_SameProcessForward/TestDllBlocklist_MultipleEntries_SameProcessForward.rc b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_SameProcessForward/TestDllBlocklist_MultipleEntries_SameProcessForward.rc new file mode 100644 index 0000000000..20d14e24d7 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_SameProcessForward/TestDllBlocklist_MultipleEntries_SameProcessForward.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_MultipleEntries_SameProcessForward.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_MultipleEntries_SameProcessForward/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_SameProcessForward/moz.build new file mode 100644 index 0000000000..1546489efb --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MultipleEntries_SameProcessForward/moz.build @@ -0,0 +1,19 @@ +# -*- 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_MultipleEntries_SameProcessForward") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_MultipleEntries_SameProcessForward.cpp", +] + +RCFILE = "TestDllBlocklist_MultipleEntries_SameProcessForward.rc" + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += [ + "!TestDllBlocklist_MultipleEntries_SameProcessForward.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_SingleNotification1/TestDllBlocklist_SingleNotification1.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SingleNotification1/TestDllBlocklist_SingleNotification1.cpp new file mode 100644 index 0000000000..4f6ce877eb --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SingleNotification1/TestDllBlocklist_SingleNotification1.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/TestDllBlocklist_SingleNotification1/TestDllBlocklist_SingleNotification1.rc b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SingleNotification1/TestDllBlocklist_SingleNotification1.rc new file mode 100644 index 0000000000..90f098b1b9 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SingleNotification1/TestDllBlocklist_SingleNotification1.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_SingleNotification1.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_SingleNotification1/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SingleNotification1/moz.build new file mode 100644 index 0000000000..79139b2b6e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SingleNotification1/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_SingleNotification1") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_SingleNotification1.cpp", +] + +RCFILE = "TestDllBlocklist_SingleNotification1.rc" + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_SingleNotification1.dll"] diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SingleNotification2/TestDllBlocklist_SingleNotification2.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SingleNotification2/TestDllBlocklist_SingleNotification2.cpp new file mode 100644 index 0000000000..4f6ce877eb --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SingleNotification2/TestDllBlocklist_SingleNotification2.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/TestDllBlocklist_SingleNotification2/TestDllBlocklist_SingleNotification2.rc b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SingleNotification2/TestDllBlocklist_SingleNotification2.rc new file mode 100644 index 0000000000..0cd44b2f9b --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SingleNotification2/TestDllBlocklist_SingleNotification2.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_SingleNotification2.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_SingleNotification2/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SingleNotification2/moz.build new file mode 100644 index 0000000000..64d551f480 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SingleNotification2/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_SingleNotification2") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_SingleNotification2.cpp", +] + +RCFILE = "TestDllBlocklist_SingleNotification2.rc" + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_SingleNotification2.dll"] 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..ba4d1a8df8 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules.cpp @@ -0,0 +1,462 @@ +/* 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 kMaximumAttempts = 500; + Vector<UntrustedModulesData> mData; + ModuleLoadCounter* mChecker = nullptr; + Maybe<nsresult> mRv; + int mAttempts = 0; + + void PollUntrustedModulesData() { + RefPtr<DllServices> dllSvc(DllServices::Get()); + dllSvc->GetUntrustedModulesData()->Then( + GetMainThreadSerialEventTarget(), __func__, + [this](Maybe<UntrustedModulesData>&& aResult) { + // Some of expected loaded modules are still missing after + // kMaximumAttempts queries were submitted. + // Giving up here to avoid an infinite loop. + if (++mAttempts > kMaximumAttempts) { + mRv = Some(NS_ERROR_ABORT); + return; + } + + if (aResult.isSome()) { + wprintf(L"Received data. (attempts=%d)\n", mAttempts); + for (auto item : aResult.ref().mEvents) { + mChecker->Decrement(item->mEvent.mRequestedDllName); + } + EXPECT_TRUE(mData.emplaceBack(std::move(aResult.ref()))); + } + + if (mChecker->IsDone()) { + mRv = Some(NS_OK); + return; + } + + PollUntrustedModulesData(); + }, + [this](nsresult aReason) { + wprintf(L"GetUntrustedModulesData() failed - %08x\n", aReason); + EXPECT_TRUE(false); + mRv = Some(aReason); + }); + } + + public: + Vector<UntrustedModulesData>& Data() { return mData; } + + nsresult Collect(ModuleLoadCounter& aChecker) { + mRv = Nothing(); + mChecker = &aChecker; + mAttempts = 0; + mData.clear(); + + PollUntrustedModulesData(); + + EXPECT_TRUE(SpinEventLoopUntil("xre:UntrustedModulesCollector"_ns, + [this]() { return mRv.isSome(); })); + + mChecker = nullptr; + return *mRv; + } +}; + +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, dom::UndefinedIsNullStringLiteral)); + + 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\"," + // It would be nice to hard-code this, but this might change with + // compiler versions, etc. + u"\"debugID\":\"[0-9A-F]{33}\"," + 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..92f341841a --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/moz.build @@ -0,0 +1,39 @@ +# 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", + "TestUntrustedModules.cpp", +] + +LOCAL_INCLUDES += [ + "/toolkit/components/telemetry/other", + "/toolkit/components/telemetry/tests/gtest", +] + +TEST_DIRS += [ + "rust", + "TestDllBlocklist_AllowByVersion", + "TestDllBlocklist_GMPluginProcessOnly", + "TestDllBlocklist_GPUProcessOnly", + "TestDllBlocklist_MatchByName", + "TestDllBlocklist_MatchByVersion", + "TestDllBlocklist_MultipleEntries_DifferentProcesses", + "TestDllBlocklist_MultipleEntries_SameProcessBackward", + "TestDllBlocklist_MultipleEntries_SameProcessForward", + "TestDllBlocklist_NoOpEntryPoint", + "TestDllBlocklist_SingleNotification1", + "TestDllBlocklist_SingleNotification2", + "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/gtest/rust/Cargo.toml b/toolkit/xre/dllservices/tests/gtest/rust/Cargo.toml new file mode 100644 index 0000000000..a342fb8bf2 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/rust/Cargo.toml @@ -0,0 +1,12 @@ +[package]
+name = "dllservices-gtest"
+version = "0.1.0"
+authors = ["nobody@mozilla.com"]
+license = "MPL-2.0"
+description = "Tests for dllservices"
+
+[dependencies]
+uuid = { version = "1.0", features = ["v4"] }
+
+[lib]
+path = "test.rs"
diff --git a/toolkit/xre/dllservices/tests/gtest/rust/TestBCryptFallback.cpp b/toolkit/xre/dllservices/tests/gtest/rust/TestBCryptFallback.cpp new file mode 100644 index 0000000000..ca4dbde3ab --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/rust/TestBCryptFallback.cpp @@ -0,0 +1,113 @@ +/* -*- 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 <ntstatus.h> + +#include <bcrypt.h> +#pragma comment(lib, "bcrypt.lib") + +#include "gtest/gtest.h" + +#include "nsWindowsDllInterceptor.h" + +#define RtlGenRandom SystemFunction036 +extern "C" BOOLEAN NTAPI RtlGenRandom(PVOID aRandomBuffer, + ULONG aRandomBufferLength); + +static mozilla::WindowsDllInterceptor BCryptIntercept; +static mozilla::WindowsDllInterceptor::FuncHookType< + decltype(&::BCryptGenRandom)> + stub_BCryptGenRandom; + +static mozilla::WindowsDllInterceptor AdvApiIntercept; +static mozilla::WindowsDllInterceptor::FuncHookType<decltype(&RtlGenRandom)> + stub_RtlGenRandom; + +volatile bool gAreHooksActive = false; +volatile bool gHasPanicked = false; +volatile bool gHasReachedBCryptGenRandom = false; +volatile bool gHasReachedRtlGenRandom = false; + +NTSTATUS WINAPI patched_BCryptGenRandom(BCRYPT_ALG_HANDLE aAlgorithm, + PUCHAR aBuffer, ULONG aSize, + ULONG aFlags) { + if (gAreHooksActive) { + gHasReachedBCryptGenRandom = true; + // Force BCryptGenRandom failures when the hook is active. + return STATUS_UNSUCCESSFUL; + } + return stub_BCryptGenRandom(aAlgorithm, aBuffer, aSize, aFlags); +} + +BOOLEAN NTAPI patched_RtlGenRandom(PVOID aRandomBuffer, + ULONG aRandomBufferLength) { + if (gAreHooksActive) { + gHasReachedRtlGenRandom = true; + } + return stub_RtlGenRandom(aRandomBuffer, aRandomBufferLength); +} + +bool InitInterception() { + static bool sSuccess = []() { + BCryptIntercept.Init(L"bcrypt.dll"); + AdvApiIntercept.Init(L"advapi32.dll"); + return stub_BCryptGenRandom.SetDetour(BCryptIntercept, "BCryptGenRandom", + patched_BCryptGenRandom) && + stub_RtlGenRandom.SetDetour(AdvApiIntercept, "SystemFunction036", + patched_RtlGenRandom); + }(); + gAreHooksActive = true; + return sSuccess; +} + +void ExitInterception() { gAreHooksActive = false; } + +DWORD WINAPI TestIsFallbackTriggeredThreadProc(LPVOID aParameter) { + auto testedFunction = reinterpret_cast<void (*)()>(aParameter); + EXPECT_TRUE(InitInterception()); + MOZ_SEH_TRY { testedFunction(); } + MOZ_SEH_EXCEPT(EXCEPTION_EXECUTE_HANDLER) { + // Catch a potential Rust panic + gHasPanicked = true; + } + ExitInterception(); + return 0; +} + +// This function hooks BCryptGenRandom to make it fail, and hooks RtlGenRandom +// to allow us to ensure that it gets visited as a fallback for +// BCryptGenRandom. +void TestIsFallbackTriggered(void (*aTestedFunction)()) { + gHasPanicked = false; + gHasReachedBCryptGenRandom = false; + gHasReachedRtlGenRandom = false; + + // The HashMap test must run on a new thread, because some random bytes have + // already been collected but not used on the current thread by previous + // calls to HashMap::new in various locations of the code base. These bytes + // would be recycled instead of calling into BCryptGenRandom and RtlGenRandom + // if running the HashMap test on the current thread. + auto thread = + ::CreateThread(nullptr, 0, TestIsFallbackTriggeredThreadProc, + reinterpret_cast<void*>(aTestedFunction), 0, nullptr); + EXPECT_TRUE(bool(thread)); + EXPECT_EQ(::WaitForSingleObject(thread, 5000), + static_cast<DWORD>(WAIT_OBJECT_0)); + + EXPECT_FALSE(gHasPanicked); + EXPECT_TRUE(gHasReachedBCryptGenRandom); + EXPECT_TRUE(gHasReachedRtlGenRandom); +} + +extern "C" void Rust_TriggerGenRandomFromHashMap(); +extern "C" void Rust_TriggerGenRandomFromUuid(); + +TEST(TestBCryptFallback, HashMapTriggersFallback) +{ TestIsFallbackTriggered(Rust_TriggerGenRandomFromHashMap); } + +TEST(TestBCryptFallback, UuidTriggersFallback) +{ TestIsFallbackTriggered(Rust_TriggerGenRandomFromUuid); } diff --git a/toolkit/xre/dllservices/tests/gtest/rust/moz.build b/toolkit/xre/dllservices/tests/gtest/rust/moz.build new file mode 100644 index 0000000000..eea8b9e978 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/rust/moz.build @@ -0,0 +1,11 @@ +# 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 += [ + "TestBCryptFallback.cpp", +] + +FINAL_LIBRARY = "xul-gtest" diff --git a/toolkit/xre/dllservices/tests/gtest/rust/test.rs b/toolkit/xre/dllservices/tests/gtest/rust/test.rs new file mode 100644 index 0000000000..51c2a6e156 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/rust/test.rs @@ -0,0 +1,19 @@ +/* -*- Mode: rust; rust-indent-offset: 2 -*- */ +/* 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/. */ + +use std::collections::HashMap; + +extern crate uuid; +use uuid::Uuid; + +#[no_mangle] +pub extern "C" fn Rust_TriggerGenRandomFromHashMap() -> () { + let _: HashMap<u32, u32> = HashMap::new(); +} + +#[no_mangle] +pub extern "C" fn Rust_TriggerGenRandomFromUuid() -> () { + let _: Uuid = Uuid::new_v4(); +} diff --git a/toolkit/xre/dllservices/tests/moz.build b/toolkit/xre/dllservices/tests/moz.build new file mode 100644 index 0000000000..957243a476 --- /dev/null +++ b/toolkit/xre/dllservices/tests/moz.build @@ -0,0 +1,47 @@ +# -*- 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( + [ + "TestDllBlocklistAssumptions", + "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", +] diff --git a/toolkit/xre/glxtest/glxtest.cpp b/toolkit/xre/glxtest/glxtest.cpp new file mode 100644 index 0000000000..33a21c11f7 --- /dev/null +++ b/toolkit/xre/glxtest/glxtest.cpp @@ -0,0 +1,1034 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=8 et : + */ +/* 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/. */ + +////////////////////////////////////////////////////////////////////////////// +// +// Explanation: See bug 639842. Safely getting GL driver info on X11 is hard, +// because the only way to do that is to create a GL context and call +// glGetString(), but with bad drivers, just creating a GL context may crash. +// +// This file implements the idea to do that in a separate process. +// +// The only non-static function here is fire_glxtest_process(). It creates a +// pipe, publishes its 'read' end as the mozilla::widget::glxtest_pipe global +// variable, forks, and runs that GLX probe in the child process, which runs the +// childgltest() static function. This creates a X connection, a GLX context, +// calls glGetString, and writes that to the 'write' end of the pipe. + +#include <cstdio> +#include <cstdlib> +#include <dlfcn.h> +#include <fcntl.h> +#include <unistd.h> +#include <getopt.h> +#include <vector> +#include <stdint.h> +#include <string.h> +#include <stdarg.h> + +#ifdef MOZ_WAYLAND +# include <gdk/gdk.h> +#endif + +#if defined(MOZ_ASAN) || defined(FUZZING) +# include <signal.h> +#endif + +#ifdef __SUNPRO_CC +# include <stdio.h> +#endif + +#ifdef MOZ_X11 +# include "X11/Xlib.h" +# include "X11/Xutil.h" +# include <X11/extensions/Xrandr.h> +#endif + +#include <vector> +#include <sys/wait.h> +#include "mozilla/ScopeExit.h" +#include "mozilla/Types.h" + +#include "mozilla/GfxInfoUtils.h" + +#ifdef MOZ_X11 +// stuff from glx.h +typedef struct __GLXcontextRec* GLXContext; +typedef XID GLXPixmap; +typedef XID GLXDrawable; +/* GLX 1.3 and later */ +typedef struct __GLXFBConfigRec* GLXFBConfig; +typedef XID GLXFBConfigID; +typedef XID GLXContextID; +typedef XID GLXWindow; +typedef XID GLXPbuffer; +# define GLX_RGBA 4 +# define GLX_RED_SIZE 8 +# define GLX_GREEN_SIZE 9 +# define GLX_BLUE_SIZE 10 +# define GLX_DOUBLEBUFFER 5 +#endif + +// stuff from gl.h +typedef uint8_t GLubyte; +typedef uint32_t GLenum; +#define GL_VENDOR 0x1F00 +#define GL_RENDERER 0x1F01 +#define GL_VERSION 0x1F02 + +// GLX_MESA_query_renderer +// clang-format off +#define GLX_RENDERER_VENDOR_ID_MESA 0x8183 +#define GLX_RENDERER_DEVICE_ID_MESA 0x8184 +#define GLX_RENDERER_VERSION_MESA 0x8185 +#define GLX_RENDERER_ACCELERATED_MESA 0x8186 +#define GLX_RENDERER_VIDEO_MEMORY_MESA 0x8187 +#define GLX_RENDERER_UNIFIED_MEMORY_ARCHITECTURE_MESA 0x8188 +#define GLX_RENDERER_PREFERRED_PROFILE_MESA 0x8189 +#define GLX_RENDERER_OPENGL_CORE_PROFILE_VERSION_MESA 0x818A +#define GLX_RENDERER_OPENGL_COMPATIBILITY_PROFILE_VERSION_MESA 0x818B +#define GLX_RENDERER_OPENGL_ES_PROFILE_VERSION_MESA 0x818C +#define GLX_RENDERER_OPENGL_ES2_PROFILE_VERSION_MESA 0x818D +#define GLX_RENDERER_ID_MESA 0x818E +// clang-format on + +// stuff from egl.h +typedef intptr_t EGLAttrib; +typedef int EGLBoolean; +typedef void* EGLConfig; +typedef void* EGLContext; +typedef void* EGLDeviceEXT; +typedef void* EGLDisplay; +typedef unsigned int EGLenum; +typedef int EGLint; +typedef void* EGLNativeDisplayType; +typedef void* EGLSurface; +typedef void* (*PFNEGLGETPROCADDRESS)(const char*); + +#define EGL_NO_CONTEXT nullptr +#define EGL_NO_SURFACE nullptr +#define EGL_FALSE 0 +#define EGL_TRUE 1 +#define EGL_OPENGL_ES2_BIT 0x0004 +#define EGL_BLUE_SIZE 0x3022 +#define EGL_GREEN_SIZE 0x3023 +#define EGL_RED_SIZE 0x3024 +#define EGL_NONE 0x3038 +#define EGL_RENDERABLE_TYPE 0x3040 +#define EGL_VENDOR 0x3053 +#define EGL_EXTENSIONS 0x3055 +#define EGL_CONTEXT_MAJOR_VERSION 0x3098 +#define EGL_OPENGL_ES_API 0x30A0 +#define EGL_OPENGL_API 0x30A2 +#define EGL_DEVICE_EXT 0x322C +#define EGL_DRM_DEVICE_FILE_EXT 0x3233 +#define EGL_DRM_RENDER_NODE_FILE_EXT 0x3377 + +// stuff from xf86drm.h +#define DRM_NODE_RENDER 2 +#define DRM_NODE_MAX 3 + +typedef struct _drmPciDeviceInfo { + uint16_t vendor_id; + uint16_t device_id; + uint16_t subvendor_id; + uint16_t subdevice_id; + uint8_t revision_id; +} drmPciDeviceInfo, *drmPciDeviceInfoPtr; + +typedef struct _drmDevice { + char** nodes; + int available_nodes; + int bustype; + union { + void* pci; + void* usb; + void* platform; + void* host1x; + } businfo; + union { + drmPciDeviceInfoPtr pci; + void* usb; + void* platform; + void* host1x; + } deviceinfo; +} drmDevice, *drmDevicePtr; + +// Open libGL and load needed symbols +#if defined(__OpenBSD__) || defined(__NetBSD__) +# define LIBGL_FILENAME "libGL.so" +# define LIBGLES_FILENAME "libGLESv2.so" +# define LIBEGL_FILENAME "libEGL.so" +# define LIBDRM_FILENAME "libdrm.so" +#else +# define LIBGL_FILENAME "libGL.so.1" +# define LIBGLES_FILENAME "libGLESv2.so.2" +# define LIBEGL_FILENAME "libEGL.so.1" +# define LIBDRM_FILENAME "libdrm.so.2" +#endif + +#ifdef MOZ_X11 +static int x_error_handler(Display*, XErrorEvent* ev) { + record_value( + "ERROR\nX error, error_code=%d, " + "request_code=%d, minor_code=%d\n", + ev->error_code, ev->request_code, ev->minor_code); + record_flush(); + _exit(EXIT_FAILURE); +} +#endif + +// childgltest is declared inside extern "C" so that the name is not mangled. +// The name is used in build/valgrind/x86_64-pc-linux-gnu.sup to suppress +// memory leak errors because we run it inside a short lived fork and we don't +// care about leaking memory +extern "C" { + +#define PCI_FILL_IDENT 0x0001 +#define PCI_FILL_CLASS 0x0020 +#define PCI_BASE_CLASS_DISPLAY 0x03 + +static void get_pci_status() { + log("GLX_TEST: get_pci_status start\n"); + + if (access("/sys/bus/pci/", F_OK) != 0 && + access("/sys/bus/pci_express/", F_OK) != 0) { + record_warning("cannot access /sys/bus/pci"); + return; + } + + void* libpci = dlopen("libpci.so.3", RTLD_LAZY); + if (!libpci) { + libpci = dlopen("libpci.so", RTLD_LAZY); + } + if (!libpci) { + record_warning("libpci missing"); + return; + } + auto release = mozilla::MakeScopeExit([&] { dlclose(libpci); }); + + typedef struct pci_dev { + struct pci_dev* next; + uint16_t domain_16; + uint8_t bus, dev, func; + unsigned int known_fields; + uint16_t vendor_id, device_id; + uint16_t device_class; + } pci_dev; + + typedef struct pci_access { + unsigned int method; + int writeable; + int buscentric; + char* id_file_name; + int free_id_name; + int numeric_ids; + unsigned int id_lookup_mode; + int debugging; + void* error; + void* warning; + void* debug; + pci_dev* devices; + } pci_access; + + typedef pci_access* (*PCIALLOC)(void); + PCIALLOC pci_alloc = cast<PCIALLOC>(dlsym(libpci, "pci_alloc")); + + typedef void (*PCIINIT)(pci_access*); + PCIINIT pci_init = cast<PCIINIT>(dlsym(libpci, "pci_init")); + + typedef void (*PCICLEANUP)(pci_access*); + PCICLEANUP pci_cleanup = cast<PCICLEANUP>(dlsym(libpci, "pci_cleanup")); + + typedef void (*PCISCANBUS)(pci_access*); + PCISCANBUS pci_scan_bus = cast<PCISCANBUS>(dlsym(libpci, "pci_scan_bus")); + + typedef void (*PCIFILLINFO)(pci_dev*, int); + PCIFILLINFO pci_fill_info = cast<PCIFILLINFO>(dlsym(libpci, "pci_fill_info")); + + if (!pci_alloc || !pci_cleanup || !pci_scan_bus || !pci_fill_info) { + dlclose(libpci); + record_warning("libpci missing methods"); + return; + } + + pci_access* pacc = pci_alloc(); + if (!pacc) { + record_warning("libpci alloc failed"); + return; + } + + pci_init(pacc); + pci_scan_bus(pacc); + + for (pci_dev* dev = pacc->devices; dev; dev = dev->next) { + pci_fill_info(dev, PCI_FILL_IDENT | PCI_FILL_CLASS); + if (dev->device_class >> 8 == PCI_BASE_CLASS_DISPLAY && dev->vendor_id && + dev->device_id) { + record_value("PCI_VENDOR_ID\n0x%04x\nPCI_DEVICE_ID\n0x%04x\n", + dev->vendor_id, dev->device_id); + } + } + + pci_cleanup(pacc); + + log("GLX_TEST: get_pci_status finished\n"); +} + +#ifdef MOZ_WAYLAND +static void set_render_device_path(const char* render_device_path) { + record_value("DRM_RENDERDEVICE\n%s\n", render_device_path); +} + +static bool device_has_name(const drmDevice* device, const char* name) { + for (size_t i = 0; i < DRM_NODE_MAX; i++) { + if (!(device->available_nodes & (1 << i))) { + continue; + } + if (strcmp(device->nodes[i], name) == 0) { + return true; + } + } + return false; +} + +static bool get_render_name(const char* name) { + void* libdrm = dlopen(LIBDRM_FILENAME, RTLD_LAZY); + if (!libdrm) { + record_warning("Failed to open libdrm"); + return false; + } + auto release = mozilla::MakeScopeExit([&] { dlclose(libdrm); }); + + typedef int (*DRMGETDEVICES2)(uint32_t, drmDevicePtr*, int); + DRMGETDEVICES2 drmGetDevices2 = + cast<DRMGETDEVICES2>(dlsym(libdrm, "drmGetDevices2")); + + typedef void (*DRMFREEDEVICE)(drmDevicePtr*); + DRMFREEDEVICE drmFreeDevice = + cast<DRMFREEDEVICE>(dlsym(libdrm, "drmFreeDevice")); + + if (!drmGetDevices2 || !drmFreeDevice) { + record_warning( + "libdrm missing methods for drmGetDevices2 or drmFreeDevice"); + return false; + } + + uint32_t flags = 0; + int devices_len = drmGetDevices2(flags, nullptr, 0); + if (devices_len < 0) { + record_warning("drmGetDevices2 failed"); + return false; + } + drmDevice** devices = (drmDevice**)calloc(devices_len, sizeof(drmDevice*)); + if (!devices) { + record_warning("Allocation error"); + return false; + } + devices_len = drmGetDevices2(flags, devices, devices_len); + if (devices_len < 0) { + free(devices); + record_warning("drmGetDevices2 failed"); + return false; + } + + const drmDevice* match = nullptr; + for (int i = 0; i < devices_len; i++) { + if (device_has_name(devices[i], name)) { + match = devices[i]; + break; + } + } + + // Fallback path for split kms/render devices - if only one drm render node + // exists it's most likely the one we're looking for. + if (match && !(match->available_nodes & (1 << DRM_NODE_RENDER))) { + match = nullptr; + for (int i = 0; i < devices_len; i++) { + if (devices[i]->available_nodes & (1 << DRM_NODE_RENDER)) { + if (!match) { + match = devices[i]; + } else { + // more than one candidate found, stop trying. + match = nullptr; + break; + } + } + } + if (match) { + record_warning( + "DRM render node not clearly detectable. Falling back to using the " + "only one that was found."); + } else { + record_warning("DRM device has no render node"); + } + } + + bool result = false; + if (!match) { + record_warning("Cannot find DRM device"); + } else { + set_render_device_path(match->nodes[DRM_NODE_RENDER]); + record_value( + "MESA_VENDOR_ID\n0x%04x\n" + "MESA_DEVICE_ID\n0x%04x\n", + match->deviceinfo.pci->vendor_id, match->deviceinfo.pci->device_id); + result = true; + } + + for (int i = 0; i < devices_len; i++) { + drmFreeDevice(&devices[i]); + } + free(devices); + return result; +} +#endif + +static bool get_egl_gl_status(EGLDisplay dpy, + PFNEGLGETPROCADDRESS eglGetProcAddress) { + typedef EGLBoolean (*PFNEGLCHOOSECONFIGPROC)( + EGLDisplay dpy, EGLint const* attrib_list, EGLConfig* configs, + EGLint config_size, EGLint* num_config); + PFNEGLCHOOSECONFIGPROC eglChooseConfig = + cast<PFNEGLCHOOSECONFIGPROC>(eglGetProcAddress("eglChooseConfig")); + + typedef EGLBoolean (*PFNEGLBINDAPIPROC)(EGLint api); + PFNEGLBINDAPIPROC eglBindAPI = + cast<PFNEGLBINDAPIPROC>(eglGetProcAddress("eglBindAPI")); + + typedef EGLContext (*PFNEGLCREATECONTEXTPROC)( + EGLDisplay dpy, EGLConfig config, EGLContext share_context, + EGLint const* attrib_list); + PFNEGLCREATECONTEXTPROC eglCreateContext = + cast<PFNEGLCREATECONTEXTPROC>(eglGetProcAddress("eglCreateContext")); + + typedef EGLBoolean (*PFNEGLDESTROYCONTEXTPROC)(EGLDisplay dpy, + EGLContext ctx); + PFNEGLDESTROYCONTEXTPROC eglDestroyContext = + cast<PFNEGLDESTROYCONTEXTPROC>(eglGetProcAddress("eglDestroyContext")); + + typedef EGLBoolean (*PFNEGLMAKECURRENTPROC)( + EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext context); + PFNEGLMAKECURRENTPROC eglMakeCurrent = + cast<PFNEGLMAKECURRENTPROC>(eglGetProcAddress("eglMakeCurrent")); + + typedef const char* (*PFNEGLQUERYDEVICESTRINGEXTPROC)(EGLDeviceEXT device, + EGLint name); + PFNEGLQUERYDEVICESTRINGEXTPROC eglQueryDeviceStringEXT = + cast<PFNEGLQUERYDEVICESTRINGEXTPROC>( + eglGetProcAddress("eglQueryDeviceStringEXT")); + + typedef EGLBoolean (*PFNEGLQUERYDISPLAYATTRIBEXTPROC)( + EGLDisplay dpy, EGLint name, EGLAttrib* value); + PFNEGLQUERYDISPLAYATTRIBEXTPROC eglQueryDisplayAttribEXT = + cast<PFNEGLQUERYDISPLAYATTRIBEXTPROC>( + eglGetProcAddress("eglQueryDisplayAttribEXT")); + + log("GLX_TEST: get_egl_gl_status start\n"); + + if (!eglChooseConfig || !eglCreateContext || !eglDestroyContext || + !eglMakeCurrent || !eglQueryDeviceStringEXT) { + record_warning("libEGL missing methods for GL test"); + return false; + } + + typedef GLubyte* (*PFNGLGETSTRING)(GLenum); + PFNGLGETSTRING glGetString = + cast<PFNGLGETSTRING>(eglGetProcAddress("glGetString")); + +#if defined(__arm__) || defined(__aarch64__) + bool useGles = true; +#else + bool useGles = false; +#endif + + std::vector<EGLint> attribs; + attribs.push_back(EGL_RED_SIZE); + attribs.push_back(8); + attribs.push_back(EGL_GREEN_SIZE); + attribs.push_back(8); + attribs.push_back(EGL_BLUE_SIZE); + attribs.push_back(8); + if (useGles) { + attribs.push_back(EGL_RENDERABLE_TYPE); + attribs.push_back(EGL_OPENGL_ES2_BIT); + } + attribs.push_back(EGL_NONE); + + EGLConfig config; + EGLint num_config; + if (eglChooseConfig(dpy, attribs.data(), &config, 1, &num_config) == + EGL_FALSE) { + record_warning("eglChooseConfig returned an error"); + return false; + } + + EGLenum api = useGles ? EGL_OPENGL_ES_API : EGL_OPENGL_API; + if (eglBindAPI(api) == EGL_FALSE) { + record_warning("eglBindAPI returned an error"); + return false; + } + + EGLint ctx_attrs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE}; + EGLContext ectx = eglCreateContext(dpy, config, EGL_NO_CONTEXT, ctx_attrs); + if (!ectx) { + EGLint ctx_attrs_fallback[] = {EGL_CONTEXT_MAJOR_VERSION, 2, EGL_NONE}; + ectx = eglCreateContext(dpy, config, EGL_NO_CONTEXT, ctx_attrs_fallback); + if (!ectx) { + record_warning("eglCreateContext returned an error"); + return false; + } + } + + if (eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ectx) == EGL_FALSE) { + eglDestroyContext(dpy, ectx); + record_warning("eglMakeCurrent returned an error"); + return false; + } + eglDestroyContext(dpy, ectx); + + // Implementations disagree about whether eglGetProcAddress or dlsym + // should be used for getting functions from the actual API, see + // https://github.com/anholt/libepoxy/commit/14f24485e33816139398d1bd170d617703473738 + void* libgl = nullptr; + if (!glGetString) { + libgl = dlopen(LIBGL_FILENAME, RTLD_LAZY); + if (!libgl) { + libgl = dlopen(LIBGLES_FILENAME, RTLD_LAZY); + if (!libgl) { + record_warning(LIBGL_FILENAME " and " LIBGLES_FILENAME " missing"); + return false; + } + } + + glGetString = cast<PFNGLGETSTRING>(dlsym(libgl, "glGetString")); + if (!glGetString) { + dlclose(libgl); + record_warning("libEGL, libGL and libGLESv2 are missing glGetString"); + return false; + } + } + auto release = mozilla::MakeScopeExit([&] { + if (libgl) { + dlclose(libgl); + } + }); + + const GLubyte* versionString = glGetString(GL_VERSION); + const GLubyte* vendorString = glGetString(GL_VENDOR); + const GLubyte* rendererString = glGetString(GL_RENDERER); + + if (versionString && vendorString && rendererString) { + record_value("VENDOR\n%s\nRENDERER\n%s\nVERSION\n%s\nTFP\nTRUE\n", + vendorString, rendererString, versionString); + } else { + record_warning("EGL glGetString returned null"); + return false; + } + + EGLDeviceEXT device; + if (eglQueryDisplayAttribEXT(dpy, EGL_DEVICE_EXT, (EGLAttrib*)&device) == + EGL_TRUE) { + const char* deviceExtensions = + eglQueryDeviceStringEXT(device, EGL_EXTENSIONS); + if (deviceExtensions && + strstr(deviceExtensions, "EGL_MESA_device_software")) { + record_value("MESA_ACCELERATED\nFALSE\n"); + } else { +#ifdef MOZ_WAYLAND + const char* deviceString = + eglQueryDeviceStringEXT(device, EGL_DRM_DEVICE_FILE_EXT); + if (!deviceString || !get_render_name(deviceString)) { + const char* renderNodeString = + eglQueryDeviceStringEXT(device, EGL_DRM_RENDER_NODE_FILE_EXT); + if (renderNodeString) { + set_render_device_path(renderNodeString); + } + } +#endif + } + } + + log("GLX_TEST: get_egl_gl_status finished\n"); + return true; +} + +static bool get_egl_status(EGLNativeDisplayType native_dpy) { + log("GLX_TEST: get_egl_status start\n"); + + EGLDisplay dpy = nullptr; + + typedef EGLBoolean (*PFNEGLTERMINATEPROC)(EGLDisplay dpy); + PFNEGLTERMINATEPROC eglTerminate = nullptr; + + void* libegl = dlopen(LIBEGL_FILENAME, RTLD_LAZY); + if (!libegl) { + record_warning("libEGL missing"); + return false; + } + auto release = mozilla::MakeScopeExit([&] { + if (dpy) { + eglTerminate(dpy); + } + // Unload libegl here causes ASAN/MemLeaks failures on Linux + // as libegl may not be meant to be unloaded runtime. + // See 1304156 for reference. + // dlclose(libegl); + }); + + PFNEGLGETPROCADDRESS eglGetProcAddress = + cast<PFNEGLGETPROCADDRESS>(dlsym(libegl, "eglGetProcAddress")); + + if (!eglGetProcAddress) { + record_warning("no eglGetProcAddress"); + return false; + } + + typedef EGLDisplay (*PFNEGLGETDISPLAYPROC)(void* native_display); + PFNEGLGETDISPLAYPROC eglGetDisplay = + cast<PFNEGLGETDISPLAYPROC>(eglGetProcAddress("eglGetDisplay")); + + typedef EGLBoolean (*PFNEGLINITIALIZEPROC)(EGLDisplay dpy, EGLint* major, + EGLint* minor); + PFNEGLINITIALIZEPROC eglInitialize = + cast<PFNEGLINITIALIZEPROC>(eglGetProcAddress("eglInitialize")); + eglTerminate = cast<PFNEGLTERMINATEPROC>(eglGetProcAddress("eglTerminate")); + + if (!eglGetDisplay || !eglInitialize || !eglTerminate) { + record_warning("libEGL missing methods"); + return false; + } + + dpy = eglGetDisplay(native_dpy); + if (!dpy) { + record_warning("libEGL no display"); + return false; + } + + EGLint major, minor; + if (!eglInitialize(dpy, &major, &minor)) { + record_warning("libEGL initialize failed"); + return false; + } + + typedef const char* (*PFNEGLGETDISPLAYDRIVERNAMEPROC)(EGLDisplay dpy); + PFNEGLGETDISPLAYDRIVERNAMEPROC eglGetDisplayDriverName = + cast<PFNEGLGETDISPLAYDRIVERNAMEPROC>( + eglGetProcAddress("eglGetDisplayDriverName")); + if (eglGetDisplayDriverName) { + const char* driDriver = eglGetDisplayDriverName(dpy); + if (driDriver) { + record_value("DRI_DRIVER\n%s\n", driDriver); + } + } + + bool ret = get_egl_gl_status(dpy, eglGetProcAddress); + log("GLX_TEST: get_egl_status finished with return: %d\n", ret); + + return ret; +} + +#ifdef MOZ_X11 +static void get_xrandr_info(Display* dpy) { + log("GLX_TEST: get_xrandr_info start\n"); + + // When running on remote X11 the xrandr version may be stuck on an ancient + // version. There are still setups using remote X11 out there, so make sure we + // don't crash. + int eventBase, errorBase, major, minor; + if (!XRRQueryExtension(dpy, &eventBase, &errorBase) || + !XRRQueryVersion(dpy, &major, &minor) || + !(major > 1 || (major == 1 && minor >= 4))) { + log("GLX_TEST: get_xrandr_info failed, old version.\n"); + return; + } + + Window root = RootWindow(dpy, DefaultScreen(dpy)); + XRRProviderResources* pr = XRRGetProviderResources(dpy, root); + if (!pr) { + return; + } + XRRScreenResources* res = XRRGetScreenResourcesCurrent(dpy, root); + if (!res) { + XRRFreeProviderResources(pr); + return; + } + if (pr->nproviders != 0) { + record_value("DDX_DRIVER\n"); + for (int i = 0; i < pr->nproviders; i++) { + XRRProviderInfo* info = XRRGetProviderInfo(dpy, res, pr->providers[i]); + if (info) { + record_value("%s%s", info->name, i == pr->nproviders - 1 ? ";\n" : ";"); + XRRFreeProviderInfo(info); + } + } + } + XRRFreeScreenResources(res); + XRRFreeProviderResources(pr); + + log("GLX_TEST: get_xrandr_info finished\n"); +} + +void glxtest() { + log("GLX_TEST: glxtest start\n"); + + Display* dpy = nullptr; + void* libgl = dlopen(LIBGL_FILENAME, RTLD_LAZY); + if (!libgl) { + record_error(LIBGL_FILENAME " missing"); + return; + } + auto release = mozilla::MakeScopeExit([&] { + if (dpy) { +# ifdef MOZ_ASAN + XCloseDisplay(dpy); +# else + // This XSync call wanted to be instead: + // XCloseDisplay(dpy); + // but this can cause 1-minute stalls on certain setups using Nouveau, see + // bug 973192 + XSync(dpy, False); +# endif + } + dlclose(libgl); + }); + + typedef void* (*PFNGLXGETPROCADDRESS)(const char*); + PFNGLXGETPROCADDRESS glXGetProcAddress = + cast<PFNGLXGETPROCADDRESS>(dlsym(libgl, "glXGetProcAddress")); + + if (!glXGetProcAddress) { + record_error("no glXGetProcAddress"); + return; + } + + typedef GLXFBConfig* (*PFNGLXQUERYEXTENSION)(Display*, int*, int*); + PFNGLXQUERYEXTENSION glXQueryExtension = + cast<PFNGLXQUERYEXTENSION>(glXGetProcAddress("glXQueryExtension")); + + typedef GLXFBConfig* (*PFNGLXQUERYVERSION)(Display*, int*, int*); + PFNGLXQUERYVERSION glXQueryVersion = + cast<PFNGLXQUERYVERSION>(dlsym(libgl, "glXQueryVersion")); + + typedef XVisualInfo* (*PFNGLXCHOOSEVISUAL)(Display*, int, int*); + PFNGLXCHOOSEVISUAL glXChooseVisual = + cast<PFNGLXCHOOSEVISUAL>(glXGetProcAddress("glXChooseVisual")); + + typedef GLXContext (*PFNGLXCREATECONTEXT)(Display*, XVisualInfo*, GLXContext, + Bool); + PFNGLXCREATECONTEXT glXCreateContext = + cast<PFNGLXCREATECONTEXT>(glXGetProcAddress("glXCreateContext")); + + typedef Bool (*PFNGLXMAKECURRENT)(Display*, GLXDrawable, GLXContext); + PFNGLXMAKECURRENT glXMakeCurrent = + cast<PFNGLXMAKECURRENT>(glXGetProcAddress("glXMakeCurrent")); + + typedef void (*PFNGLXDESTROYCONTEXT)(Display*, GLXContext); + PFNGLXDESTROYCONTEXT glXDestroyContext = + cast<PFNGLXDESTROYCONTEXT>(glXGetProcAddress("glXDestroyContext")); + + typedef GLubyte* (*PFNGLGETSTRING)(GLenum); + PFNGLGETSTRING glGetString = + cast<PFNGLGETSTRING>(glXGetProcAddress("glGetString")); + + if (!glXQueryExtension || !glXQueryVersion || !glXChooseVisual || + !glXCreateContext || !glXMakeCurrent || !glXDestroyContext || + !glGetString) { + record_error(LIBGL_FILENAME " missing methods"); + return; + } + + ///// Open a connection to the X server ///// + dpy = XOpenDisplay(nullptr); + if (!dpy) { + record_error("Unable to open a connection to the X server"); + return; + } + + ///// Check that the GLX extension is present ///// + if (!glXQueryExtension(dpy, nullptr, nullptr)) { + record_error("GLX extension missing"); + return; + } + + XSetErrorHandler(x_error_handler); + + ///// Get a visual ///// + int attribs[] = {GLX_RGBA, GLX_RED_SIZE, 1, GLX_GREEN_SIZE, + 1, GLX_BLUE_SIZE, 1, None}; + XVisualInfo* vInfo = glXChooseVisual(dpy, DefaultScreen(dpy), attribs); + if (!vInfo) { + int attribs2[] = {GLX_RGBA, GLX_RED_SIZE, 1, GLX_GREEN_SIZE, + 1, GLX_BLUE_SIZE, 1, GLX_DOUBLEBUFFER, + None}; + vInfo = glXChooseVisual(dpy, DefaultScreen(dpy), attribs2); + if (!vInfo) { + record_error("No visuals found"); + return; + } + } + + // using a X11 Window instead of a GLXPixmap does not crash + // fglrx in indirect rendering. bug 680644 + Window window; + XSetWindowAttributes swa; + swa.colormap = XCreateColormap(dpy, RootWindow(dpy, vInfo->screen), + vInfo->visual, AllocNone); + + swa.border_pixel = 0; + window = XCreateWindow(dpy, RootWindow(dpy, vInfo->screen), 0, 0, 16, 16, 0, + vInfo->depth, InputOutput, vInfo->visual, + CWBorderPixel | CWColormap, &swa); + + ///// Get a GL context and make it current ////// + GLXContext context = glXCreateContext(dpy, vInfo, nullptr, True); + glXMakeCurrent(dpy, window, context); + + ///// Look for this symbol to determine texture_from_pixmap support ///// + void* glXBindTexImageEXT = glXGetProcAddress("glXBindTexImageEXT"); + + ///// Get GL vendor/renderer/versions strings ///// + const GLubyte* versionString = glGetString(GL_VERSION); + const GLubyte* vendorString = glGetString(GL_VENDOR); + const GLubyte* rendererString = glGetString(GL_RENDERER); + + if (versionString && vendorString && rendererString) { + record_value("VENDOR\n%s\nRENDERER\n%s\nVERSION\n%s\nTFP\n%s\n", + vendorString, rendererString, versionString, + glXBindTexImageEXT ? "TRUE" : "FALSE"); + } else { + record_error("glGetString returned null"); + } + + // If GLX_MESA_query_renderer is available, populate additional data. + typedef Bool (*PFNGLXQUERYCURRENTRENDERERINTEGERMESAPROC)( + int attribute, unsigned int* value); + PFNGLXQUERYCURRENTRENDERERINTEGERMESAPROC + glXQueryCurrentRendererIntegerMESAProc = + cast<PFNGLXQUERYCURRENTRENDERERINTEGERMESAPROC>( + glXGetProcAddress("glXQueryCurrentRendererIntegerMESA")); + if (glXQueryCurrentRendererIntegerMESAProc) { + unsigned int vendorId, deviceId, accelerated, videoMemoryMB; + glXQueryCurrentRendererIntegerMESAProc(GLX_RENDERER_VENDOR_ID_MESA, + &vendorId); + glXQueryCurrentRendererIntegerMESAProc(GLX_RENDERER_DEVICE_ID_MESA, + &deviceId); + glXQueryCurrentRendererIntegerMESAProc(GLX_RENDERER_ACCELERATED_MESA, + &accelerated); + glXQueryCurrentRendererIntegerMESAProc(GLX_RENDERER_VIDEO_MEMORY_MESA, + &videoMemoryMB); + + // Truncate IDs to 4 digits- that's all PCI IDs are. + vendorId &= 0xFFFF; + deviceId &= 0xFFFF; + + record_value( + "MESA_VENDOR_ID\n0x%04x\n" + "MESA_DEVICE_ID\n0x%04x\n" + "MESA_ACCELERATED\n%s\n" + "MESA_VRAM\n%dMB\n", + vendorId, deviceId, accelerated ? "TRUE" : "FALSE", videoMemoryMB); + } + + // From Mesa's GL/internal/dri_interface.h, to be used by DRI clients. + typedef const char* (*PFNGLXGETSCREENDRIVERPROC)(Display* dpy, int scrNum); + PFNGLXGETSCREENDRIVERPROC glXGetScreenDriverProc = + cast<PFNGLXGETSCREENDRIVERPROC>(glXGetProcAddress("glXGetScreenDriver")); + if (glXGetScreenDriverProc) { + const char* driDriver = glXGetScreenDriverProc(dpy, DefaultScreen(dpy)); + if (driDriver) { + record_value("DRI_DRIVER\n%s\n", driDriver); + } + } + + // Get monitor and DDX driver information + get_xrandr_info(dpy); + + ///// Clean up. Indeed, the parent process might fail to kill us (e.g. if it + ///// doesn't need to check GL info) so we might be staying alive for longer + ///// than expected, so it's important to consume as little memory as + ///// possible. Also we want to check that we're able to do that too without + ///// generating X errors. + glXMakeCurrent(dpy, None, + nullptr); // must release the GL context before destroying it + glXDestroyContext(dpy, context); + XDestroyWindow(dpy, window); + XFreeColormap(dpy, swa.colormap); + XFree(vInfo); + + record_value("TEST_TYPE\nGLX\n"); + log("GLX_TEST: glxtest finished\n"); +} + +bool x11_egltest() { + log("GLX_TEST: x11_egltest start\n"); + + Display* dpy = XOpenDisplay(nullptr); + if (!dpy) { + return false; + } +# ifdef MOZ_ASAN + auto release = mozilla::MakeScopeExit([&] { + // Bug 1715245: Closing the display connection here crashes on NV prop. + // drivers. Just leave it open, the process will exit shortly after anyway. + XCloseDisplay(dpy); + }); +# endif + + XSetErrorHandler(x_error_handler); + + if (!get_egl_status(dpy)) { + return false; + } + + // Bug 1667621: 30bit "Deep Color" is broken on EGL on Mesa (as of 2021/10). + // Disable all non-standard depths for the initial EGL roleout. + int screenCount = ScreenCount(dpy); + for (int idx = 0; idx < screenCount; idx++) { + if (DefaultDepth(dpy, idx) != 24) { + return false; + } + } + + // Get monitor and DDX driver information + get_xrandr_info(dpy); + + record_value("TEST_TYPE\nEGL\n"); + + log("GLX_TEST: x11_egltest finished\n"); + return true; +} +#endif + +#ifdef MOZ_WAYLAND +void wayland_egltest() { + log("GLX_TEST: wayland_egltest start\n"); + + static auto sWlDisplayConnect = (struct wl_display * (*)(const char*)) + dlsym(RTLD_DEFAULT, "wl_display_connect"); + static auto sWlDisplayRoundtrip = + (int (*)(struct wl_display*))dlsym(RTLD_DEFAULT, "wl_display_roundtrip"); + static auto sWlDisplayDisconnect = (void (*)(struct wl_display*))dlsym( + RTLD_DEFAULT, "wl_display_disconnect"); + + if (!sWlDisplayConnect || !sWlDisplayRoundtrip || !sWlDisplayDisconnect) { + record_error("Missing Wayland libraries"); + return; + } + + // NOTE: returns false to fall back to X11 when the Wayland socket doesn't + // exist but fails with record_error if something actually went wrong + struct wl_display* dpy = sWlDisplayConnect(nullptr); + if (!dpy) { + record_error("Could not connect to wayland socket"); + return; + } + + if (!get_egl_status((EGLNativeDisplayType)dpy)) { + record_error("EGL test failed"); + } + + // This is enough to crash some broken NVIDIA prime + Wayland setups, see + // https://github.com/NVIDIA/egl-wayland/issues/41 and bug 1768260. + sWlDisplayRoundtrip(dpy); + + sWlDisplayDisconnect(dpy); + record_value("TEST_TYPE\nEGL\n"); + log("GLX_TEST: wayland_egltest finished\n"); +} +#endif + +int childgltest(bool aWayland) { + log("GLX_TEST: childgltest start\n"); + + // Get a list of all GPUs from the PCI bus. + get_pci_status(); + +#ifdef MOZ_WAYLAND + if (aWayland) { + wayland_egltest(); + } +#endif +#ifdef MOZ_X11 + if (!aWayland) { + // TODO: --display command line argument is not properly handled + if (!x11_egltest()) { + glxtest(); + } + } +#endif + // Finally write buffered data to the pipe. + record_flush(); + + log("GLX_TEST: childgltest finished\n"); + return EXIT_SUCCESS; +} + +} // extern "C" + +static void PrintUsage() { + printf( + "Firefox OpenGL probe utility\n" + "\n" + "usage: glxtest [options]\n" + "\n" + "Options:\n" + "\n" + " -h --help show this message\n" + " -f --fd num where to print output, default it stdout\n" + " -w --wayland probe OpenGL/EGL on Wayland (default is " + "X11)\n" + "\n"); +} + +int main(int argc, char** argv) { + struct option longOptions[] = {{"help", no_argument, nullptr, 'h'}, + {"fd", required_argument, nullptr, 'f'}, + {"wayland", no_argument, nullptr, 'w'}, + {nullptr, 0, nullptr, 0}}; + const char* shortOptions = "hf:w"; + int c; + bool wayland = false; + while ((c = getopt_long(argc, argv, shortOptions, longOptions, nullptr)) != + -1) { + switch (c) { + case 'w': + wayland = true; + break; + case 'f': + output_pipe = atoi(optarg); + break; + case 'h': +#ifdef MOZ_WAYLAND + // Dummy call to mozgtk to prevent the linker from removing + // the dependency with --as-needed. + // see toolkit/library/moz.build for details. + gdk_display_get_default(); +#endif + PrintUsage(); + return 0; + default: + break; + } + } + if (getenv("MOZ_AVOID_OPENGL_ALTOGETHER")) { + const char* msg = "ERROR\nMOZ_AVOID_OPENGL_ALTOGETHER envvar set"; + MOZ_UNUSED(write(output_pipe, msg, strlen(msg))); + exit(EXIT_FAILURE); + } + const char* env = getenv("MOZ_GFX_DEBUG"); + enable_logging = env && *env == '1'; + if (!enable_logging) { + close_logging(); + } +#if defined(MOZ_ASAN) || defined(FUZZING) + // If handle_segv=1 (default), then glxtest crash will print a sanitizer + // report which can confuse the harness in fuzzing automation. + signal(SIGSEGV, SIG_DFL); +#endif + return childgltest(wayland); +} diff --git a/toolkit/xre/glxtest/moz.build b/toolkit/xre/glxtest/moz.build new file mode 100644 index 0000000000..bc7365582b --- /dev/null +++ b/toolkit/xre/glxtest/moz.build @@ -0,0 +1,26 @@ +# -*- 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Widget: Gtk") + +Program("glxtest") +SOURCES += [ + "glxtest.cpp", +] + +if CONFIG["MOZ_ASAN"]: + SOURCES += [ + "../../../mozglue/build/AsanOptions.cpp", + ] + +CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] +CXXFLAGS += CONFIG["MOZ_PANGO_CFLAGS"] +OS_LIBS += CONFIG["MOZ_X11_LIBS"] +OS_LIBS += CONFIG["MOZ_GTK3_LIBS"] + +NO_PGO = True +DisableStlWrapping() diff --git a/toolkit/xre/metrics.yaml b/toolkit/xre/metrics.yaml new file mode 100644 index 0000000000..c53ee95ecd --- /dev/null +++ b/toolkit/xre/metrics.yaml @@ -0,0 +1,11 @@ +# 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/. + +# Adding a new metric? We have docs for that! +# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 +$tags: + - "Toolkit :: Startup and Profile System" diff --git a/toolkit/xre/moz.build b/toolkit/xre/moz.build new file mode 100644 index 0000000000..dc7695e7ac --- /dev/null +++ b/toolkit/xre/moz.build @@ -0,0 +1,277 @@ +# -*- 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/. + +include("../components/telemetry/telemetry-constants.mozbuild") + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Startup and Profile System") + +if CONFIG["OS_ARCH"] == "WINNT": + TEST_DIRS += ["test/win"] + +MOCHITEST_MANIFESTS += ["test/mochitest.ini"] +BROWSER_CHROME_MANIFESTS += ["test/browser.ini"] +XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell.ini"] +MARIONETTE_UNIT_MANIFESTS += ["test/marionette/marionette.ini"] + +XPIDL_SOURCES += [ + "nsINativeAppSupport.idl", + "nsIXREDirProvider.idl", +] + +if CONFIG["OS_ARCH"] == "WINNT": + XPIDL_SOURCES += [ + "nsIWinAppHelper.idl", + ] + +XPIDL_MODULE = "xulapp" + +EXPORTS += [ + "nsAppRunner.h", +] + +EXPORTS.mozilla += [ + "AutoSQLiteLifetime.h", + "Bootstrap.h", + "CmdLineAndEnvUtils.h", + "GeckoArgs.h", + "MultiInstanceLock.h", + "SafeMode.h", +] + +if CONFIG["MOZ_INSTRUMENT_EVENT_LOOP"]: + EXPORTS += ["EventTracer.h"] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows": + EXPORTS.mozilla += [ + "AssembleCmdLine.h", + "DllPrefetchExperimentRegistryInfo.h", + "PolicyChecks.h", + "WinTokenUtils.h", + ] + UNIFIED_SOURCES += [ + "/toolkit/mozapps/update/common/pathhash.cpp", + "/toolkit/mozapps/update/common/updateutils_win.cpp", + "DllPrefetchExperimentRegistryInfo.cpp", + "nsNativeAppSupportWin.cpp", + "WinTokenUtils.cpp", + ] + DEFINES["PROXY_PRINTING"] = 1 + LOCAL_INCLUDES += [ + "../components/printingui", + ] + if CONFIG["MOZ_LAUNCHER_PROCESS"]: + EXPORTS.mozilla += [ + "LauncherRegistryInfo.h", + ] + UNIFIED_SOURCES += [ + "LauncherRegistryInfo.cpp", + ] + DIRS += [ + "dllservices", + ] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + EXPORTS.mozilla += [ + "MacRunFromDmgUtils.h", + ] + UNIFIED_SOURCES += [ + "MacApplicationDelegate.mm", + "MacAutoreleasePool.mm", + "MacLaunchHelper.mm", + "MacRunFromDmgUtils.mm", + "nsCommandLineServiceMac.mm", + "nsNativeAppSupportCocoa.mm", + "updaterfileutils_osx.mm", + ] + DEFINES["PROXY_PRINTING"] = 1 + LOCAL_INCLUDES += [ + "../components/printingui", + ] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "uikit": + UNIFIED_SOURCES += [ + "nsNativeAppSupportDefault.cpp", + "UIKitDirProvider.mm", + ] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + UNIFIED_SOURCES += [ + "nsNativeAppSupportUnix.cpp", + ] + CXXFLAGS += CONFIG["MOZ_X11_SM_CFLAGS"] +else: + UNIFIED_SOURCES += [ + "nsNativeAppSupportDefault.cpp", + ] + +if CONFIG["MOZ_HAS_REMOTE"]: + LOCAL_INCLUDES += [ + "../components/remote", + ] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + UNIFIED_SOURCES += [ + "nsGDKErrorHandler.cpp", + ] + +if CONFIG["MOZ_X11"]: + EXPORTS += ["nsX11ErrorHandler.h"] + UNIFIED_SOURCES += [ + "nsX11ErrorHandler.cpp", + ] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "android": + UNIFIED_SOURCES += [ + "nsAndroidStartup.cpp", + ] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] != "android": + UNIFIED_SOURCES += [ + "MultiInstanceLock.cpp", + ] + +UNIFIED_SOURCES += [ + "/toolkit/mozapps/update/common/commonupdatedir.cpp", + "AutoSQLiteLifetime.cpp", + "Bootstrap.cpp", + "CmdLineAndEnvUtils.cpp", + "CreateAppData.cpp", + "nsConsoleWriter.cpp", + "nsNativeAppSupportBase.cpp", + "nsSigHandlers.cpp", + "nsXREDirProvider.cpp", +] + +# nsAppRunner.cpp and ProfileReset.cpp cannot be built in unified mode because +# they pull in OS X system headers. +# nsEmbedFunctions.cpp cannot be built in unified mode because it pulls in X11 headers. +SOURCES += [ + "../../other-licenses/nsis/Contrib/CityHash/cityhash/city.cpp", + "nsAppRunner.cpp", + "nsEmbedFunctions.cpp", + "ProfileReset.cpp", +] + +if CONFIG["MOZ_INSTRUMENT_EVENT_LOOP"]: + UNIFIED_SOURCES += [ + "EventTracer.cpp", + ] + +if CONFIG["MOZ_UPDATER"]: + if CONFIG["MOZ_WIDGET_TOOLKIT"] != "android": + UNIFIED_SOURCES += [ + "nsUpdateDriver.cpp", + "nsUpdateSyncManager.cpp", + ] + +if CONFIG["MOZ_PDF_PRINTING"]: + DEFINES["PROXY_PRINTING"] = 1 + LOCAL_INCLUDES += [ + "../components/printingui", + ] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +if CONFIG["MOZ_X11"] or CONFIG["MOZ_WAYLAND"]: + DEFINES["USE_GLX_TEST"] = True + DIRS += ["glxtest"] + +for var in ( + "MOZ_APP_NAME", + "MOZ_APP_BASENAME", + "MOZ_APP_DISPLAYNAME", + "MOZ_APP_VENDOR", + "MOZ_APP_VERSION", + "OS_TARGET", + "MOZ_WIDGET_TOOLKIT", +): + DEFINES[var] = '"%s"' % CONFIG[var] + +if CONFIG["MOZ_DEFAULT_BROWSER_AGENT"] and CONFIG["OS_ARCH"] == "WINNT": + DEFINES["MOZ_DEFAULT_BROWSER_AGENT"] = CONFIG["MOZ_DEFAULT_BROWSER_AGENT"] + +if CONFIG["MOZ_UPDATER"] and CONFIG["MOZ_WIDGET_TOOLKIT"] != "android": + DEFINES["MOZ_UPDATER"] = True + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows": + DEFINES["WIN32_LEAN_AND_MEAN"] = True + DEFINES["UNICODE"] = True + DEFINES["_UNICODE"] = True + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "android": + DEFINES["ANDROID_PACKAGE_NAME"] = '"%s"' % CONFIG["ANDROID_PACKAGE_NAME"] + +DEFINES["MOZ_DISTRIBUTION_ID"] = '"%s"' % CONFIG["MOZ_DISTRIBUTION_ID"] + +if CONFIG["TARGET_XPCOM_ABI"]: + DEFINES["TARGET_OS_ABI"] = '"%s_%s"' % ( + CONFIG["OS_TARGET"], + CONFIG["TARGET_XPCOM_ABI"], + ) + +if CONFIG["OS_ARCH"] == "Linux" and "lib64" in CONFIG["libdir"]: + DEFINES["HAVE_USR_LIB64_DIR"] = True + +DEFINES["GRE_MILESTONE"] = CONFIG["GRE_MILESTONE"] +DEFINES["MOZ_APP_VERSION_DISPLAY"] = CONFIG["MOZ_APP_VERSION_DISPLAY"] + +for var in ("APP_VERSION", "APP_ID"): + DEFINES[var] = CONFIG["MOZ_%s" % var] + +if CONFIG["MOZ_BUILD_APP"] == "browser": + DEFINES["MOZ_BUILD_APP_IS_BROWSER"] = True + +LOCAL_INCLUDES += [ + "../../other-licenses/nsis/Contrib/CityHash/cityhash", + "../components/find", + "../components/printingui/ipc", + "../components/windowwatcher", + "../mozapps/update/common", + "../profile", + "/config", + "/dom/base", + "/dom/commandhandler", + "/dom/ipc", + "/dom/webbrowserpersist", + "/testing/gtest/mozilla", + "/toolkit/crashreporter", + "/xpcom/build", +] + +if CONFIG["MOZ_SANDBOX"] and CONFIG["OS_ARCH"] == "WINNT": + LOCAL_INCLUDES += [ + "/security/sandbox/chromium", + "/security/sandbox/chromium-shim", + ] + +if CONFIG["MOZ_SANDBOX"] and CONFIG["OS_ARCH"] == "Linux": + USE_LIBS += [ + "mozsandbox", + ] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + LOCAL_INCLUDES += [ + "/widget", + "/widget/cocoa", + "/xpcom/base", + ] + +CXXFLAGS += CONFIG["MOZ_DBUS_CFLAGS"] +CXXFLAGS += CONFIG["MOZ_DBUS_GLIB_CFLAGS"] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + CXXFLAGS += CONFIG["MOZ_PANGO_CFLAGS"] + +DEFINES["TOPOBJDIR"] = TOPOBJDIR +FINAL_TARGET_PP_FILES += ["platform.ini"] + +if CONFIG["ENABLE_TESTS"]: + DIRS += ["test/gtest"] diff --git a/toolkit/xre/nsAndroidStartup.cpp b/toolkit/xre/nsAndroidStartup.cpp new file mode 100644 index 0000000000..d581dc47a4 --- /dev/null +++ b/toolkit/xre/nsAndroidStartup.cpp @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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 <android/log.h> + +#include <jni.h> + +#include <stdlib.h> +#include <string.h> +#include <pthread.h> + +#include "mozilla/jni/Utils.h" +#include "nsTArray.h" +#include "nsString.h" +#include "nsAppRunner.h" +#include "mozilla/Bootstrap.h" +#include "XREShellData.h" + +#define LOG(args...) __android_log_print(ANDROID_LOG_INFO, MOZ_APP_NAME, args) + +using namespace mozilla; + +extern "C" NS_EXPORT void GeckoStart(JNIEnv* env, char** argv, int argc, + const StaticXREAppData& aAppData, + bool xpcshell, const char* outFilePath) { + mozilla::jni::SetGeckoThreadEnv(env); + + if (!argv) { + LOG("Failed to get arguments for GeckoStart\n"); + return; + } + + if (xpcshell) { + XREShellData shellData; + FILE* outFile = fopen(outFilePath, "w"); + if (!outFile) { + LOG("XRE_XPCShellMain cannot open %s", outFilePath); + return; + } + // We redirect both stdout and stderr to the same file, to conform with + // what runxpcshell.py does on Desktop. + shellData.outFile = outFile; + shellData.errFile = outFile; + int result = XRE_XPCShellMain(argc, argv, nullptr, &shellData); + fclose(shellData.outFile); + if (result) LOG("XRE_XPCShellMain returned %d", result); + } else { + BootstrapConfig config; + config.appData = &aAppData; + config.appDataPath = nullptr; + + int result = XRE_main(argc, argv, config); + if (result) LOG("XRE_main returned %d", result); + } +} diff --git a/toolkit/xre/nsAppRunner.cpp b/toolkit/xre/nsAppRunner.cpp new file mode 100644 index 0000000000..14b8c98dc8 --- /dev/null +++ b/toolkit/xre/nsAppRunner.cpp @@ -0,0 +1,6153 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "mozilla/dom/ContentParent.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/ipc/GeckoChildProcessHost.h" + +#include "mozilla/AppShutdown.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/Components.h" +#include "mozilla/FilePreferences.h" +#include "mozilla/ChaosMode.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/IOInterposer.h" +#include "mozilla/ipc/UtilityProcessChild.h" +#include "mozilla/Likely.h" +#include "mozilla/MemoryChecking.h" +#include "mozilla/Poison.h" +#include "mozilla/Preferences.h" +#include "mozilla/Printf.h" +#include "mozilla/ProcessType.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/RuntimeExceptionModule.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_fission.h" +#include "mozilla/StaticPrefs_webgl.h" +#include "mozilla/StaticPrefs_widget.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Utf8.h" +#include "mozilla/intl/LocaleService.h" +#include "mozilla/JSONWriter.h" +#include "mozilla/gfx/gfxVars.h" +#include "mozilla/glean/GleanPings.h" +#include "mozilla/widget/TextRecognition.h" +#include "BaseProfiler.h" + +#include "nsAppRunner.h" +#include "mozilla/XREAppData.h" +#include "mozilla/Bootstrap.h" +#if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID) +# include "nsUpdateDriver.h" +# include "nsUpdateSyncManager.h" +#endif +#include "ProfileReset.h" + +#ifdef MOZ_INSTRUMENT_EVENT_LOOP +# include "EventTracer.h" +#endif + +#ifdef XP_MACOSX +# include "nsVersionComparator.h" +# include "MacLaunchHelper.h" +# include "MacApplicationDelegate.h" +# include "MacAutoreleasePool.h" +# include "MacRunFromDmgUtils.h" +// these are needed for sysctl +# include <sys/types.h> +# include <sys/sysctl.h> +#endif + +#include "prnetdb.h" +#include "prprf.h" +#include "prproces.h" +#include "prenv.h" +#include "prtime.h" + +#include "nsIAppStartup.h" +#include "nsCategoryManagerUtils.h" +#include "nsIMutableArray.h" +#include "nsCommandLine.h" +#include "nsIComponentRegistrar.h" +#include "nsIDialogParamBlock.h" +#include "mozilla/ModuleUtils.h" +#include "nsIIOService.h" +#include "nsIObserverService.h" +#include "nsINativeAppSupport.h" +#include "nsIPlatformInfo.h" +#include "nsIProcess.h" +#include "nsIProfileUnlocker.h" +#include "nsIPromptService.h" +#include "nsIPropertyBag2.h" +#include "nsIServiceManager.h" +#include "nsIStringBundle.h" +#include "nsISupportsPrimitives.h" +#include "nsIToolkitProfile.h" +#include "nsToolkitProfileService.h" +#include "nsIURI.h" +#include "nsIURL.h" +#include "nsIWindowCreator.h" +#include "nsIWindowWatcher.h" +#include "nsIXULAppInfo.h" +#include "nsIXULRuntime.h" +#include "nsPIDOMWindow.h" +#include "nsIWidget.h" +#include "nsAppShellCID.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/scache/StartupCache.h" +#include "gfxPlatform.h" +#include "PDMFactory.h" +#ifdef XP_MACOSX +# include "gfxPlatformMac.h" +#endif + +#include "mozilla/Unused.h" + +#ifdef XP_WIN +# include "nsIWinAppHelper.h" +# include <windows.h> +# include <intrin.h> +# include <math.h> +# include "cairo/cairo-features.h" +# include "detect_win32k_conflicts.h" +# include "mozilla/PreXULSkeletonUI.h" +# include "mozilla/DllPrefetchExperimentRegistryInfo.h" +# include "mozilla/WindowsBCryptInitialization.h" +# include "mozilla/WindowsDllBlocklist.h" +# include "mozilla/WindowsProcessMitigations.h" +# include "mozilla/WinHeaderOnlyUtils.h" +# include "mozilla/mscom/ProcessRuntime.h" +# include "mozilla/mscom/ProfilerMarkers.h" +# include "WinTokenUtils.h" + +# if defined(MOZ_LAUNCHER_PROCESS) +# include "mozilla/LauncherRegistryInfo.h" +# endif + +# if defined(MOZ_DEFAULT_BROWSER_AGENT) +# include "nsIWindowsRegKey.h" +# endif + +# ifndef PROCESS_DEP_ENABLE +# define PROCESS_DEP_ENABLE 0x1 +# endif +#endif + +#if defined(MOZ_SANDBOX) +# include "mozilla/SandboxSettings.h" +#endif + +#ifdef ACCESSIBILITY +# include "nsAccessibilityService.h" +# if defined(XP_WIN) +# include "mozilla/a11y/Compatibility.h" +# include "mozilla/a11y/Platform.h" +# endif +#endif + +#include "nsCRT.h" +#include "nsCOMPtr.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsEmbedCID.h" +#include "nsIDUtils.h" +#include "nsNetUtil.h" +#include "nsReadableUtils.h" +#include "nsXPCOM.h" +#include "nsXPCOMCIDInternal.h" +#include "nsString.h" +#include "nsPrintfCString.h" +#include "nsVersionComparator.h" + +#include "nsAppDirectoryServiceDefs.h" +#include "nsXULAppAPI.h" +#include "nsXREDirProvider.h" + +#include "nsINIParser.h" +#include "mozilla/Omnijar.h" +#include "mozilla/StartupTimeline.h" +#include "mozilla/LateWriteChecks.h" + +#include <stdlib.h> +#include <locale.h> + +#ifdef XP_UNIX +# include <errno.h> +# include <pwd.h> +# include <string.h> +# include <sys/resource.h> +# include <sys/stat.h> +# include <unistd.h> +#endif + +#ifdef XP_WIN +# include <process.h> +# include <shlobj.h> +# include "mozilla/WinDllServices.h" +# include "nsThreadUtils.h" +# include "WinUtils.h" +#endif + +#ifdef XP_MACOSX +# include "nsILocalFileMac.h" +# include "nsCommandLineServiceMac.h" +#endif + +// for X remote support +#if defined(MOZ_HAS_REMOTE) +# include "nsRemoteService.h" +#endif + +#if defined(DEBUG) && defined(XP_WIN) +# include <malloc.h> +#endif + +#if defined(XP_MACOSX) +# include <Carbon/Carbon.h> +#endif + +#ifdef DEBUG +# include "mozilla/Logging.h" +#endif + +#ifdef MOZ_JPROF +# include "jprof.h" +#endif + +#include "nsExceptionHandler.h" +#include "nsICrashReporter.h" +#include "nsIPrefService.h" +#include "nsIMemoryInfoDumper.h" +#if defined(XP_LINUX) && !defined(ANDROID) +# include "mozilla/widget/LSBUtils.h" +#endif + +#include "base/command_line.h" +#include "GTestRunner.h" + +#ifdef MOZ_WIDGET_ANDROID +# include "mozilla/java/GeckoAppShellWrappers.h" +#endif + +#if defined(MOZ_SANDBOX) +# if defined(XP_LINUX) && !defined(ANDROID) +# include "mozilla/SandboxInfo.h" +# elif defined(XP_WIN) +# include "sandboxBroker.h" +# endif +#endif + +#ifdef MOZ_CODE_COVERAGE +# include "mozilla/CodeCoverageHandler.h" +#endif + +#include "SafeMode.h" + +#ifdef MOZ_BACKGROUNDTASKS +# include "mozilla/BackgroundTasks.h" +# include "nsIPowerManagerService.h" +# include "nsIStringBundle.h" +#endif + +#ifdef USE_GLX_TEST +# include "mozilla/GUniquePtr.h" +# include "mozilla/GfxInfo.h" +#endif + +extern uint32_t gRestartMode; +extern void InstallSignalHandlers(const char* ProgramName); + +#define FILE_COMPATIBILITY_INFO "compatibility.ini"_ns +#define FILE_INVALIDATE_CACHES ".purgecaches"_ns +#define FILE_STARTUP_INCOMPLETE u".startup-incomplete"_ns + +#if defined(MOZ_BLOCK_PROFILE_DOWNGRADE) || defined(MOZ_LAUNCHER_PROCESS) || \ + defined(MOZ_DEFAULT_BROWSER_AGENT) +static const char kPrefHealthReportUploadEnabled[] = + "datareporting.healthreport.uploadEnabled"; +#endif // defined(MOZ_BLOCK_PROFILE_DOWNGRADE) || defined(MOZ_LAUNCHER_PROCESS) + // || defined(MOZ_DEFAULT_BROWSER_AGENT) +#if defined(MOZ_DEFAULT_BROWSER_AGENT) +static const char kPrefDefaultAgentEnabled[] = "default-browser-agent.enabled"; + +static const char kPrefServicesSettingsServer[] = "services.settings.server"; +static const char kPrefSecurityContentSignatureRootHash[] = + "security.content.signature.root_hash"; +static const char kPrefSetDefaultBrowserUserChoicePref[] = + "browser.shell.setDefaultBrowserUserChoice"; +#endif // defined(MOZ_DEFAULT_BROWSER_AGENT) + +#if defined(XP_WIN) +static const char kPrefThemeId[] = "extensions.activeThemeID"; +static const char kPrefBrowserStartupBlankWindow[] = + "browser.startup.blankWindow"; +static const char kPrefPreXulSkeletonUI[] = "browser.startup.preXulSkeletonUI"; +#endif // defined(XP_WIN) + +#if defined(MOZ_WIDGET_GTK) +constexpr nsLiteralCString kStartupTokenNames[] = { + "XDG_ACTIVATION_TOKEN"_ns, + "DESKTOP_STARTUP_ID"_ns, +}; +#endif + +int gArgc; +char** gArgv; + +static const char gToolkitVersion[] = MOZ_STRINGIFY(GRE_MILESTONE); +// The gToolkitBuildID global is defined to MOZ_BUILDID via gen_buildid.py +// in toolkit/library. See related comment in toolkit/library/moz.build. +extern const char gToolkitBuildID[]; + +static nsIProfileLock* gProfileLock; +#if defined(MOZ_HAS_REMOTE) +static nsRemoteService* gRemoteService; +bool gRestartWithoutRemote = false; +#endif + +int gRestartArgc; +char** gRestartArgv; + +// If gRestartedByOS is set, we were automatically restarted by the OS. +bool gRestartedByOS = false; + +bool gIsGtest = false; + +nsString gAbsoluteArgv0Path; + +#if defined(XP_WIN) +nsString gProcessStartupShortcut; +#endif + +#if defined(MOZ_WIDGET_GTK) +# include <glib.h> +# include "mozilla/WidgetUtilsGtk.h" +# include <gtk/gtk.h> +# ifdef MOZ_WAYLAND +# include <gdk/gdkwayland.h> +# include "mozilla/widget/nsWaylandDisplay.h" +# endif +# ifdef MOZ_X11 +# include <gdk/gdkx.h> +# endif /* MOZ_X11 */ +#endif +#include "BinaryPath.h" + +#ifdef MOZ_LINKER +extern "C" MFBT_API bool IsSignalHandlingBroken(); +#endif + +#ifdef FUZZING +# include "FuzzerRunner.h" + +namespace mozilla { +FuzzerRunner* fuzzerRunner = 0; +} // namespace mozilla + +# ifdef LIBFUZZER +void XRE_LibFuzzerSetDriver(LibFuzzerDriver aDriver) { + mozilla::fuzzerRunner->setParams(aDriver); +} +# endif +#endif // FUZZING + +// Undo X11/X.h's definition of None +#undef None + +namespace mozilla { +int (*RunGTest)(int*, char**) = 0; + +bool RunningGTest() { return RunGTest; } +} // namespace mozilla + +using namespace mozilla; +using namespace mozilla::widget; +using namespace mozilla::startup; +using mozilla::Unused; +using mozilla::dom::ContentChild; +using mozilla::dom::ContentParent; +using mozilla::dom::quota::QuotaManager; +using mozilla::intl::LocaleService; +using mozilla::scache::StartupCache; + +// Save the given word to the specified environment variable. +static void MOZ_NEVER_INLINE SaveWordToEnv(const char* name, + const nsACString& word) { + char* expr = + Smprintf("%s=%s", name, PromiseFlatCString(word).get()).release(); + if (expr) PR_SetEnv(expr); + // We intentionally leak |expr| here since it is required by PR_SetEnv. +} + +// Save the path of the given file to the specified environment variable. +static void SaveFileToEnv(const char* name, nsIFile* file) { +#ifdef XP_WIN + nsAutoString path; + file->GetPath(path); + SetEnvironmentVariableW(NS_ConvertASCIItoUTF16(name).get(), path.get()); +#else + nsAutoCString path; + file->GetNativePath(path); + SaveWordToEnv(name, path); +#endif +} + +static bool gIsExpectedExit = false; + +void MozExpectedExit() { gIsExpectedExit = true; } + +/** + * Runs atexit() to catch unexpected exit from 3rd party libraries like the + * Intel graphics driver calling exit in an error condition. When they + * call exit() to report an error we won't shutdown correctly and wont catch + * the issue with our crash reporter. + */ +static void UnexpectedExit() { + if (!gIsExpectedExit) { + gIsExpectedExit = true; // Don't risk re-entrency issues when crashing. + MOZ_CRASH("Exit called by third party code."); + } +} + +#if defined(MOZ_WAYLAND) +bool IsWaylandEnabled() { + static bool isWaylandEnabled = []() { + const char* waylandDisplay = PR_GetEnv("WAYLAND_DISPLAY"); + if (!waylandDisplay) { + return false; + } + if (!PR_GetEnv("DISPLAY")) { + // No X11 display, so try to run wayland. + return true; + } + // MOZ_ENABLE_WAYLAND is our primary Wayland on/off switch. + if (const char* waylandPref = PR_GetEnv("MOZ_ENABLE_WAYLAND")) { + return *waylandPref == '1'; + } + if (const char* backendPref = PR_GetEnv("GDK_BACKEND")) { + if (!strncmp(backendPref, "wayland", 7)) { + NS_WARNING( + "Wayland backend should be enabled by MOZ_ENABLE_WAYLAND=1." + "GDK_BACKEND is a Gtk3 debug variable and may cause issues."); + return true; + } + } +# ifdef EARLY_BETA_OR_EARLIER + // Enable by default when we're running on a recent enough GTK version. We'd + // like to check further details like compositor version and so on ideally + // to make sure we don't enable it on old Mutter or what not, but we can't, + // so let's assume that if the user is running on a Wayland session by + // default we're ok, since either the distro has enabled Wayland by default, + // or the user has gone out of their way to use Wayland. + // + // TODO(emilio): If users hit problems, we might be able to restrict it to + // GNOME / KDE / known-good-desktop environments by checking + // XDG_CURRENT_DESKTOP or so... + return !gtk_check_version(3, 24, 30); +# else + return false; +# endif + }(); + return isWaylandEnabled; +} +#else +bool IsWaylandEnabled() { return false; } +#endif + +/** + * Output a string to the user. This method is really only meant to be used to + * output last-ditch error messages designed for developers NOT END USERS. + * + * @param isError + * Pass true to indicate severe errors. + * @param fmt + * printf-style format string followed by arguments. + */ +static MOZ_FORMAT_PRINTF(2, 3) void Output(bool isError, const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + +#if defined(XP_WIN) && !MOZ_WINCONSOLE + SmprintfPointer msg = mozilla::Vsmprintf(fmt, ap); + if (msg) { + UINT flags = MB_OK; + if (isError) + flags |= MB_ICONERROR; + else + flags |= MB_ICONINFORMATION; + + wchar_t wide_msg[1024]; + MultiByteToWideChar(CP_ACP, 0, msg.get(), -1, wide_msg, + sizeof(wide_msg) / sizeof(wchar_t)); + + MessageBoxW(nullptr, wide_msg, L"XULRunner", flags); + } +#elif defined(MOZ_WIDGET_ANDROID) + SmprintfPointer msg = mozilla::Vsmprintf(fmt, ap); + if (msg) { + __android_log_print(isError ? ANDROID_LOG_ERROR : ANDROID_LOG_INFO, + "GeckoRuntime", "%s", msg.get()); + } +#else + vfprintf(stderr, fmt, ap); +#endif + + va_end(ap); +} + +/** + * Check for a commandline flag. If the flag takes a parameter, the + * parameter is returned in aParam. Flags may be in the form -arg or + * --arg (or /arg on win32). + * + * @param aArg the parameter to check. Must be lowercase. + * @param aParam if non-null, the -arg <data> will be stored in this pointer. + * This is *not* allocated, but rather a pointer to the argv data. + * @param aFlags flags @see CheckArgFlag + */ +static ArgResult CheckArg(const char* aArg, const char** aParam = nullptr, + CheckArgFlag aFlags = CheckArgFlag::RemoveArg) { + MOZ_ASSERT(gArgv, "gArgv must be initialized before CheckArg()"); + return CheckArg(gArgc, gArgv, aArg, aParam, aFlags); +} + +/** + * Check for a commandline flag. Ignore data that's passed in with the flag. + * Flags may be in the form -arg or --arg (or /arg on win32). + * Will not remove flag if found. + * + * @param aArg the parameter to check. Must be lowercase. + */ +static ArgResult CheckArgExists(const char* aArg) { + return CheckArg(aArg, nullptr, CheckArgFlag::None); +} + +bool gSafeMode = false; +bool gFxREmbedded = false; + +enum E10sStatus { + kE10sEnabledByDefault, + kE10sDisabledByUser, + kE10sForceDisabled, +}; + +static bool gBrowserTabsRemoteAutostart = false; +static E10sStatus gBrowserTabsRemoteStatus; +static bool gBrowserTabsRemoteAutostartInitialized = false; + +namespace mozilla { + +bool BrowserTabsRemoteAutostart() { + if (gBrowserTabsRemoteAutostartInitialized) { + return gBrowserTabsRemoteAutostart; + } + gBrowserTabsRemoteAutostartInitialized = true; + + // If we're not in the parent process, we are running E10s. + if (!XRE_IsParentProcess()) { + gBrowserTabsRemoteAutostart = true; + return gBrowserTabsRemoteAutostart; + } + +#if defined(MOZILLA_OFFICIAL) && MOZ_BUILD_APP_IS_BROWSER + bool allowSingleProcessOutsideAutomation = false; +#else + bool allowSingleProcessOutsideAutomation = true; +#endif + + E10sStatus status = kE10sEnabledByDefault; + // We use "are non-local connections disabled" as a proxy for + // "are we running some kind of automated test". It would be nicer to use + // xpc::IsInAutomation(), but that depends on some prefs being set, which + // they are not in (at least) gtests (where we can't) and xpcshell. + // Long-term, hopefully we can make tests switch to environment variables + // to disable e10s and then we can get rid of this. + if (allowSingleProcessOutsideAutomation || + xpc::AreNonLocalConnectionsDisabled()) { + bool optInPref = + Preferences::GetBool("browser.tabs.remote.autostart", true); + + if (optInPref) { + gBrowserTabsRemoteAutostart = true; + } else { + status = kE10sDisabledByUser; + } + } else { + gBrowserTabsRemoteAutostart = true; + } + + // Uber override pref for emergency blocking + if (gBrowserTabsRemoteAutostart) { + const char* forceDisable = PR_GetEnv("MOZ_FORCE_DISABLE_E10S"); +#if defined(MOZ_WIDGET_ANDROID) + // We need this for xpcshell on Android + if (forceDisable && *forceDisable) { +#else + // The environment variable must match the application version to apply. + if (forceDisable && gAppData && !strcmp(forceDisable, gAppData->version)) { +#endif + gBrowserTabsRemoteAutostart = false; + status = kE10sForceDisabled; + } + } + + gBrowserTabsRemoteStatus = status; + + return gBrowserTabsRemoteAutostart; +} + +} // namespace mozilla + +// Win32k Infrastructure ============================================== + +// This bool tells us if we have initialized the following two statics +// upon startup. + +static bool gWin32kInitialized = false; + +// Win32k Lockdown for the current session is determined once, at startup, +// and then remains the same for the duration of the session. + +static nsIXULRuntime::ContentWin32kLockdownState gWin32kStatus; + +// The status of the win32k experiment. It is determined once, at startup, +// and then remains the same for the duration of the session. + +static nsIXULRuntime::ExperimentStatus gWin32kExperimentStatus = + nsIXULRuntime::eExperimentStatusUnenrolled; + +#ifdef XP_WIN +// The states for win32k lockdown can be generalized to: +// - User doesn't meet requirements (bad OS, webrender, remotegl) aka +// PersistentRequirement +// - User has Safe Mode enabled, Win32k Disabled by Env Var, or E10S disabled +// by Env Var aka TemporaryRequirement +// - User has set the win32k pref to something non-default aka NonDefaultPref +// - User has been enrolled in the experiment and does what it says aka +// Enrolled +// - User does the default aka Default +// +// We expect the below behvaior. In the code, there may be references to these +// behaviors (e.g. [A]) but not always. +// +// [A] Becoming enrolled in the experiment while NonDefaultPref disqualifies +// you upon next browser start +// [B] Becoming enrolled in the experiment while PersistentRequirement +// disqualifies you upon next browser start +// [C] Becoming enrolled in the experiment while TemporaryRequirement does not +// disqualify you +// [D] Becoming enrolled in the experiment will only take effect after restart +// [E] While enrolled, becoming PersistentRequirement will not disqualify you +// until browser restart, but if the state is present at browser start, +// will disqualify you. Additionally, it will not affect the session value +// of gWin32kStatus. +// XXX(bobowen): I hope both webrender and wbgl.out-of-process require a +// restart to take effect! +// [F] While enrolled, becoming NonDefaultPref will disqualify you upon next +// browser start +// [G] While enrolled, becoming TemporaryRequirement will _not_ disqualify you, +// but the session status will prevent win32k lockdown from taking effect. + +// The win32k pref +static const char kPrefWin32k[] = "security.sandbox.content.win32k-disable"; + +// The current enrollment status as controlled by Normandy. This value is only +// stored in the default preference branch, and is not persisted across +// sessions by the preference service. It therefore isn't available early +// enough at startup, and needs to be synced to a preference in the user +// branch which is persisted across sessions. +static const char kPrefWin32kExperimentEnrollmentStatus[] = + "security.sandbox.content.win32k-experiment.enrollmentStatus"; + +// The enrollment status to be used at browser startup. This automatically +// synced from the above enrollmentStatus preference whenever the latter is +// changed. We reused the Fission experiment enum - it can have any of the +// values defined in the `nsIXULRuntime_ExperimentStatus` enum _except_ rollout. +// Meanings are documented in the declaration of +// `nsIXULRuntime.fissionExperimentStatus` +static const char kPrefWin32kExperimentStartupEnrollmentStatus[] = + "security.sandbox.content.win32k-experiment.startupEnrollmentStatus"; + +namespace mozilla { + +bool Win32kExperimentEnrolled() { + MOZ_ASSERT(XRE_IsParentProcess()); + return gWin32kExperimentStatus == nsIXULRuntime::eExperimentStatusControl || + gWin32kExperimentStatus == nsIXULRuntime::eExperimentStatusTreatment; +} + +} // namespace mozilla + +static bool Win32kRequirementsUnsatisfied( + nsIXULRuntime::ContentWin32kLockdownState aStatus) { + return aStatus == nsIXULRuntime::ContentWin32kLockdownState:: + OperatingSystemNotSupported || + aStatus == + nsIXULRuntime::ContentWin32kLockdownState::MissingWebRender || + aStatus == + nsIXULRuntime::ContentWin32kLockdownState::MissingRemoteWebGL || + aStatus == + nsIXULRuntime::ContentWin32kLockdownState::DecodersArentRemote; +} + +static void Win32kExperimentDisqualify() { + MOZ_ASSERT(XRE_IsParentProcess()); + Preferences::SetUint(kPrefWin32kExperimentEnrollmentStatus, + nsIXULRuntime::eExperimentStatusDisqualified); +} + +static void OnWin32kEnrollmentStatusChanged(const char* aPref, void* aData) { + auto newStatusInt = + Preferences::GetUint(kPrefWin32kExperimentEnrollmentStatus, + nsIXULRuntime::eExperimentStatusUnenrolled); + auto newStatus = newStatusInt < nsIXULRuntime::eExperimentStatusCount + ? nsIXULRuntime::ExperimentStatus(newStatusInt) + : nsIXULRuntime::eExperimentStatusDisqualified; + + // Set the startup pref for next browser start [D] + Preferences::SetUint(kPrefWin32kExperimentStartupEnrollmentStatus, newStatus); +} + +namespace { +// This observer is notified during `profile-before-change`, and ensures that +// the experiment enrollment status is synced over before the browser shuts +// down, even if it was not modified since win32k was initialized. +class Win32kEnrollmentStatusShutdownObserver final : public nsIObserver { + public: + NS_DECL_ISUPPORTS + + NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) override { + MOZ_ASSERT(!strcmp("profile-before-change", aTopic)); + OnWin32kEnrollmentStatusChanged(kPrefWin32kExperimentEnrollmentStatus, + nullptr); + return NS_OK; + } + + private: + ~Win32kEnrollmentStatusShutdownObserver() = default; +}; +NS_IMPL_ISUPPORTS(Win32kEnrollmentStatusShutdownObserver, nsIObserver) +} // namespace + +static void OnWin32kChanged(const char* aPref, void* aData) { + // If we're actively enrolled in the Win32k experiment, disqualify the user + // from the experiment if the Win32k pref is modified. [F] + if (Win32kExperimentEnrolled() && Preferences::HasUserValue(kPrefWin32k)) { + Win32kExperimentDisqualify(); + } +} + +#endif // XP_WIN + +namespace mozilla { +void EnsureWin32kInitialized(); +} + +nsIXULRuntime::ContentWin32kLockdownState GetLiveWin32kLockdownState() { +#ifdef XP_WIN + + // HasUserValue The Pref functions can only be called on main thread + MOZ_ASSERT(NS_IsMainThread()); + +# ifdef MOZ_BACKGROUNDTASKS + if (BackgroundTasks::IsBackgroundTaskMode()) { + // Let's bail out before loading all the graphics libs. + return nsIXULRuntime::ContentWin32kLockdownState::DisabledByDefault; + } +# endif + + mozilla::EnsureWin32kInitialized(); + gfxPlatform::GetPlatform(); + + if (gSafeMode) { + return nsIXULRuntime::ContentWin32kLockdownState::DisabledBySafeMode; + } + + if (EnvHasValue("MOZ_ENABLE_WIN32K")) { + return nsIXULRuntime::ContentWin32kLockdownState::DisabledByEnvVar; + } + + if (!mozilla::BrowserTabsRemoteAutostart()) { + return nsIXULRuntime::ContentWin32kLockdownState::DisabledByE10S; + } + + // Win32k lockdown is available on Win8+, but we are initially limiting it to + // Windows 10 v1709 (build 16299) or later. Before this COM initialization + // currently fails if user32.dll has loaded before it is called. + if (!IsWin10FallCreatorsUpdateOrLater()) { + return nsIXULRuntime::ContentWin32kLockdownState:: + OperatingSystemNotSupported; + } + + { + ConflictingMitigationStatus conflictingMitigationStatus = {}; + if (!detect_win32k_conflicting_mitigations(&conflictingMitigationStatus)) { + return nsIXULRuntime::ContentWin32kLockdownState:: + IncompatibleMitigationPolicy; + } + if (conflictingMitigationStatus.caller_check || + conflictingMitigationStatus.sim_exec || + conflictingMitigationStatus.stack_pivot) { + return nsIXULRuntime::ContentWin32kLockdownState:: + IncompatibleMitigationPolicy; + } + } + + // Non-native theming is required as well + if (!StaticPrefs::widget_non_native_theme_enabled()) { + return nsIXULRuntime::ContentWin32kLockdownState::MissingNonNativeTheming; + } + + // Win32k Lockdown requires Remote WebGL, but it may be disabled on + // certain hardware or virtual machines. + if (!gfx::gfxVars::AllowWebglOop() || !StaticPrefs::webgl_out_of_process()) { + return nsIXULRuntime::ContentWin32kLockdownState::MissingRemoteWebGL; + } + + // Some (not sure exactly which) decoders are not compatible + if (!PDMFactory::AllDecodersAreRemote()) { + return nsIXULRuntime::ContentWin32kLockdownState::DecodersArentRemote; + } + + bool prefSetByUser = + Preferences::HasUserValue("security.sandbox.content.win32k-disable"); + bool prefValue = Preferences::GetBool( + "security.sandbox.content.win32k-disable", false, PrefValueKind::User); + bool defaultValue = Preferences::GetBool( + "security.sandbox.content.win32k-disable", false, PrefValueKind::Default); + + if (prefSetByUser) { + if (prefValue) { + return nsIXULRuntime::ContentWin32kLockdownState::EnabledByUserPref; + } else { + return nsIXULRuntime::ContentWin32kLockdownState::DisabledByUserPref; + } + } + + if (gWin32kExperimentStatus == + nsIXULRuntime::ExperimentStatus::eExperimentStatusControl) { + return nsIXULRuntime::ContentWin32kLockdownState::DisabledByControlGroup; + } else if (gWin32kExperimentStatus == + nsIXULRuntime::ExperimentStatus::eExperimentStatusTreatment) { + return nsIXULRuntime::ContentWin32kLockdownState::EnabledByTreatmentGroup; + } + + if (defaultValue) { + return nsIXULRuntime::ContentWin32kLockdownState::EnabledByDefault; + } else { + return nsIXULRuntime::ContentWin32kLockdownState::DisabledByDefault; + } + +#else + + return nsIXULRuntime::ContentWin32kLockdownState::OperatingSystemNotSupported; + +#endif +} + +namespace mozilla { + +void EnsureWin32kInitialized() { + if (gWin32kInitialized) { + return; + } + gWin32kInitialized = true; + +#ifdef XP_WIN + + // Initialize the Win32k experiment, configuring win32k's + // default, before checking other overrides. This allows opting-out of a + // Win32k experiment through about:preferences or about:config from a + // safemode session. + uint32_t experimentRaw = + Preferences::GetUint(kPrefWin32kExperimentStartupEnrollmentStatus, + nsIXULRuntime::eExperimentStatusUnenrolled); + gWin32kExperimentStatus = + experimentRaw < nsIXULRuntime::eExperimentStatusCount + ? nsIXULRuntime::ExperimentStatus(experimentRaw) + : nsIXULRuntime::eExperimentStatusDisqualified; + + // Watch the experiment enrollment status pref to detect experiment + // disqualification, and ensure it is propagated for the next restart. + Preferences::RegisterCallback(&OnWin32kEnrollmentStatusChanged, + kPrefWin32kExperimentEnrollmentStatus); + if (nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService()) { + nsCOMPtr<nsIObserver> shutdownObserver = + new Win32kEnrollmentStatusShutdownObserver(); + observerService->AddObserver(shutdownObserver, "profile-before-change", + false); + } + + // If the user no longer qualifies because they edited a required pref, check + // that. [B] [E] + auto tmpStatus = GetLiveWin32kLockdownState(); + if (Win32kExperimentEnrolled() && Win32kRequirementsUnsatisfied(tmpStatus)) { + Win32kExperimentDisqualify(); + gWin32kExperimentStatus = nsIXULRuntime::eExperimentStatusDisqualified; + } + + // If the user has overridden an active experiment by setting a user value for + // "security.sandbox.content.win32k-disable", disqualify the user from the + // experiment. [A] [F] + if (Preferences::HasUserValue(kPrefWin32k) && Win32kExperimentEnrolled()) { + Win32kExperimentDisqualify(); + gWin32kExperimentStatus = nsIXULRuntime::eExperimentStatusDisqualified; + } + + // Unlike Fission, we do not configure the default branch based on experiment + // enrollment status. Instead we check the startupEnrollmentPref in + // GetContentWin32kLockdownState() + + Preferences::RegisterCallback(&OnWin32kChanged, kPrefWin32k); + + // Set the state + gWin32kStatus = GetLiveWin32kLockdownState(); + +#else + gWin32kStatus = + nsIXULRuntime::ContentWin32kLockdownState::OperatingSystemNotSupported; + gWin32kExperimentStatus = nsIXULRuntime::eExperimentStatusUnenrolled; + +#endif // XP_WIN +} + +nsIXULRuntime::ContentWin32kLockdownState GetWin32kLockdownState() { +#ifdef XP_WIN + + mozilla::EnsureWin32kInitialized(); + return gWin32kStatus; + +#else + + return nsIXULRuntime::ContentWin32kLockdownState::OperatingSystemNotSupported; + +#endif +} + +} // namespace mozilla + +// End Win32k Infrastructure ========================================== + +// Fission Infrastructure ============================================= + +// Fission enablement for the current session is determined once, at startup, +// and then remains the same for the duration of the session. +// +// The following factors determine whether or not Fission is enabled for a +// session, in order of precedence: +// +// - Safe mode: In safe mode, Fission is never enabled. +// +// - The MOZ_FORCE_ENABLE_FISSION environment variable: If set to any value, +// Fission will be enabled. +// +// - The 'fission.autostart' preference, if it has been configured by the user. +static const char kPrefFissionAutostart[] = "fission.autostart"; + +// The computed FissionAutostart value for the session, read by content +// processes to initialize gFissionAutostart. +// +// This pref is locked, and only configured on the default branch, so should +// never be persisted in a profile. +static const char kPrefFissionAutostartSession[] = "fission.autostart.session"; + +// +// The computed FissionAutostart value for the session, read by content +// processes to initialize gFissionAutostart. + +static bool gFissionAutostart = false; +static bool gFissionAutostartInitialized = false; +static nsIXULRuntime::FissionDecisionStatus gFissionDecisionStatus; +static void EnsureFissionAutostartInitialized() { + if (gFissionAutostartInitialized) { + return; + } + gFissionAutostartInitialized = true; + + if (!XRE_IsParentProcess()) { + // This pref is configured for the current session by the parent process. + gFissionAutostart = Preferences::GetBool(kPrefFissionAutostartSession, + false, PrefValueKind::Default); + return; + } + + if (!BrowserTabsRemoteAutostart()) { + gFissionAutostart = false; + if (gBrowserTabsRemoteStatus == kE10sForceDisabled) { + gFissionDecisionStatus = nsIXULRuntime::eFissionDisabledByE10sEnv; + } else { + gFissionDecisionStatus = nsIXULRuntime::eFissionDisabledByE10sOther; + } + } else if (EnvHasValue("MOZ_FORCE_ENABLE_FISSION")) { + gFissionAutostart = true; + gFissionDecisionStatus = nsIXULRuntime::eFissionEnabledByEnv; + } else if (EnvHasValue("MOZ_FORCE_DISABLE_FISSION")) { + gFissionAutostart = false; + gFissionDecisionStatus = nsIXULRuntime::eFissionDisabledByEnv; + } else { + // NOTE: This will take into account changes to the default due to + // `InitializeFissionExperimentStatus`. + gFissionAutostart = Preferences::GetBool(kPrefFissionAutostart, false); + if (Preferences::HasUserValue(kPrefFissionAutostart)) { + gFissionDecisionStatus = gFissionAutostart + ? nsIXULRuntime::eFissionEnabledByUserPref + : nsIXULRuntime::eFissionDisabledByUserPref; + } else { + gFissionDecisionStatus = gFissionAutostart + ? nsIXULRuntime::eFissionEnabledByDefault + : nsIXULRuntime::eFissionDisabledByDefault; + } + } + + // Content processes cannot run the same logic as we're running in the parent + // process, as the current value of various preferences may have changed + // between launches. Instead, the content process will read the default branch + // of the locked `fission.autostart.session` preference to determine the value + // determined by this method. + Preferences::Unlock(kPrefFissionAutostartSession); + Preferences::ClearUser(kPrefFissionAutostartSession); + Preferences::SetBool(kPrefFissionAutostartSession, gFissionAutostart, + PrefValueKind::Default); + Preferences::Lock(kPrefFissionAutostartSession); +} + +namespace mozilla { + +bool FissionAutostart() { + EnsureFissionAutostartInitialized(); + return gFissionAutostart; +} + +} // namespace mozilla + +// End Fission Infrastructure ========================================= + +namespace mozilla { + +bool SessionHistoryInParent() { + return FissionAutostart() || + !StaticPrefs:: + fission_disableSessionHistoryInParent_AtStartup_DoNotUseDirectly(); +} + +bool BFCacheInParent() { + return SessionHistoryInParent() && + StaticPrefs::fission_bfcacheInParent_DoNotUseDirectly(); +} + +} // namespace mozilla + +/** + * The nsXULAppInfo object implements nsIFactory so that it can be its own + * singleton. + */ +class nsXULAppInfo : public nsIXULAppInfo, +#ifdef XP_WIN + public nsIWinAppHelper, +#endif + public nsICrashReporter, + public nsIFinishDumpingCallback, + public nsIXULRuntime + +{ + public: + constexpr nsXULAppInfo() = default; + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIPLATFORMINFO + NS_DECL_NSIXULAPPINFO + NS_DECL_NSIXULRUNTIME + NS_DECL_NSICRASHREPORTER + NS_DECL_NSIFINISHDUMPINGCALLBACK +#ifdef XP_WIN + NS_DECL_NSIWINAPPHELPER +#endif +}; + +NS_INTERFACE_MAP_BEGIN(nsXULAppInfo) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIXULRuntime) + NS_INTERFACE_MAP_ENTRY(nsIXULRuntime) +#ifdef XP_WIN + NS_INTERFACE_MAP_ENTRY(nsIWinAppHelper) +#endif + NS_INTERFACE_MAP_ENTRY(nsICrashReporter) + NS_INTERFACE_MAP_ENTRY(nsIFinishDumpingCallback) + NS_INTERFACE_MAP_ENTRY(nsIPlatformInfo) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIXULAppInfo, + gAppData || XRE_IsContentProcess()) +NS_INTERFACE_MAP_END + +NS_IMETHODIMP_(MozExternalRefCountType) +nsXULAppInfo::AddRef() { return 1; } + +NS_IMETHODIMP_(MozExternalRefCountType) +nsXULAppInfo::Release() { return 1; } + +NS_IMETHODIMP +nsXULAppInfo::GetVendor(nsACString& aResult) { + if (XRE_IsContentProcess()) { + ContentChild* cc = ContentChild::GetSingleton(); + aResult = cc->GetAppInfo().vendor; + return NS_OK; + } + aResult.Assign(gAppData->vendor); + + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetName(nsACString& aResult) { + if (XRE_IsContentProcess()) { + ContentChild* cc = ContentChild::GetSingleton(); + aResult = cc->GetAppInfo().name; + return NS_OK; + } + +#ifdef MOZ_WIDGET_ANDROID + nsCString name = java::GeckoAppShell::GetAppName()->ToCString(); + aResult.Assign(std::move(name)); +#else + aResult.Assign(gAppData->name); +#endif + + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetID(nsACString& aResult) { + if (XRE_IsContentProcess()) { + ContentChild* cc = ContentChild::GetSingleton(); + aResult = cc->GetAppInfo().ID; + return NS_OK; + } + aResult.Assign(gAppData->ID); + + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetVersion(nsACString& aResult) { + if (XRE_IsContentProcess()) { + ContentChild* cc = ContentChild::GetSingleton(); + aResult = cc->GetAppInfo().version; + return NS_OK; + } + aResult.Assign(gAppData->version); + + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetPlatformVersion(nsACString& aResult) { + aResult.Assign(gToolkitVersion); + + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetAppBuildID(nsACString& aResult) { + if (XRE_IsContentProcess()) { + ContentChild* cc = ContentChild::GetSingleton(); + aResult = cc->GetAppInfo().buildID; + return NS_OK; + } + aResult.Assign(gAppData->buildID); + + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetPlatformBuildID(nsACString& aResult) { + aResult.Assign(gToolkitBuildID); + + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetUAName(nsACString& aResult) { + if (XRE_IsContentProcess()) { + ContentChild* cc = ContentChild::GetSingleton(); + aResult = cc->GetAppInfo().UAName; + return NS_OK; + } + aResult.Assign(gAppData->UAName); + + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetSourceURL(nsACString& aResult) { + if (XRE_IsContentProcess()) { + ContentChild* cc = ContentChild::GetSingleton(); + aResult = cc->GetAppInfo().sourceURL; + return NS_OK; + } + aResult.Assign(gAppData->sourceURL); + + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetUpdateURL(nsACString& aResult) { + if (XRE_IsContentProcess()) { + ContentChild* cc = ContentChild::GetSingleton(); + aResult = cc->GetAppInfo().updateURL; + return NS_OK; + } + aResult.Assign(gAppData->updateURL); + + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetLogConsoleErrors(bool* aResult) { + *aResult = gLogConsoleErrors; + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::SetLogConsoleErrors(bool aValue) { + gLogConsoleErrors = aValue; + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetInSafeMode(bool* aResult) { + *aResult = gSafeMode; + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetOS(nsACString& aResult) { + aResult.AssignLiteral(OS_TARGET); + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetXPCOMABI(nsACString& aResult) { +#ifdef TARGET_XPCOM_ABI + aResult.AssignLiteral(TARGET_XPCOM_ABI); + return NS_OK; +#else + return NS_ERROR_NOT_AVAILABLE; +#endif +} + +NS_IMETHODIMP +nsXULAppInfo::GetWidgetToolkit(nsACString& aResult) { + aResult.AssignLiteral(MOZ_WIDGET_TOOLKIT); + return NS_OK; +} + +// Ensure that the GeckoProcessType enum, defined in xpcom/build/nsXULAppAPI.h, +// is synchronized with the const unsigned longs defined in +// xpcom/system/nsIXULRuntime.idl. +#define GECKO_PROCESS_TYPE(enum_value, enum_name, string_name, proc_typename, \ + process_bin_type, procinfo_typename, \ + webidl_typename, allcaps_name) \ + static_assert(nsIXULRuntime::PROCESS_TYPE_##allcaps_name == \ + static_cast<int>(GeckoProcessType_##enum_name), \ + "GeckoProcessType in nsXULAppAPI.h not synchronized with " \ + "nsIXULRuntime.idl"); +#include "mozilla/GeckoProcessTypes.h" +#undef GECKO_PROCESS_TYPE + +// .. and ensure that that is all of them: +static_assert(GeckoProcessType_Utility + 1 == GeckoProcessType_End, + "Did not find the final GeckoProcessType"); + +NS_IMETHODIMP +nsXULAppInfo::GetProcessType(uint32_t* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = XRE_GetProcessType(); + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetProcessID(uint32_t* aResult) { +#ifdef XP_WIN + *aResult = GetCurrentProcessId(); +#else + *aResult = getpid(); +#endif + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetUniqueProcessID(uint64_t* aResult) { + if (XRE_IsContentProcess()) { + ContentChild* cc = ContentChild::GetSingleton(); + *aResult = cc->GetID(); + } else { + *aResult = 0; + } + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetRemoteType(nsACString& aRemoteType) { + if (XRE_IsContentProcess()) { + aRemoteType = ContentChild::GetSingleton()->GetRemoteType(); + } else { + aRemoteType = NOT_REMOTE_TYPE; + } + + return NS_OK; +} + +static nsCString gLastAppVersion; +static nsCString gLastAppBuildID; + +NS_IMETHODIMP +nsXULAppInfo::GetLastAppVersion(nsACString& aResult) { + if (XRE_IsContentProcess()) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (!gLastAppVersion.IsVoid() && gLastAppVersion.IsEmpty()) { + NS_WARNING("Attempt to retrieve lastAppVersion before it has been set."); + return NS_ERROR_NOT_AVAILABLE; + } + + aResult.Assign(gLastAppVersion); + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetLastAppBuildID(nsACString& aResult) { + if (XRE_IsContentProcess()) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (!gLastAppBuildID.IsVoid() && gLastAppBuildID.IsEmpty()) { + NS_WARNING("Attempt to retrieve lastAppBuildID before it has been set."); + return NS_ERROR_NOT_AVAILABLE; + } + + aResult.Assign(gLastAppBuildID); + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetFissionAutostart(bool* aResult) { + *aResult = FissionAutostart(); + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetWin32kExperimentStatus(ExperimentStatus* aResult) { + if (!XRE_IsParentProcess()) { + return NS_ERROR_NOT_AVAILABLE; + } + + EnsureWin32kInitialized(); + *aResult = gWin32kExperimentStatus; + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetWin32kLiveStatusTestingOnly( + nsIXULRuntime::ContentWin32kLockdownState* aResult) { + if (!XRE_IsParentProcess()) { + return NS_ERROR_NOT_AVAILABLE; + } + + EnsureWin32kInitialized(); + *aResult = GetLiveWin32kLockdownState(); + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetWin32kSessionStatus( + nsIXULRuntime::ContentWin32kLockdownState* aResult) { + if (!XRE_IsParentProcess()) { + return NS_ERROR_NOT_AVAILABLE; + } + + EnsureWin32kInitialized(); + *aResult = gWin32kStatus; + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetFissionDecisionStatus(FissionDecisionStatus* aResult) { + if (!XRE_IsParentProcess()) { + return NS_ERROR_NOT_AVAILABLE; + } + + EnsureFissionAutostartInitialized(); + + MOZ_ASSERT(gFissionDecisionStatus != eFissionStatusUnknown); + *aResult = gFissionDecisionStatus; + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetFissionDecisionStatusString(nsACString& aResult) { + if (!XRE_IsParentProcess()) { + return NS_ERROR_NOT_AVAILABLE; + } + + EnsureFissionAutostartInitialized(); + switch (gFissionDecisionStatus) { + case eFissionExperimentControl: + aResult = "experimentControl"; + break; + case eFissionExperimentTreatment: + aResult = "experimentTreatment"; + break; + case eFissionDisabledByE10sEnv: + aResult = "disabledByE10sEnv"; + break; + case eFissionEnabledByEnv: + aResult = "enabledByEnv"; + break; + case eFissionDisabledByEnv: + aResult = "disabledByEnv"; + break; + case eFissionEnabledByDefault: + aResult = "enabledByDefault"; + break; + case eFissionDisabledByDefault: + aResult = "disabledByDefault"; + break; + case eFissionEnabledByUserPref: + aResult = "enabledByUserPref"; + break; + case eFissionDisabledByUserPref: + aResult = "disabledByUserPref"; + break; + case eFissionDisabledByE10sOther: + aResult = "disabledByE10sOther"; + break; + case eFissionEnabledByRollout: + aResult = "enabledByRollout"; + break; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected enum value"); + } + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetSessionHistoryInParent(bool* aResult) { + *aResult = SessionHistoryInParent(); + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetBrowserTabsRemoteAutostart(bool* aResult) { + *aResult = BrowserTabsRemoteAutostart(); + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetMaxWebProcessCount(uint32_t* aResult) { + *aResult = mozilla::GetMaxWebProcessCount(); + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetAccessibilityEnabled(bool* aResult) { +#ifdef ACCESSIBILITY + *aResult = GetAccService() != nullptr; +#else + *aResult = false; +#endif + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetAccessibilityInstantiator(nsAString& aInstantiator) { +#if defined(ACCESSIBILITY) && defined(XP_WIN) + if (!GetAccService()) { + aInstantiator.Truncate(); + return NS_OK; + } + nsAutoString ipClientInfo; + a11y::Compatibility::GetHumanReadableConsumersStr(ipClientInfo); + aInstantiator.Append(ipClientInfo); + aInstantiator.AppendLiteral("|"); + + nsCOMPtr<nsIFile> oopClientExe; + if (a11y::GetInstantiator(getter_AddRefs(oopClientExe))) { + nsAutoString oopClientInfo; + if (NS_SUCCEEDED(oopClientExe->GetPath(oopClientInfo))) { + aInstantiator.Append(oopClientInfo); + } + } +#else + aInstantiator.Truncate(); +#endif + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetShouldBlockIncompatJaws(bool* aResult) { + *aResult = false; +#if defined(ACCESSIBILITY) && defined(XP_WIN) + *aResult = mozilla::a11y::Compatibility::IsOldJAWS(); +#endif + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetIs64Bit(bool* aResult) { +#ifdef HAVE_64BIT_BUILD + *aResult = true; +#else + *aResult = false; +#endif + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetIsTextRecognitionSupported(bool* aResult) { + *aResult = widget::TextRecognition::IsSupported(); + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::EnsureContentProcess() { + if (!XRE_IsParentProcess()) return NS_ERROR_NOT_AVAILABLE; + + RefPtr<ContentParent> unused = + ContentParent::GetNewOrUsedBrowserProcess(DEFAULT_REMOTE_TYPE); + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::InvalidateCachesOnRestart() { + nsCOMPtr<nsIFile> file; + nsresult rv = + NS_GetSpecialDirectory(NS_APP_PROFILE_DIR_STARTUP, getter_AddRefs(file)); + if (NS_FAILED(rv)) return rv; + if (!file) return NS_ERROR_NOT_AVAILABLE; + + file->AppendNative(FILE_COMPATIBILITY_INFO); + + nsINIParser parser; + rv = parser.Init(file); + if (NS_FAILED(rv)) { + // This fails if compatibility.ini is not there, so we'll + // flush the caches on the next restart anyways. + return NS_OK; + } + + nsAutoCString buf; + rv = parser.GetString("Compatibility", "InvalidateCaches", buf); + + if (NS_FAILED(rv)) { + PRFileDesc* fd; + rv = file->OpenNSPRFileDesc(PR_RDWR | PR_APPEND, 0600, &fd); + if (NS_FAILED(rv)) { + NS_ERROR("could not create output stream"); + return NS_ERROR_NOT_AVAILABLE; + } + static const char kInvalidationHeader[] = + NS_LINEBREAK "InvalidateCaches=1" NS_LINEBREAK; + PR_Write(fd, kInvalidationHeader, sizeof(kInvalidationHeader) - 1); + PR_Close(fd); + } + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetReplacedLockTime(PRTime* aReplacedLockTime) { + if (!gProfileLock) return NS_ERROR_NOT_AVAILABLE; + gProfileLock->GetReplacedLockTime(aReplacedLockTime); + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetDefaultUpdateChannel(nsACString& aResult) { + aResult.AssignLiteral(MOZ_STRINGIFY(MOZ_UPDATE_CHANNEL)); + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetDistributionID(nsACString& aResult) { + aResult.AssignLiteral(MOZ_DISTRIBUTION_ID); + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetWindowsDLLBlocklistStatus(bool* aResult) { +#if defined(HAS_DLL_BLOCKLIST) + *aResult = DllBlocklist_CheckStatus(); +#else + *aResult = false; +#endif + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetRestartedByOS(bool* aResult) { + *aResult = gRestartedByOS; + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetChromeColorSchemeIsDark(bool* aResult) { + LookAndFeel::EnsureColorSchemesInitialized(); + *aResult = LookAndFeel::ColorSchemeForChrome() == ColorScheme::Dark; + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetContentThemeDerivedColorSchemeIsDark(bool* aResult) { + *aResult = + LookAndFeel::ThemeDerivedColorSchemeForContent() == ColorScheme::Dark; + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetPrefersReducedMotion(bool* aResult) { + *aResult = + LookAndFeel::GetInt(LookAndFeel::IntID::PrefersReducedMotion, 0) == 1; + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetDrawInTitlebar(bool* aResult) { + *aResult = LookAndFeel::DrawInTitlebar(); + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetDesktopEnvironment(nsACString& aDesktopEnvironment) { +#ifdef MOZ_WIDGET_GTK + aDesktopEnvironment.Assign(GetDesktopEnvironmentIdentifier()); +#endif + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetProcessStartupShortcut(nsAString& aShortcut) { +#if defined(XP_WIN) + if (XRE_IsParentProcess()) { + aShortcut.Assign(gProcessStartupShortcut); + return NS_OK; + } +#endif + return NS_ERROR_NOT_AVAILABLE; +} + +#if defined(XP_WIN) && defined(MOZ_LAUNCHER_PROCESS) +// Forward declaration +void SetupLauncherProcessPref(); + +static Maybe<LauncherRegistryInfo::EnabledState> gLauncherProcessState; +#endif // defined(XP_WIN) && defined(MOZ_LAUNCHER_PROCESS) + +NS_IMETHODIMP +nsXULAppInfo::GetLauncherProcessState(uint32_t* aResult) { +#if defined(XP_WIN) && defined(MOZ_LAUNCHER_PROCESS) + SetupLauncherProcessPref(); + + if (!gLauncherProcessState) { + return NS_ERROR_UNEXPECTED; + } + + *aResult = static_cast<uint32_t>(gLauncherProcessState.value()); + return NS_OK; +#else + return NS_ERROR_NOT_AVAILABLE; +#endif +} + +#ifdef XP_WIN +NS_IMETHODIMP +nsXULAppInfo::GetUserCanElevate(bool* aUserCanElevate) { + HANDLE rawToken; + if (!::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, &rawToken)) { + *aUserCanElevate = false; + return NS_OK; + } + + nsAutoHandle token(rawToken); + LauncherResult<TOKEN_ELEVATION_TYPE> elevationType = GetElevationType(token); + if (elevationType.isErr()) { + *aUserCanElevate = false; + return NS_OK; + } + + // The possible values returned for elevationType and their meanings are: + // TokenElevationTypeDefault: The token does not have a linked token + // (e.g. UAC disabled or a standard user, so they can't be elevated) + // TokenElevationTypeFull: The token is linked to an elevated token + // (e.g. UAC is enabled and the user is already elevated so they can't + // be elevated again) + // TokenElevationTypeLimited: The token is linked to a limited token + // (e.g. UAC is enabled and the user is not elevated, so they can be + // elevated) + *aUserCanElevate = (elevationType.inspect() == TokenElevationTypeLimited); + return NS_OK; +} +#endif + +NS_IMETHODIMP +nsXULAppInfo::GetCrashReporterEnabled(bool* aEnabled) { + *aEnabled = CrashReporter::GetEnabled(); + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::SetEnabled(bool aEnabled) { + if (aEnabled) { + if (CrashReporter::GetEnabled()) { + // no point in erroring for double-enabling + return NS_OK; + } + + nsCOMPtr<nsIFile> greBinDir; + NS_GetSpecialDirectory(NS_GRE_BIN_DIR, getter_AddRefs(greBinDir)); + if (!greBinDir) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIFile> xreBinDirectory = greBinDir; + if (!xreBinDirectory) { + return NS_ERROR_FAILURE; + } + + return CrashReporter::SetExceptionHandler(xreBinDirectory, true); + } + + if (!CrashReporter::GetEnabled()) { + // no point in erroring for double-disabling + return NS_OK; + } + + return CrashReporter::UnsetExceptionHandler(); +} + +NS_IMETHODIMP +nsXULAppInfo::GetServerURL(nsIURL** aServerURL) { + NS_ENSURE_ARG_POINTER(aServerURL); + if (!CrashReporter::GetEnabled()) return NS_ERROR_NOT_INITIALIZED; + + nsAutoCString data; + if (!CrashReporter::GetServerURL(data)) { + return NS_ERROR_FAILURE; + } + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), data); + if (!uri) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIURL> url; + url = do_QueryInterface(uri); + NS_ADDREF(*aServerURL = url); + + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::SetServerURL(nsIURL* aServerURL) { + // Only allow https or http URLs + if (!aServerURL->SchemeIs("http") && !aServerURL->SchemeIs("https")) { + return NS_ERROR_INVALID_ARG; + } + + nsAutoCString spec; + nsresult rv = aServerURL->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + return CrashReporter::SetServerURL(spec); +} + +NS_IMETHODIMP +nsXULAppInfo::GetMinidumpPath(nsIFile** aMinidumpPath) { + if (!CrashReporter::GetEnabled()) return NS_ERROR_NOT_INITIALIZED; + + nsAutoString path; + if (!CrashReporter::GetMinidumpPath(path)) return NS_ERROR_FAILURE; + + nsresult rv = NS_NewLocalFile(path, false, aMinidumpPath); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::SetMinidumpPath(nsIFile* aMinidumpPath) { + nsAutoString path; + nsresult rv = aMinidumpPath->GetPath(path); + NS_ENSURE_SUCCESS(rv, rv); + return CrashReporter::SetMinidumpPath(path); +} + +NS_IMETHODIMP +nsXULAppInfo::GetMinidumpForID(const nsAString& aId, nsIFile** aMinidump) { + if (!CrashReporter::GetMinidumpForID(aId, aMinidump)) { + return NS_ERROR_FILE_NOT_FOUND; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::GetExtraFileForID(const nsAString& aId, nsIFile** aExtraFile) { + if (!CrashReporter::GetExtraFileForID(aId, aExtraFile)) { + return NS_ERROR_FILE_NOT_FOUND; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::AnnotateCrashReport(const nsACString& key, + const nsACString& data) { + CrashReporter::Annotation annotation; + + if (!AnnotationFromString(annotation, PromiseFlatCString(key).get())) { + return NS_ERROR_INVALID_ARG; + } + + return CrashReporter::AnnotateCrashReport(annotation, data); +} + +NS_IMETHODIMP +nsXULAppInfo::RemoveCrashReportAnnotation(const nsACString& key) { + CrashReporter::Annotation annotation; + + if (!AnnotationFromString(annotation, PromiseFlatCString(key).get())) { + return NS_ERROR_INVALID_ARG; + } + + return CrashReporter::RemoveCrashReportAnnotation(annotation); +} + +NS_IMETHODIMP +nsXULAppInfo::IsAnnotationAllowlistedForPing(const nsACString& aValue, + bool* aIsAllowlisted) { + CrashReporter::Annotation annotation; + + if (!AnnotationFromString(annotation, PromiseFlatCString(aValue).get())) { + return NS_ERROR_INVALID_ARG; + } + + *aIsAllowlisted = CrashReporter::IsAnnotationAllowlistedForPing(annotation); + + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::AppendAppNotesToCrashReport(const nsACString& data) { + return CrashReporter::AppendAppNotesToCrashReport(data); +} + +NS_IMETHODIMP +nsXULAppInfo::RegisterAppMemory(uint64_t pointer, uint64_t len) { + return CrashReporter::RegisterAppMemory((void*)pointer, len); +} + +NS_IMETHODIMP +nsXULAppInfo::WriteMinidumpForException(void* aExceptionInfo) { +#ifdef XP_WIN + return CrashReporter::WriteMinidumpForException( + static_cast<EXCEPTION_POINTERS*>(aExceptionInfo)); +#else + return NS_ERROR_NOT_IMPLEMENTED; +#endif +} + +NS_IMETHODIMP +nsXULAppInfo::AppendObjCExceptionInfoToAppNotes(void* aException) { +#ifdef XP_MACOSX + return CrashReporter::AppendObjCExceptionInfoToAppNotes(aException); +#else + return NS_ERROR_NOT_IMPLEMENTED; +#endif +} + +NS_IMETHODIMP +nsXULAppInfo::GetSubmitReports(bool* aEnabled) { + return CrashReporter::GetSubmitReports(aEnabled); +} + +NS_IMETHODIMP +nsXULAppInfo::SetSubmitReports(bool aEnabled) { + return CrashReporter::SetSubmitReports(aEnabled); +} + +NS_IMETHODIMP +nsXULAppInfo::UpdateCrashEventsDir() { + CrashReporter::UpdateCrashEventsDir(); + return NS_OK; +} + +NS_IMETHODIMP +nsXULAppInfo::SaveMemoryReport() { + if (!CrashReporter::GetEnabled()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsCOMPtr<nsIFile> file; + nsresult rv = CrashReporter::GetDefaultMemoryReportFile(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsString path; + file->GetPath(path); + + nsCOMPtr<nsIMemoryInfoDumper> dumper = + do_GetService("@mozilla.org/memory-info-dumper;1"); + if (NS_WARN_IF(!dumper)) { + return NS_ERROR_UNEXPECTED; + } + + rv = dumper->DumpMemoryReportsToNamedFile( + path, this, file, true /* anonymize */, false /* minimizeMemoryUsage */); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + +// This method is from nsIFInishDumpingCallback. +NS_IMETHODIMP +nsXULAppInfo::Callback(nsISupports* aData) { + nsCOMPtr<nsIFile> file = do_QueryInterface(aData); + MOZ_ASSERT(file); + + CrashReporter::SetMemoryReportFile(file); + return NS_OK; +} + +static const nsXULAppInfo kAppInfo; +namespace mozilla { +nsresult AppInfoConstructor(REFNSIID aIID, void** aResult) { + return const_cast<nsXULAppInfo*>(&kAppInfo)->QueryInterface(aIID, aResult); +} +} // namespace mozilla + +bool gLogConsoleErrors = false; + +#define NS_ENSURE_TRUE_LOG(x, ret) \ + PR_BEGIN_MACRO \ + if (MOZ_UNLIKELY(!(x))) { \ + NS_WARNING("NS_ENSURE_TRUE(" #x ") failed"); \ + gLogConsoleErrors = true; \ + return ret; \ + } \ + PR_END_MACRO + +#define NS_ENSURE_SUCCESS_LOG(res, ret) \ + NS_ENSURE_TRUE_LOG(NS_SUCCEEDED(res), ret) + +/** + * Because we're starting/stopping XPCOM several times in different scenarios, + * this class is a stack-based critter that makes sure that XPCOM is shut down + * during early returns. + */ + +class ScopedXPCOMStartup { + public: + ScopedXPCOMStartup() : mServiceManager(nullptr) {} + ~ScopedXPCOMStartup(); + + nsresult Initialize(bool aInitJSContext = true); + nsresult SetWindowCreator(nsINativeAppSupport* native); + + private: + nsIServiceManager* mServiceManager; + static nsINativeAppSupport* gNativeAppSupport; + + friend already_AddRefed<nsINativeAppSupport> NS_GetNativeAppSupport(); +}; + +ScopedXPCOMStartup::~ScopedXPCOMStartup() { + NS_IF_RELEASE(gNativeAppSupport); + + if (mServiceManager) { +#ifdef XP_MACOSX + // On OS X, we need a pool to catch cocoa objects that are autoreleased + // during teardown. + mozilla::MacAutoreleasePool pool; +#endif + + nsCOMPtr<nsIAppStartup> appStartup(components::AppStartup::Service()); + if (appStartup) appStartup->DestroyHiddenWindow(); + + gDirServiceProvider->DoShutdown(); + PROFILER_MARKER_UNTYPED("Shutdown early", OTHER); + + WriteConsoleLog(); + + NS_ShutdownXPCOM(mServiceManager); + mServiceManager = nullptr; + } +} + +nsresult ScopedXPCOMStartup::Initialize(bool aInitJSContext) { + NS_ASSERTION(gDirServiceProvider, "Should not get here!"); + + nsresult rv; + + rv = NS_InitXPCOM(&mServiceManager, gDirServiceProvider->GetAppDir(), + gDirServiceProvider, aInitJSContext); + if (NS_FAILED(rv)) { + NS_ERROR("Couldn't start xpcom!"); + mServiceManager = nullptr; + } else { +#ifdef DEBUG + nsCOMPtr<nsIComponentRegistrar> reg = do_QueryInterface(mServiceManager); + NS_ASSERTION(reg, "Service Manager doesn't QI to Registrar."); +#endif + } + + return rv; +} + +/** + * This is a little factory class that serves as a singleton-service-factory + * for the nativeappsupport object. + */ +class nsSingletonFactory final : public nsIFactory { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIFACTORY + + explicit nsSingletonFactory(nsISupports* aSingleton); + + private: + ~nsSingletonFactory() = default; + nsCOMPtr<nsISupports> mSingleton; +}; + +nsSingletonFactory::nsSingletonFactory(nsISupports* aSingleton) + : mSingleton(aSingleton) { + NS_ASSERTION(mSingleton, "Singleton was null!"); +} + +NS_IMPL_ISUPPORTS(nsSingletonFactory, nsIFactory) + +NS_IMETHODIMP +nsSingletonFactory::CreateInstance(const nsIID& aIID, void** aResult) { + return mSingleton->QueryInterface(aIID, aResult); +} + +/** + * Set our windowcreator on the WindowWatcher service. + */ +nsresult ScopedXPCOMStartup::SetWindowCreator(nsINativeAppSupport* native) { + nsresult rv; + + NS_IF_ADDREF(gNativeAppSupport = native); + + nsCOMPtr<nsIWindowCreator> creator(components::AppStartup::Service()); + if (!creator) return NS_ERROR_UNEXPECTED; + + nsCOMPtr<nsIWindowWatcher> wwatch( + do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + return wwatch->SetWindowCreator(creator); +} + +/* static */ already_AddRefed<nsINativeAppSupport> NS_GetNativeAppSupport() { + if (!ScopedXPCOMStartup::gNativeAppSupport) { + return nullptr; + } + + return do_AddRef(ScopedXPCOMStartup::gNativeAppSupport); +} + +nsINativeAppSupport* ScopedXPCOMStartup::gNativeAppSupport; + +static void DumpArbitraryHelp() { + nsresult rv; + + ScopedLogging log; + + { + ScopedXPCOMStartup xpcom; + xpcom.Initialize(); + + nsCOMPtr<nsICommandLineRunner> cmdline(new nsCommandLine()); + + nsCString text; + rv = cmdline->GetHelpText(text); + if (NS_SUCCEEDED(rv)) printf("%s", text.get()); + } +} + +// English text needs to go into a dtd file. +// But when this is called we have no components etc. These strings must either +// be here, or in a native resource file. +static void DumpHelp() { + printf( + "Usage: %s [ options ... ] [URL]\n" + " where options include:\n\n", + gArgv[0]); + +#ifdef MOZ_X11 + printf( + "X11 options\n" + " --display=DISPLAY X display to use\n" + " --sync Make X calls synchronous\n"); +#endif +#ifdef XP_UNIX + printf( + " --g-fatal-warnings Make all warnings fatal\n" + "\n%s options\n", + (const char*)gAppData->name); +#endif + + printf( + " -h or --help Print this message.\n" + " -v or --version Print %s version.\n" + " --full-version Print %s version, build and platform build ids.\n" + " -P <profile> Start with <profile>.\n" + " --profile <path> Start with profile at <path>.\n" + " --migration Start with migration wizard.\n" + " --ProfileManager Start with ProfileManager.\n" +#ifdef MOZ_HAS_REMOTE + " --no-remote Do not accept or send remote commands; implies\n" + " --new-instance.\n" + " --new-instance Open new instance, not a new window in running " + "instance.\n" +#endif + " --safe-mode Disables extensions and themes for this session.\n" +#ifdef MOZ_BLOCK_PROFILE_DOWNGRADE + " --allow-downgrade Allows downgrading a profile.\n" +#endif + " --MOZ_LOG=<modules> Treated as MOZ_LOG=<modules> environment " + "variable,\n" + " overrides it.\n" + " --MOZ_LOG_FILE=<file> Treated as MOZ_LOG_FILE=<file> environment " + "variable,\n" + " overrides it. If MOZ_LOG_FILE is not specified as " + "an\n" + " argument or as an environment variable, logging " + "will be\n" + " written to stdout.\n", + (const char*)gAppData->name, (const char*)gAppData->name); + +#if defined(XP_WIN) + printf(" --console Start %s with a debugging console.\n", + (const char*)gAppData->name); +#endif + +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) || defined(XP_MACOSX) + printf(" --headless Run without a GUI.\n"); +#endif + + // this works, but only after the components have registered. so if you drop + // in a new command line handler, --help won't not until the second run. out + // of the bug, because we ship a component.reg file, it works correctly. + DumpArbitraryHelp(); +} + +static inline void DumpVersion() { + if (gAppData->vendor && *gAppData->vendor) { + printf("%s ", (const char*)gAppData->vendor); + } + printf("%s ", (const char*)gAppData->name); + + // Use the displayed version + // For example, for beta, we would display 42.0b2 instead of 42.0 + printf("%s", MOZ_STRINGIFY(MOZ_APP_VERSION_DISPLAY)); + + if (gAppData->copyright && *gAppData->copyright) { + printf(", %s", (const char*)gAppData->copyright); + } + printf("\n"); +} + +static inline void DumpFullVersion() { + if (gAppData->vendor && *gAppData->vendor) { + printf("%s ", (const char*)gAppData->vendor); + } + printf("%s ", (const char*)gAppData->name); + + // Use the displayed version + // For example, for beta, we would display 42.0b2 instead of 42.0 + printf("%s ", MOZ_STRINGIFY(MOZ_APP_VERSION_DISPLAY)); + + printf("%s ", (const char*)gAppData->buildID); + printf("%s ", (const char*)PlatformBuildID()); + if (gAppData->copyright && *gAppData->copyright) { + printf(", %s", (const char*)gAppData->copyright); + } + printf("\n"); +} + +void XRE_InitOmnijar(nsIFile* greOmni, nsIFile* appOmni) { + mozilla::Omnijar::Init(greOmni, appOmni); +} + +nsresult XRE_GetBinaryPath(nsIFile** aResult) { + return mozilla::BinaryPath::GetFile(aResult); +} + +#ifdef XP_WIN +# include "nsWindowsRestart.cpp" +# include <shellapi.h> + +typedef BOOL(WINAPI* SetProcessDEPPolicyFunc)(DWORD dwFlags); + +static void RegisterApplicationRestartChanged(const char* aPref, void* aData) { + DWORD cchCmdLine = 0; + HRESULT rc = ::GetApplicationRestartSettings(::GetCurrentProcess(), nullptr, + &cchCmdLine, nullptr); + bool wasRegistered = false; + if (rc == S_OK) { + wasRegistered = true; + } + + if (Preferences::GetBool(PREF_WIN_REGISTER_APPLICATION_RESTART, false) && + !wasRegistered) { + // Make the command line to use when restarting. + // Excludes argv[0] because RegisterApplicationRestart adds the + // executable name, replace that temporarily with -os-restarted + char* exeName = gRestartArgv[0]; + gRestartArgv[0] = const_cast<char*>("-os-restarted"); + wchar_t** restartArgvConverted = + AllocConvertUTF8toUTF16Strings(gRestartArgc, gRestartArgv); + gRestartArgv[0] = exeName; + + mozilla::UniquePtr<wchar_t[]> restartCommandLine; + if (restartArgvConverted) { + restartCommandLine = + mozilla::MakeCommandLine(gRestartArgc, restartArgvConverted); + FreeAllocStrings(gRestartArgc, restartArgvConverted); + } + + if (restartCommandLine) { + // Flags RESTART_NO_PATCH and RESTART_NO_REBOOT are not set, so we + // should be restarted if terminated by an update or restart. + ::RegisterApplicationRestart(restartCommandLine.get(), + RESTART_NO_CRASH | RESTART_NO_HANG); + } + } else if (wasRegistered) { + ::UnregisterApplicationRestart(); + } +} + +static void OnAlteredPrefetchPrefChanged(const char* aPref, void* aData) { + int32_t prefVal = Preferences::GetInt(PREF_WIN_ALTERED_DLL_PREFETCH, 0); + + mozilla::DllPrefetchExperimentRegistryInfo prefetchRegInfo; + mozilla::DebugOnly<mozilla::Result<Ok, nsresult>> reflectResult = + prefetchRegInfo.ReflectPrefToRegistry(prefVal); + + MOZ_ASSERT(reflectResult.value.isOk()); +} + +static void SetupAlteredPrefetchPref() { + mozilla::DllPrefetchExperimentRegistryInfo prefetchRegInfo; + + mozilla::DebugOnly<mozilla::Result<Ok, nsresult>> reflectResult = + prefetchRegInfo.ReflectPrefToRegistry( + Preferences::GetInt(PREF_WIN_ALTERED_DLL_PREFETCH, 0)); + MOZ_ASSERT(reflectResult.value.isOk()); + + Preferences::RegisterCallback(&OnAlteredPrefetchPrefChanged, + PREF_WIN_ALTERED_DLL_PREFETCH); +} + +static void ReflectSkeletonUIPrefToRegistry(const char* aPref, void* aData) { + Unused << aPref; + Unused << aData; + + bool shouldBeEnabled = + Preferences::GetBool(kPrefPreXulSkeletonUI, false) && + Preferences::GetBool(kPrefBrowserStartupBlankWindow, false) && + LookAndFeel::DrawInTitlebar(); + if (shouldBeEnabled && Preferences::HasUserValue(kPrefThemeId)) { + nsCString themeId; + Preferences::GetCString(kPrefThemeId, themeId); + if (themeId.EqualsLiteral("default-theme@mozilla.org")) { + Unused << SetPreXULSkeletonUIThemeId(ThemeMode::Default); + } else if (themeId.EqualsLiteral("firefox-compact-dark@mozilla.org")) { + Unused << SetPreXULSkeletonUIThemeId(ThemeMode::Dark); + } else if (themeId.EqualsLiteral("firefox-compact-light@mozilla.org")) { + Unused << SetPreXULSkeletonUIThemeId(ThemeMode::Light); + } else { + shouldBeEnabled = false; + } + } else if (shouldBeEnabled) { + Unused << SetPreXULSkeletonUIThemeId(ThemeMode::Default); + } + + if (GetPreXULSkeletonUIEnabled() != shouldBeEnabled) { + Unused << SetPreXULSkeletonUIEnabledIfAllowed(shouldBeEnabled); + } +} + +static void SetupSkeletonUIPrefs() { + ReflectSkeletonUIPrefToRegistry(nullptr, nullptr); + Preferences::RegisterCallback(&ReflectSkeletonUIPrefToRegistry, + kPrefPreXulSkeletonUI); + Preferences::RegisterCallback(&ReflectSkeletonUIPrefToRegistry, + kPrefBrowserStartupBlankWindow); + Preferences::RegisterCallback(&ReflectSkeletonUIPrefToRegistry, kPrefThemeId); + Preferences::RegisterCallback( + &ReflectSkeletonUIPrefToRegistry, + nsDependentCString(StaticPrefs::GetPrefName_browser_tabs_inTitlebar())); +} + +# if defined(MOZ_LAUNCHER_PROCESS) + +static void OnLauncherPrefChanged(const char* aPref, void* aData) { + bool prefVal = Preferences::GetBool(PREF_WIN_LAUNCHER_PROCESS_ENABLED, true); + + mozilla::LauncherRegistryInfo launcherRegInfo; + mozilla::DebugOnly<mozilla::LauncherVoidResult> reflectResult = + launcherRegInfo.ReflectPrefToRegistry(prefVal); + MOZ_ASSERT(reflectResult.inspect().isOk()); +} + +static void OnLauncherTelemetryPrefChanged(const char* aPref, void* aData) { + bool prefVal = Preferences::GetBool(kPrefHealthReportUploadEnabled, true); + + mozilla::LauncherRegistryInfo launcherRegInfo; + mozilla::DebugOnly<mozilla::LauncherVoidResult> reflectResult = + launcherRegInfo.ReflectTelemetryPrefToRegistry(prefVal); + MOZ_ASSERT(reflectResult.inspect().isOk()); +} + +static void SetupLauncherProcessPref() { + if (gLauncherProcessState) { + // We've already successfully run + return; + } + + mozilla::LauncherRegistryInfo launcherRegInfo; + + mozilla::LauncherResult<mozilla::LauncherRegistryInfo::EnabledState> + enabledState = launcherRegInfo.IsEnabled(); + + if (enabledState.isOk()) { + gLauncherProcessState = Some(enabledState.unwrap()); + + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::LauncherProcessState, + static_cast<uint32_t>(enabledState.unwrap())); + + // Reflect the launcher process registry state into user prefs + Preferences::SetBool( + PREF_WIN_LAUNCHER_PROCESS_ENABLED, + enabledState.unwrap() != + mozilla::LauncherRegistryInfo::EnabledState::ForceDisabled); + } + + mozilla::DebugOnly<mozilla::LauncherVoidResult> reflectResult = + launcherRegInfo.ReflectTelemetryPrefToRegistry( + Preferences::GetBool(kPrefHealthReportUploadEnabled, true)); + MOZ_ASSERT(reflectResult.inspect().isOk()); + + Preferences::RegisterCallback(&OnLauncherPrefChanged, + PREF_WIN_LAUNCHER_PROCESS_ENABLED); + Preferences::RegisterCallback(&OnLauncherTelemetryPrefChanged, + kPrefHealthReportUploadEnabled); +} + +# endif // defined(MOZ_LAUNCHER_PROCESS) + +# if defined(MOZ_DEFAULT_BROWSER_AGENT) + +# define DEFAULT_BROWSER_AGENT_KEY_NAME \ + "SOFTWARE\\" MOZ_APP_VENDOR "\\" MOZ_APP_NAME "\\Default Browser Agent" + +static nsresult PrependRegistryValueName(nsAutoString& aValueName) { + nsresult rv; + + nsCOMPtr<nsIFile> binaryPath; + rv = XRE_GetBinaryPath(getter_AddRefs(binaryPath)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> binaryDir; + rv = binaryPath->GetParent(getter_AddRefs(binaryDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString prefix; + rv = binaryDir->GetPath(prefix); + NS_ENSURE_SUCCESS(rv, rv); + + prefix.AppendLiteral("|"); + aValueName.Insert(prefix, 0); + + return NS_OK; +} + +static void OnDefaultAgentTelemetryPrefChanged(const char* aPref, void* aData) { + nsresult rv; + nsAutoString valueName; + if (strcmp(aPref, kPrefHealthReportUploadEnabled) == 0) { + valueName.AssignLiteral("DisableTelemetry"); + } else if (strcmp(aPref, kPrefDefaultAgentEnabled) == 0) { + valueName.AssignLiteral("DisableDefaultBrowserAgent"); + } else { + return; + } + rv = PrependRegistryValueName(valueName); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); + NS_ENSURE_SUCCESS_VOID(rv); + + nsAutoString keyName; + keyName.AppendLiteral(DEFAULT_BROWSER_AGENT_KEY_NAME); + rv = regKey->Create(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, keyName, + nsIWindowsRegKey::ACCESS_WRITE); + + bool prefVal = Preferences::GetBool(aPref, true); + + // We're recording whether the pref is *disabled*, so invert the value. + rv = regKey->WriteIntValue(valueName, prefVal ? 0 : 1); + NS_ENSURE_SUCCESS_VOID(rv); +} + +static void OnSetDefaultBrowserUserChoicePrefChanged(const char* aPref, + void* aData) { + nsresult rv; + if (strcmp(aPref, kPrefSetDefaultBrowserUserChoicePref) != 0) { + return; + } + nsAutoString valueName; + valueName.AssignLiteral("SetDefaultBrowserUserChoice"); + rv = PrependRegistryValueName(valueName); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); + NS_ENSURE_SUCCESS_VOID(rv); + + nsAutoString keyName; + keyName.AppendLiteral(DEFAULT_BROWSER_AGENT_KEY_NAME); + rv = regKey->Create(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, keyName, + nsIWindowsRegKey::ACCESS_WRITE); + + bool prefVal = Preferences::GetBool(aPref, true); + + rv = regKey->WriteIntValue(valueName, prefVal); + NS_ENSURE_SUCCESS_VOID(rv); +} + +static void OnDefaultAgentRemoteSettingsPrefChanged(const char* aPref, + void* aData) { + nsresult rv; + nsAutoString valueName; + if (strcmp(aPref, kPrefServicesSettingsServer) == 0) { + valueName.AssignLiteral("ServicesSettingsServer"); + } else if (strcmp(aPref, kPrefSecurityContentSignatureRootHash) == 0) { + valueName.AssignLiteral("SecurityContentSignatureRootHash"); + } else { + return; + } + rv = PrependRegistryValueName(valueName); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); + NS_ENSURE_SUCCESS_VOID(rv); + + nsAutoString keyName; + keyName.AppendLiteral(DEFAULT_BROWSER_AGENT_KEY_NAME); + rv = regKey->Create(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, keyName, + nsIWindowsRegKey::ACCESS_WRITE); + + NS_ENSURE_SUCCESS_VOID(rv); + + nsAutoString prefVal; + rv = Preferences::GetString(aPref, prefVal); + NS_ENSURE_SUCCESS_VOID(rv); + + if (prefVal.IsEmpty()) { + rv = regKey->RemoveValue(valueName); + } else { + rv = regKey->WriteStringValue(valueName, prefVal); + } + NS_ENSURE_SUCCESS_VOID(rv); +} + +static void SetDefaultAgentLastRunTime() { + nsresult rv; + nsAutoString valueName; + valueName.AppendLiteral("AppLastRunTime"); + rv = PrependRegistryValueName(valueName); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); + NS_ENSURE_SUCCESS_VOID(rv); + + nsAutoString keyName; + keyName.AppendLiteral(DEFAULT_BROWSER_AGENT_KEY_NAME); + rv = regKey->Create(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, keyName, + nsIWindowsRegKey::ACCESS_WRITE); + NS_ENSURE_SUCCESS_VOID(rv); + + FILETIME fileTime; + GetSystemTimeAsFileTime(&fileTime); + + ULARGE_INTEGER integerTime; + integerTime.u.LowPart = fileTime.dwLowDateTime; + integerTime.u.HighPart = fileTime.dwHighDateTime; + + rv = regKey->WriteInt64Value(valueName, integerTime.QuadPart); + NS_ENSURE_SUCCESS_VOID(rv); +} + +# endif // defined(MOZ_DEFAULT_BROWSER_AGENT) + +#endif // XP_WIN + +void UnlockProfile() { + if (gProfileLock) { + gProfileLock->Unlock(); + } +} + +nsresult LaunchChild(bool aBlankCommandLine, bool aTryExec) { + // Restart this process by exec'ing it into the current process + // if supported by the platform. Otherwise, use NSPR. + +#ifdef MOZ_JPROF + // make sure JPROF doesn't think we're E10s + unsetenv("JPROF_ISCHILD"); +#endif + + if (aBlankCommandLine) { + gRestartArgc = 1; + gRestartArgv[gRestartArgc] = nullptr; + } + +#if defined(MOZ_HAS_REMOTE) + if (gRestartWithoutRemote) { + SaveToEnv("MOZ_NO_REMOTE=1"); + } +#endif + + SaveToEnv("MOZ_LAUNCHED_CHILD=1"); +#if defined(MOZ_LAUNCHER_PROCESS) + SaveToEnv("MOZ_LAUNCHER_PROCESS=1"); +#endif // defined(MOZ_LAUNCHER_PROCESS) + +#if !defined(MOZ_WIDGET_ANDROID) // Android has separate restart code. +# if defined(XP_MACOSX) + CommandLineServiceMac::SetupMacCommandLine(gRestartArgc, gRestartArgv, true); + LaunchChildMac(gRestartArgc, gRestartArgv); +# else + nsCOMPtr<nsIFile> lf; + nsresult rv = XRE_GetBinaryPath(getter_AddRefs(lf)); + if (NS_FAILED(rv)) return rv; + +# if defined(XP_WIN) + nsAutoString exePath; + rv = lf->GetPath(exePath); + if (NS_FAILED(rv)) return rv; + + HANDLE hProcess; + if (!WinLaunchChild(exePath.get(), gRestartArgc, gRestartArgv, nullptr, + &hProcess)) + return NS_ERROR_FAILURE; + // Keep the current process around until the restarted process has created + // its message queue, to avoid the launched process's windows being forced + // into the background. + mozilla::WaitForInputIdle(hProcess); + ::CloseHandle(hProcess); + +# else + nsAutoCString exePath; + rv = lf->GetNativePath(exePath); + if (NS_FAILED(rv)) return rv; + +# if defined(XP_UNIX) + if (aTryExec) { + execv(exePath.get(), gRestartArgv); + + // If execv returns we know it's because it failed. + return NS_ERROR_FAILURE; + } +# endif + if (PR_CreateProcessDetached(exePath.get(), gRestartArgv, nullptr, nullptr) == + PR_FAILURE) { + return NS_ERROR_FAILURE; + } + + // Note that we don't know if the child process starts okay, if it + // immediately returns non-zero then we may mask that by returning a zero + // exit status. + +# endif // WP_WIN +# endif // WP_MACOSX +#endif // MOZ_WIDGET_ANDROID + + return NS_ERROR_LAUNCHED_CHILD_PROCESS; +} + +static const char kProfileProperties[] = + "chrome://mozapps/locale/profile/profileSelection.properties"; + +namespace { + +/** + * This class, instead of a raw nsresult, should be the return type of any + * function called by SelectProfile that initializes XPCOM. + */ +class ReturnAbortOnError { + public: + MOZ_IMPLICIT ReturnAbortOnError(nsresult aRv) { mRv = ConvertRv(aRv); } + + operator nsresult() { return mRv; } + + private: + inline nsresult ConvertRv(nsresult aRv) { + if (NS_SUCCEEDED(aRv) || aRv == NS_ERROR_LAUNCHED_CHILD_PROCESS) { + return aRv; + } +#ifdef MOZ_BACKGROUNDTASKS + // A background task that fails to lock its profile will return + // NS_ERROR_UNEXPECTED and this will allow the task to exit with a + // non-zero exit code. + if (aRv == NS_ERROR_UNEXPECTED && BackgroundTasks::IsBackgroundTaskMode()) { + return aRv; + } +#endif + return NS_ERROR_ABORT; + } + + nsresult mRv; +}; + +} // namespace + +static nsresult ProfileMissingDialog(nsINativeAppSupport* aNative) { +#ifdef MOZ_WIDGET_ANDROID + // We cannot really do anything this early during initialization, so we just + // return as this is likely the effect of misconfiguration on the test side. + // Non-test code paths cannot set the profile folder, which is always the + // default one. + Output(true, "Could not find profile folder.\n"); + return NS_ERROR_ABORT; +#else +# ifdef MOZ_BACKGROUNDTASKS + if (BackgroundTasks::IsBackgroundTaskMode()) { + // We should never get to this point in background task mode. + Output(false, + "Could not determine any profile running in backgroundtask mode!\n"); + return NS_ERROR_ABORT; + } +# endif // MOZ_BACKGROUNDTASKS + + nsresult rv; + + ScopedXPCOMStartup xpcom; + rv = xpcom.Initialize(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = xpcom.SetWindowCreator(aNative); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + { // extra scoping is needed so we release these components before xpcom + // shutdown + nsCOMPtr<nsIStringBundleService> sbs = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(sbs, NS_ERROR_FAILURE); + + nsCOMPtr<nsIStringBundle> sb; + sbs->CreateBundle(kProfileProperties, getter_AddRefs(sb)); + NS_ENSURE_TRUE_LOG(sbs, NS_ERROR_FAILURE); + + NS_ConvertUTF8toUTF16 appName(gAppData->name); + AutoTArray<nsString, 2> params = {appName, appName}; + + // profileMissing + nsAutoString missingMessage; + rv = sb->FormatStringFromName("profileMissing", params, missingMessage); + NS_ENSURE_SUCCESS(rv, NS_ERROR_ABORT); + + nsAutoString missingTitle; + params.SetLength(1); + rv = sb->FormatStringFromName("profileMissingTitle", params, missingTitle); + NS_ENSURE_SUCCESS(rv, NS_ERROR_ABORT); + + nsCOMPtr<nsIPromptService> ps(do_GetService(NS_PROMPTSERVICE_CONTRACTID)); + NS_ENSURE_TRUE(ps, NS_ERROR_FAILURE); + + ps->Alert(nullptr, missingTitle.get(), missingMessage.get()); + + return NS_ERROR_ABORT; + } +#endif // MOZ_WIDGET_ANDROID +} + +static ReturnAbortOnError ProfileLockedDialog(nsIFile* aProfileDir, + nsIFile* aProfileLocalDir, + nsIProfileUnlocker* aUnlocker, + nsINativeAppSupport* aNative, + nsIProfileLock** aResult) { + nsresult rv; + + bool exists; + aProfileDir->Exists(&exists); + if (!exists) { + return ProfileMissingDialog(aNative); + } + + ScopedXPCOMStartup xpcom; + rv = xpcom.Initialize(); + NS_ENSURE_SUCCESS(rv, rv); + +#if defined(MOZ_TELEMETRY_REPORTING) + // We cannot check if telemetry has been disabled by the user, yet. + // So, rely on the build time settings, instead. + mozilla::Telemetry::WriteFailedProfileLock(aProfileDir); +#endif + + rv = xpcom.SetWindowCreator(aNative); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + { // extra scoping is needed so we release these components before xpcom + // shutdown + nsCOMPtr<nsIStringBundleService> sbs = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(sbs, NS_ERROR_FAILURE); + + nsCOMPtr<nsIStringBundle> sb; + sbs->CreateBundle(kProfileProperties, getter_AddRefs(sb)); + NS_ENSURE_TRUE_LOG(sbs, NS_ERROR_FAILURE); + + NS_ConvertUTF8toUTF16 appName(gAppData->name); + AutoTArray<nsString, 3> params = {appName, appName, appName}; + + nsAutoString killMessage; +#ifndef XP_MACOSX + rv = sb->FormatStringFromName( + aUnlocker ? "restartMessageUnlocker" : "restartMessageNoUnlocker2", + params, killMessage); +#else + rv = sb->FormatStringFromName( + aUnlocker ? "restartMessageUnlockerMac" : "restartMessageNoUnlockerMac", + params, killMessage); +#endif + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + params.SetLength(1); + nsAutoString killTitle; + rv = sb->FormatStringFromName("restartTitle", params, killTitle); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + +#ifdef MOZ_BACKGROUNDTASKS + if (BackgroundTasks::IsBackgroundTaskMode()) { + // This error is handled specially to exit with a non-zero exit code. + printf_stderr("%s\n", NS_LossyConvertUTF16toASCII(killMessage).get()); + return NS_ERROR_UNEXPECTED; + } +#endif + + if (gfxPlatform::IsHeadless()) { + // TODO: make a way to turn off all dialogs when headless. + Output(true, "%s\n", NS_LossyConvertUTF16toASCII(killMessage).get()); + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIPromptService> ps(do_GetService(NS_PROMPTSERVICE_CONTRACTID)); + NS_ENSURE_TRUE(ps, NS_ERROR_FAILURE); + + if (aUnlocker) { + int32_t button; +#ifdef MOZ_WIDGET_ANDROID + // On Android we always kill the process if the lock is still being held + button = 0; +#else + const uint32_t flags = (nsIPromptService::BUTTON_TITLE_IS_STRING * + nsIPromptService::BUTTON_POS_0) + + (nsIPromptService::BUTTON_TITLE_CANCEL * + nsIPromptService::BUTTON_POS_1); + + bool checkState = false; + rv = ps->ConfirmEx(nullptr, killTitle.get(), killMessage.get(), flags, + killTitle.get(), nullptr, nullptr, nullptr, + &checkState, &button); + NS_ENSURE_SUCCESS_LOG(rv, rv); +#endif + + if (button == 0) { + rv = aUnlocker->Unlock(nsIProfileUnlocker::FORCE_QUIT); + if (NS_FAILED(rv)) { + return rv; + } + + SaveFileToEnv("XRE_PROFILE_PATH", aProfileDir); + SaveFileToEnv("XRE_PROFILE_LOCAL_PATH", aProfileLocalDir); + +#if defined(MOZ_HAS_REMOTE) + if (gRemoteService) { + gRemoteService->UnlockStartup(); + gRemoteService = nullptr; + } +#endif + return LaunchChild(false, true); + } + } else { + rv = ps->Alert(nullptr, killTitle.get(), killMessage.get()); + NS_ENSURE_SUCCESS_LOG(rv, rv); + } + + return NS_ERROR_ABORT; + } +} + +static const char kProfileManagerURL[] = + "chrome://mozapps/content/profile/profileSelection.xhtml"; + +static ReturnAbortOnError ShowProfileManager( + nsIToolkitProfileService* aProfileSvc, nsINativeAppSupport* aNative) { + nsresult rv; + + nsCOMPtr<nsIFile> profD, profLD; + bool offline = false; + int32_t dialogReturn; + + { + ScopedXPCOMStartup xpcom; + rv = xpcom.Initialize(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = xpcom.SetWindowCreator(aNative); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + +#ifdef XP_MACOSX + CommandLineServiceMac::SetupMacCommandLine(gRestartArgc, gRestartArgv, + true); +#endif + + { // extra scoping is needed so we release these components before xpcom + // shutdown + nsCOMPtr<nsIWindowWatcher> windowWatcher( + do_GetService(NS_WINDOWWATCHER_CONTRACTID)); + nsCOMPtr<nsIDialogParamBlock> ioParamBlock( + do_CreateInstance(NS_DIALOGPARAMBLOCK_CONTRACTID)); + nsCOMPtr<nsIMutableArray> dlgArray( + do_CreateInstance(NS_ARRAY_CONTRACTID)); + NS_ENSURE_TRUE(windowWatcher && ioParamBlock && dlgArray, + NS_ERROR_FAILURE); + + ioParamBlock->SetObjects(dlgArray); + + nsCOMPtr<nsIAppStartup> appStartup(components::AppStartup::Service()); + NS_ENSURE_TRUE(appStartup, NS_ERROR_FAILURE); + + nsAutoCString features("centerscreen,chrome,modal,titlebar"); + // If we're launching a private browsing window make sure to set that + // feature for the Profile Manager window as well, so it groups correctly + // on the Windows taskbar. + if (CheckArgExists("private-window") == ARG_FOUND) { + features.AppendLiteral(",private"); + } + nsCOMPtr<mozIDOMWindowProxy> newWindow; + rv = windowWatcher->OpenWindow( + nullptr, nsDependentCString(kProfileManagerURL), "_blank"_ns, + features, ioParamBlock, getter_AddRefs(newWindow)); + + NS_ENSURE_SUCCESS_LOG(rv, rv); + + rv = ioParamBlock->GetInt(0, &dialogReturn); + if (NS_FAILED(rv) || dialogReturn == nsIToolkitProfileService::exit) { + return NS_ERROR_ABORT; + } + + int32_t startOffline; + rv = ioParamBlock->GetInt(1, &startOffline); + offline = NS_SUCCEEDED(rv) && startOffline == 1; + + rv = dlgArray->QueryElementAt(0, NS_GET_IID(nsIFile), + getter_AddRefs(profD)); + NS_ENSURE_SUCCESS_LOG(rv, rv); + + rv = dlgArray->QueryElementAt(1, NS_GET_IID(nsIFile), + getter_AddRefs(profLD)); + NS_ENSURE_SUCCESS_LOG(rv, rv); + } + } + + if (offline) { + SaveToEnv("XRE_START_OFFLINE=1"); + } + + // User requested that we restart back into the profile manager. + if (dialogReturn == nsIToolkitProfileService::restart) { + SaveToEnv("XRE_RESTART_TO_PROFILE_MANAGER=1"); + SaveToEnv("XRE_RESTARTED_BY_PROFILE_MANAGER=1"); + } else { + MOZ_ASSERT(dialogReturn == nsIToolkitProfileService::launchWithProfile); + SaveFileToEnv("XRE_PROFILE_PATH", profD); + SaveFileToEnv("XRE_PROFILE_LOCAL_PATH", profLD); + SaveToEnv("XRE_RESTARTED_BY_PROFILE_MANAGER=1"); + } + + if (gRestartedByOS) { + // Re-add this argument when actually starting the application. + char** newArgv = + (char**)realloc(gRestartArgv, sizeof(char*) * (gRestartArgc + 2)); + NS_ENSURE_TRUE(newArgv, NS_ERROR_OUT_OF_MEMORY); + gRestartArgv = newArgv; + gRestartArgv[gRestartArgc++] = const_cast<char*>("-os-restarted"); + gRestartArgv[gRestartArgc] = nullptr; + } +#if defined(MOZ_HAS_REMOTE) + if (gRemoteService) { + gRemoteService->UnlockStartup(); + gRemoteService = nullptr; + } +#endif + return LaunchChild(false, true); +} + +static bool gDoMigration = false; +static bool gDoProfileReset = false; +static nsCOMPtr<nsIToolkitProfile> gResetOldProfile; + +static nsresult LockProfile(nsINativeAppSupport* aNative, nsIFile* aRootDir, + nsIFile* aLocalDir, nsIToolkitProfile* aProfile, + nsIProfileLock** aResult) { + // If you close Firefox and very quickly reopen it, the old Firefox may + // still be closing down. Rather than immediately showing the + // "Firefox is running but is not responding" message, we spend a few + // seconds retrying first. + + static const int kLockRetrySeconds = 5; + static const int kLockRetrySleepMS = 100; + + nsresult rv; + nsCOMPtr<nsIProfileUnlocker> unlocker; + const TimeStamp start = TimeStamp::Now(); + do { + if (aProfile) { + rv = aProfile->Lock(getter_AddRefs(unlocker), aResult); + } else { + rv = NS_LockProfilePath(aRootDir, aLocalDir, getter_AddRefs(unlocker), + aResult); + } + if (NS_SUCCEEDED(rv)) { + StartupTimeline::Record(StartupTimeline::AFTER_PROFILE_LOCKED); + return NS_OK; + } + PR_Sleep(kLockRetrySleepMS); + } while (TimeStamp::Now() - start < + TimeDuration::FromSeconds(kLockRetrySeconds)); + + return ProfileLockedDialog(aRootDir, aLocalDir, unlocker, aNative, aResult); +} + +// Pick a profile. We need to end up with a profile root dir, local dir and +// potentially an nsIToolkitProfile instance. +// +// 1) check for --profile <path> +// 2) check for -P <name> +// 3) check for --ProfileManager +// 4) use the default profile, if there is one +// 5) if there are *no* profiles, set up profile-migration +// 6) display the profile-manager UI +static nsresult SelectProfile(nsToolkitProfileService* aProfileSvc, + nsINativeAppSupport* aNative, nsIFile** aRootDir, + nsIFile** aLocalDir, nsIToolkitProfile** aProfile, + bool* aWasDefaultSelection) { + StartupTimeline::Record(StartupTimeline::SELECT_PROFILE); + + nsresult rv; + + if (EnvHasValue("MOZ_RESET_PROFILE_RESTART")) { + gDoProfileReset = true; + gDoMigration = true; + } + + // reset-profile and migration args need to be checked before any profiles are + // chosen below. + ArgResult ar = CheckArg("reset-profile"); + if (ar == ARG_FOUND) { + gDoProfileReset = true; + } + + ar = CheckArg("migration"); + if (ar == ARG_FOUND) { + gDoMigration = true; + } + +#if defined(XP_WIN) + // This arg is only used to indicate to telemetry that a profile refresh + // (reset+migration) was requested from the uninstaller, pass this along + // via an environment variable for simplicity. + ar = CheckArg("uninstaller-profile-refresh"); + if (ar == ARG_FOUND) { + SaveToEnv("MOZ_UNINSTALLER_PROFILE_REFRESH=1"); + } +#endif + + if (EnvHasValue("XRE_RESTART_TO_PROFILE_MANAGER")) { + return ShowProfileManager(aProfileSvc, aNative); + } + + // Ask the profile manager to select the profile directories to use. + bool didCreate = false; + rv = aProfileSvc->SelectStartupProfile(&gArgc, gArgv, gDoProfileReset, + aRootDir, aLocalDir, aProfile, + &didCreate, aWasDefaultSelection); + + if (rv == NS_ERROR_SHOW_PROFILE_MANAGER) { + return ShowProfileManager(aProfileSvc, aNative); + } + + NS_ENSURE_SUCCESS(rv, rv); + + if (didCreate) { + // For a fresh install, we would like to let users decide + // to do profile migration on their own later after using. + gDoProfileReset = false; + gDoMigration = false; + } + + if (gDoProfileReset && !*aProfile) { + NS_WARNING("Profile reset is only supported for named profiles."); + return NS_ERROR_ABORT; + } + + // No profile could be found. This generally shouldn't happen, a new profile + // should be created in all cases except for profile reset which is covered + // above, but just in case... + if (!*aRootDir) { + NS_WARNING("Failed to select or create profile."); + return NS_ERROR_ABORT; + } + + return NS_OK; +} + +#ifdef MOZ_BLOCK_PROFILE_DOWNGRADE +struct FileWriteFunc final : public JSONWriteFunc { + FILE* mFile; + explicit FileWriteFunc(FILE* aFile) : mFile(aFile) {} + + void Write(const Span<const char>& aStr) final { + fprintf(mFile, "%.*s", int(aStr.size()), aStr.data()); + } +}; + +static void SubmitDowngradeTelemetry(const nsCString& aLastVersion, + bool aHasSync, int32_t aButton) { + nsCOMPtr<nsIPrefService> prefSvc = + do_GetService("@mozilla.org/preferences-service;1"); + NS_ENSURE_TRUE_VOID(prefSvc); + + nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(prefSvc); + NS_ENSURE_TRUE_VOID(prefBranch); + + bool enabled; + nsresult rv = + prefBranch->GetBoolPref(kPrefHealthReportUploadEnabled, &enabled); + NS_ENSURE_SUCCESS_VOID(rv); + if (!enabled) { + return; + } + + nsCString server; + rv = prefBranch->GetCharPref("toolkit.telemetry.server", server); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCString clientId; + rv = prefBranch->GetCharPref("toolkit.telemetry.cachedClientID", clientId); + NS_ENSURE_SUCCESS_VOID(rv); + + rv = prefSvc->GetDefaultBranch(nullptr, getter_AddRefs(prefBranch)); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCString channel("default"); + rv = prefBranch->GetCharPref("app.update.channel", channel); + NS_ENSURE_SUCCESS_VOID(rv); + + nsID uuid; + rv = nsID::GenerateUUIDInPlace(uuid); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCString arch("null"); + nsCOMPtr<nsIPropertyBag2> sysInfo = + do_GetService("@mozilla.org/system-info;1"); + NS_ENSURE_TRUE_VOID(sysInfo); + sysInfo->GetPropertyAsACString(u"arch"_ns, arch); + + time_t now; + time(&now); + char date[sizeof "YYYY-MM-DDThh:mm:ss.000Z"]; + strftime(date, sizeof date, "%FT%T.000Z", gmtime(&now)); + + NSID_TrimBracketsASCII pingId(uuid); + constexpr auto pingType = "downgrade"_ns; + + int32_t pos = aLastVersion.Find("_"); + if (pos == kNotFound) { + return; + } + + const nsDependentCSubstring lastVersion = Substring(aLastVersion, 0, pos); + const nsDependentCSubstring lastBuildId = + Substring(aLastVersion, pos + 1, 14); + + nsPrintfCString url("%s/submit/telemetry/%s/%s/%s/%s/%s/%s?v=%d", + server.get(), PromiseFlatCString(pingId).get(), + pingType.get(), (const char*)gAppData->name, + (const char*)gAppData->version, channel.get(), + (const char*)gAppData->buildID, + TELEMETRY_PING_FORMAT_VERSION); + + nsCOMPtr<nsIFile> pingFile; + rv = NS_GetSpecialDirectory(XRE_USER_APP_DATA_DIR, getter_AddRefs(pingFile)); + NS_ENSURE_SUCCESS_VOID(rv); + rv = pingFile->Append(u"Pending Pings"_ns); + NS_ENSURE_SUCCESS_VOID(rv); + rv = pingFile->Create(nsIFile::DIRECTORY_TYPE, 0755); + if (NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS) { + return; + } + rv = pingFile->Append(NS_ConvertUTF8toUTF16(pingId)); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr<nsIFile> pingSender; + rv = NS_GetSpecialDirectory(NS_GRE_BIN_DIR, getter_AddRefs(pingSender)); + NS_ENSURE_SUCCESS_VOID(rv); +# ifdef XP_WIN + pingSender->Append(u"pingsender.exe"_ns); +# else + pingSender->Append(u"pingsender"_ns); +# endif + + bool exists; + rv = pingSender->Exists(&exists); + NS_ENSURE_SUCCESS_VOID(rv); + if (!exists) { + return; + } + + FILE* file; + rv = pingFile->OpenANSIFileDesc("w", &file); + NS_ENSURE_SUCCESS_VOID(rv); + + JSONWriter w(MakeUnique<FileWriteFunc>(file)); + w.Start(); + { + w.StringProperty("type", + Span<const char>(pingType.Data(), pingType.Length())); + w.StringProperty("id", PromiseFlatCString(pingId)); + w.StringProperty("creationDate", MakeStringSpan(date)); + w.IntProperty("version", TELEMETRY_PING_FORMAT_VERSION); + w.StringProperty("clientId", clientId); + w.StartObjectProperty("application"); + { + w.StringProperty("architecture", arch); + w.StringProperty( + "buildId", + MakeStringSpan(static_cast<const char*>(gAppData->buildID))); + w.StringProperty( + "name", MakeStringSpan(static_cast<const char*>(gAppData->name))); + w.StringProperty( + "version", + MakeStringSpan(static_cast<const char*>(gAppData->version))); + w.StringProperty("displayVersion", + MOZ_STRINGIFY(MOZ_APP_VERSION_DISPLAY)); + w.StringProperty( + "vendor", MakeStringSpan(static_cast<const char*>(gAppData->vendor))); + w.StringProperty("platformVersion", gToolkitVersion); +# ifdef TARGET_XPCOM_ABI + w.StringProperty("xpcomAbi", TARGET_XPCOM_ABI); +# else + w.StringProperty("xpcomAbi", "unknown"); +# endif + w.StringProperty("channel", channel); + } + w.EndObject(); + w.StartObjectProperty("payload"); + { + w.StringProperty("lastVersion", PromiseFlatCString(lastVersion)); + w.StringProperty("lastBuildId", PromiseFlatCString(lastBuildId)); + w.BoolProperty("hasSync", aHasSync); + w.IntProperty("button", aButton); + } + w.EndObject(); + } + w.End(); + + fclose(file); + + PathString filePath = pingFile->NativePath(); + const filesystem::Path::value_type* args[2]; +# ifdef XP_WIN + nsString urlw = NS_ConvertUTF8toUTF16(url); + args[0] = urlw.get(); +# else + args[0] = url.get(); +# endif + args[1] = filePath.get(); + + nsCOMPtr<nsIProcess> process = + do_CreateInstance("@mozilla.org/process/util;1"); + NS_ENSURE_TRUE_VOID(process); + process->Init(pingSender); + process->SetStartHidden(true); + process->SetNoShell(true); + +# ifdef XP_WIN + process->Runw(false, args, 2); +# else + process->Run(false, args, 2); +# endif +} + +static const char kProfileDowngradeURL[] = + "chrome://mozapps/content/profile/profileDowngrade.xhtml"; + +static ReturnAbortOnError CheckDowngrade(nsIFile* aProfileDir, + nsINativeAppSupport* aNative, + nsIToolkitProfileService* aProfileSvc, + const nsCString& aLastVersion) { + int32_t result = 0; + nsresult rv; + + { + if (gfxPlatform::IsHeadless()) { + // TODO: make a way to turn off all dialogs when headless. + Output(true, + "This profile was last used with a newer version of this " + "application. Please create a new profile.\n"); + return NS_ERROR_ABORT; + } + + ScopedXPCOMStartup xpcom; + rv = xpcom.Initialize(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = xpcom.SetWindowCreator(aNative); + NS_ENSURE_SUCCESS(rv, rv); + + { // extra scoping is needed so we release these components before xpcom + // shutdown + bool hasSync = false; + nsCOMPtr<nsIPrefService> prefSvc = + do_GetService("@mozilla.org/preferences-service;1"); + NS_ENSURE_TRUE(prefSvc, rv); + + nsCOMPtr<nsIFile> prefsFile; + rv = aProfileDir->Clone(getter_AddRefs(prefsFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = prefsFile->Append(u"prefs.js"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + rv = prefSvc->ReadUserPrefsFromFile(prefsFile); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(prefSvc); + + rv = prefBranch->PrefHasUserValue("services.sync.username", &hasSync); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIWindowWatcher> windowWatcher = + do_GetService(NS_WINDOWWATCHER_CONTRACTID); + NS_ENSURE_TRUE(windowWatcher, NS_ERROR_ABORT); + + nsCOMPtr<nsIAppStartup> appStartup(components::AppStartup::Service()); + NS_ENSURE_TRUE(appStartup, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDialogParamBlock> paramBlock = + do_CreateInstance(NS_DIALOGPARAMBLOCK_CONTRACTID); + NS_ENSURE_TRUE(paramBlock, NS_ERROR_ABORT); + + uint8_t flags = 0; + if (hasSync) { + flags |= nsIToolkitProfileService::hasSync; + } + + paramBlock->SetInt(0, flags); + + nsAutoCString features("centerscreen,chrome,modal,titlebar"); + // If we're launching a private browsing window make sure to set that + // feature for the Profile Manager window as well, so it groups correctly + // on the Windows taskbar. + if (CheckArgExists("private-window") == ARG_FOUND) { + features.AppendLiteral(",private"); + } + nsCOMPtr<mozIDOMWindowProxy> newWindow; + rv = windowWatcher->OpenWindow( + nullptr, nsDependentCString(kProfileDowngradeURL), "_blank"_ns, + features, paramBlock, getter_AddRefs(newWindow)); + NS_ENSURE_SUCCESS(rv, rv); + + paramBlock->GetInt(1, &result); + + SubmitDowngradeTelemetry(aLastVersion, hasSync, result); + } + } + + if (result == nsIToolkitProfileService::createNewProfile) { + // Create a new profile and start it. + nsCString profileName; + profileName.AssignLiteral("default"); +# ifdef MOZ_DEDICATED_PROFILES + profileName.Append("-" MOZ_STRINGIFY(MOZ_UPDATE_CHANNEL)); +# endif + nsCOMPtr<nsIToolkitProfile> newProfile; + rv = aProfileSvc->CreateUniqueProfile(nullptr, profileName, + getter_AddRefs(newProfile)); + NS_ENSURE_SUCCESS(rv, rv); + rv = aProfileSvc->SetDefaultProfile(newProfile); + NS_ENSURE_SUCCESS(rv, rv); + rv = aProfileSvc->Flush(); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> profD, profLD; + rv = newProfile->GetRootDir(getter_AddRefs(profD)); + NS_ENSURE_SUCCESS(rv, rv); + rv = newProfile->GetLocalDir(getter_AddRefs(profLD)); + NS_ENSURE_SUCCESS(rv, rv); + + SaveFileToEnv("XRE_PROFILE_PATH", profD); + SaveFileToEnv("XRE_PROFILE_LOCAL_PATH", profLD); + + return LaunchChild(false, true); + } + + // Cancel + return NS_ERROR_ABORT; +} +#endif + +/** + * Extracts the various parts of a compatibility version string. + * + * Compatibility versions are of the form + * "<appversion>_<appbuildid>/<platformbuildid>". The toolkit version comparator + * can only handle 32-bit numbers and in the normal case build IDs are larger + * than this. So if the build ID is numeric we split it into two version parts. + */ +static void ExtractCompatVersionInfo(const nsACString& aCompatVersion, + nsACString& aAppVersion, + nsACString& aAppBuildID) { + int32_t underscorePos = aCompatVersion.FindChar('_'); + int32_t slashPos = aCompatVersion.FindChar('/'); + + if (underscorePos == kNotFound || slashPos == kNotFound || + slashPos < underscorePos) { + NS_WARNING( + "compatibility.ini Version string does not match the expected format."); + + // Fall back to just using the entire string as the version. + aAppVersion = aCompatVersion; + aAppBuildID.Truncate(0); + return; + } + + aAppVersion = Substring(aCompatVersion, 0, underscorePos); + aAppBuildID = Substring(aCompatVersion, underscorePos + 1, + slashPos - (underscorePos + 1)); +} + +/** + * Compares the provided compatibility versions. Returns 0 if they match, + * < 0 if the new version is considered an upgrade from the old version and + * > 0 if the new version is considered a downgrade from the old version. + */ +int32_t CompareCompatVersions(const nsACString& aOldCompatVersion, + const nsACString& aNewCompatVersion) { + // Hardcode the case where the last run was in safe mode (Bug 1556612). We + // cannot tell if this is a downgrade or not so just assume it isn't and let + // the user proceed. + if (aOldCompatVersion.EqualsLiteral("Safe Mode")) { + return -1; + } + + // Extract the major version part from the version string and only use that + // for version comparison. + int32_t index = aOldCompatVersion.FindChar('.'); + const nsACString& oldMajorVersion = Substring( + aOldCompatVersion, 0, index < 0 ? aOldCompatVersion.Length() : index); + index = aNewCompatVersion.FindChar('.'); + const nsACString& newMajorVersion = Substring( + aNewCompatVersion, 0, index < 0 ? aNewCompatVersion.Length() : index); + + return CompareVersions(PromiseFlatCString(oldMajorVersion).get(), + PromiseFlatCString(newMajorVersion).get()); +} + +/** + * Checks the compatibility.ini file to see if we have updated our application + * or otherwise invalidated our caches. If the application has been updated, + * we return false; otherwise, we return true. + * + * We also write the status of the caches (valid/invalid) into the return param + * aCachesOK. The aCachesOK is always invalid if the application has been + * updated. + * + * Finally, aIsDowngrade is set to true if the current application is older + * than that previously used by the profile. + */ +static bool CheckCompatibility(nsIFile* aProfileDir, const nsCString& aVersion, + const nsCString& aOSABI, nsIFile* aXULRunnerDir, + nsIFile* aAppDir, nsIFile* aFlagFile, + bool* aCachesOK, bool* aIsDowngrade, + nsCString& aLastVersion) { + *aCachesOK = false; + *aIsDowngrade = false; + gLastAppVersion.SetIsVoid(true); + gLastAppBuildID.SetIsVoid(true); + + nsCOMPtr<nsIFile> file; + aProfileDir->Clone(getter_AddRefs(file)); + if (!file) return false; + file->AppendNative(FILE_COMPATIBILITY_INFO); + + nsINIParser parser; + nsresult rv = parser.Init(file); + if (NS_FAILED(rv)) return false; + + rv = parser.GetString("Compatibility", "LastVersion", aLastVersion); + if (NS_FAILED(rv)) { + return false; + } + + if (!aLastVersion.Equals(aVersion)) { + // The version is not the same. Whether it's a downgrade depends on an + // actual comparison: + *aIsDowngrade = 0 < CompareCompatVersions(aLastVersion, aVersion); + ExtractCompatVersionInfo(aLastVersion, gLastAppVersion, gLastAppBuildID); + return false; + } + + // If we get here, the version matched, but there may still be other + // differences between us and the build that the profile last ran under. + + gLastAppVersion.Assign(gAppData->version); + gLastAppBuildID.Assign(gAppData->buildID); + + nsAutoCString buf; + rv = parser.GetString("Compatibility", "LastOSABI", buf); + if (NS_FAILED(rv) || !aOSABI.Equals(buf)) return false; + + rv = parser.GetString("Compatibility", "LastPlatformDir", buf); + if (NS_FAILED(rv)) return false; + + nsCOMPtr<nsIFile> lf; + rv = NS_NewNativeLocalFile(""_ns, false, getter_AddRefs(lf)); + if (NS_FAILED(rv)) return false; + + rv = lf->SetPersistentDescriptor(buf); + if (NS_FAILED(rv)) return false; + + bool eq; + rv = lf->Equals(aXULRunnerDir, &eq); + if (NS_FAILED(rv) || !eq) return false; + + if (aAppDir) { + rv = parser.GetString("Compatibility", "LastAppDir", buf); + if (NS_FAILED(rv)) return false; + + rv = NS_NewNativeLocalFile(""_ns, false, getter_AddRefs(lf)); + if (NS_FAILED(rv)) return false; + + rv = lf->SetPersistentDescriptor(buf); + if (NS_FAILED(rv)) return false; + + rv = lf->Equals(aAppDir, &eq); + if (NS_FAILED(rv) || !eq) return false; + } + + // If we see this flag, caches are invalid. + rv = parser.GetString("Compatibility", "InvalidateCaches", buf); + *aCachesOK = (NS_FAILED(rv) || !buf.EqualsLiteral("1")); + + bool purgeCaches = false; + if (aFlagFile && NS_SUCCEEDED(aFlagFile->Exists(&purgeCaches)) && + purgeCaches) { + *aCachesOK = false; + } + + return true; +} + +void BuildCompatVersion(const char* aAppVersion, const char* aAppBuildID, + const char* aToolkitBuildID, nsACString& aBuf) { + aBuf.Assign(aAppVersion); + aBuf.Append('_'); + aBuf.Append(aAppBuildID); + aBuf.Append('/'); + aBuf.Append(aToolkitBuildID); +} + +static void BuildVersion(nsCString& aBuf) { + BuildCompatVersion(gAppData->version, gAppData->buildID, gToolkitBuildID, + aBuf); +} + +static void WriteVersion(nsIFile* aProfileDir, const nsCString& aVersion, + const nsCString& aOSABI, nsIFile* aXULRunnerDir, + nsIFile* aAppDir, bool invalidateCache) { + nsCOMPtr<nsIFile> file; + aProfileDir->Clone(getter_AddRefs(file)); + if (!file) return; + file->AppendNative(FILE_COMPATIBILITY_INFO); + + nsAutoCString platformDir; + Unused << aXULRunnerDir->GetPersistentDescriptor(platformDir); + + nsAutoCString appDir; + if (aAppDir) Unused << aAppDir->GetPersistentDescriptor(appDir); + + PRFileDesc* fd; + nsresult rv = file->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, + 0600, &fd); + if (NS_FAILED(rv)) { + NS_ERROR("could not create output stream"); + return; + } + + static const char kHeader[] = "[Compatibility]" NS_LINEBREAK "LastVersion="; + + PR_Write(fd, kHeader, sizeof(kHeader) - 1); + PR_Write(fd, aVersion.get(), aVersion.Length()); + + static const char kOSABIHeader[] = NS_LINEBREAK "LastOSABI="; + PR_Write(fd, kOSABIHeader, sizeof(kOSABIHeader) - 1); + PR_Write(fd, aOSABI.get(), aOSABI.Length()); + + static const char kPlatformDirHeader[] = NS_LINEBREAK "LastPlatformDir="; + + PR_Write(fd, kPlatformDirHeader, sizeof(kPlatformDirHeader) - 1); + PR_Write(fd, platformDir.get(), platformDir.Length()); + + static const char kAppDirHeader[] = NS_LINEBREAK "LastAppDir="; + if (aAppDir) { + PR_Write(fd, kAppDirHeader, sizeof(kAppDirHeader) - 1); + PR_Write(fd, appDir.get(), appDir.Length()); + } + + static const char kInvalidationHeader[] = NS_LINEBREAK "InvalidateCaches=1"; + if (invalidateCache) + PR_Write(fd, kInvalidationHeader, sizeof(kInvalidationHeader) - 1); + + static const char kNL[] = NS_LINEBREAK; + PR_Write(fd, kNL, sizeof(kNL) - 1); + + PR_Close(fd); +} + +/** + * Returns true if the startup cache file was successfully removed. + * Returns false if file->Clone fails at any point (OOM) or if unable + * to remove the startup cache file. Note in particular the return value + * is unaffected by a failure to remove extensions.ini + */ +static bool RemoveComponentRegistries(nsIFile* aProfileDir, + nsIFile* aLocalProfileDir, + bool aRemoveEMFiles) { + nsCOMPtr<nsIFile> file; + aProfileDir->Clone(getter_AddRefs(file)); + if (!file) return false; + + if (aRemoveEMFiles) { + file->SetNativeLeafName("extensions.ini"_ns); + file->Remove(false); + } + + aLocalProfileDir->Clone(getter_AddRefs(file)); + if (!file) return false; + + file->AppendNative("startupCache"_ns); + nsresult rv = file->Remove(true); + return NS_SUCCEEDED(rv) || rv == NS_ERROR_FILE_NOT_FOUND; +} + +// When we first initialize the crash reporter we don't have a profile, +// so we set the minidump path to $TEMP. Once we have a profile, +// we set it to $PROFILE/minidumps, creating the directory +// if needed. +static void MakeOrSetMinidumpPath(nsIFile* profD) { + nsCOMPtr<nsIFile> dumpD; + profD->Clone(getter_AddRefs(dumpD)); + + if (dumpD) { + bool fileExists; + // XXX: do some more error checking here + dumpD->Append(u"minidumps"_ns); + dumpD->Exists(&fileExists); + if (!fileExists) { + nsresult rv = dumpD->Create(nsIFile::DIRECTORY_TYPE, 0700); + NS_ENSURE_SUCCESS_VOID(rv); + } + + nsAutoString pathStr; + if (NS_SUCCEEDED(dumpD->GetPath(pathStr))) + CrashReporter::SetMinidumpPath(pathStr); + } +} + +const XREAppData* gAppData = nullptr; + +/** + * NSPR will search for the "nspr_use_zone_allocator" symbol throughout + * the process and use it to determine whether the application defines its own + * memory allocator or not. + * + * Since most applications (e.g. Firefox and Thunderbird) don't use any special + * allocators and therefore don't define this symbol, NSPR must search the + * entire process, which reduces startup performance. + * + * By defining the symbol here, we can avoid the wasted lookup and hopefully + * improve startup performance. + */ +NS_VISIBILITY_DEFAULT PRBool nspr_use_zone_allocator = PR_FALSE; + +#ifdef CAIRO_HAS_DWRITE_FONT + +# include <dwrite.h> +# include "nsWindowsHelpers.h" + +# ifdef DEBUG_DWRITE_STARTUP + +# define LOGREGISTRY(msg) LogRegistryEvent(msg) + +// for use when monitoring process +static void LogRegistryEvent(const wchar_t* msg) { + HKEY dummyKey; + HRESULT hr; + wchar_t buf[512]; + + wsprintf(buf, L" log %s", msg); + hr = RegOpenKeyEx(HKEY_LOCAL_MACHINE, buf, 0, KEY_READ, &dummyKey); + if (SUCCEEDED(hr)) { + RegCloseKey(dummyKey); + } +} +# else + +# define LOGREGISTRY(msg) + +# endif + +static DWORD WINAPI InitDwriteBG(LPVOID lpdwThreadParam) { + SetThreadPriority(GetCurrentThread(), THREAD_MODE_BACKGROUND_BEGIN); + LOGREGISTRY(L"loading dwrite.dll"); + HMODULE dwdll = LoadLibrarySystem32(L"dwrite.dll"); + if (dwdll) { + decltype(DWriteCreateFactory)* createDWriteFactory = + (decltype(DWriteCreateFactory)*)GetProcAddress(dwdll, + "DWriteCreateFactory"); + if (createDWriteFactory) { + LOGREGISTRY(L"creating dwrite factory"); + IDWriteFactory* factory; + HRESULT hr = createDWriteFactory(DWRITE_FACTORY_TYPE_SHARED, + __uuidof(IDWriteFactory), + reinterpret_cast<IUnknown**>(&factory)); + if (SUCCEEDED(hr)) { + LOGREGISTRY(L"dwrite factory done"); + factory->Release(); + LOGREGISTRY(L"freed factory"); + } else { + LOGREGISTRY(L"failed to create factory"); + } + } + } + SetThreadPriority(GetCurrentThread(), THREAD_MODE_BACKGROUND_END); + return 0; +} +#endif + +#include "GeckoProfiler.h" +#include "ProfilerControl.h" + +// Encapsulates startup and shutdown state for XRE_main +class XREMain { + public: + XREMain() = default; + + ~XREMain() { + mScopedXPCOM = nullptr; + mAppData = nullptr; + } + + int XRE_main(int argc, char* argv[], const BootstrapConfig& aConfig); + int XRE_mainInit(bool* aExitFlag); + int XRE_mainStartup(bool* aExitFlag); + nsresult XRE_mainRun(); + + bool CheckLastStartupWasCrash(); + + nsCOMPtr<nsINativeAppSupport> mNativeApp; + RefPtr<nsToolkitProfileService> mProfileSvc; + nsCOMPtr<nsIFile> mProfD; + nsCOMPtr<nsIFile> mProfLD; + nsCOMPtr<nsIProfileLock> mProfileLock; +#if defined(MOZ_HAS_REMOTE) + RefPtr<nsRemoteService> mRemoteService; +#endif + + UniquePtr<ScopedXPCOMStartup> mScopedXPCOM; + UniquePtr<XREAppData> mAppData; + + nsXREDirProvider mDirProvider; + +#ifdef MOZ_WIDGET_GTK + nsAutoCString mXDGActivationToken; + nsAutoCString mDesktopStartupID; +#endif + + bool mStartOffline = false; +#if defined(MOZ_HAS_REMOTE) + bool mDisableRemoteClient = false; + bool mDisableRemoteServer = false; +#endif +}; + +#if defined(XP_UNIX) && !defined(ANDROID) +static SmprintfPointer FormatUid(uid_t aId) { + if (const auto pw = getpwuid(aId)) { + return mozilla::Smprintf("%s", pw->pw_name); + } + return mozilla::Smprintf("uid %d", static_cast<int>(aId)); +} + +// Bug 1323302: refuse to run under sudo or similar. +static bool CheckForUserMismatch() { + static char const* const kVars[] = { + "HOME", +# ifdef MOZ_WIDGET_GTK + "XDG_RUNTIME_DIR", +# endif +# ifdef MOZ_X11 + "XAUTHORITY", +# endif + }; + + const uid_t euid = geteuid(); + if (euid != 0) { + // On Linux it's possible to have superuser capabilities with a + // nonzero uid, but anyone who knows enough to make that happen + // probably knows enough to debug the resulting problems. + // Otherwise, a non-root user can't cause the problems we're + // concerned about. + return false; + } + + for (const auto var : kVars) { + if (const auto path = PR_GetEnv(var)) { + struct stat st; + if (stat(path, &st) == 0) { + if (st.st_uid != euid) { + const auto owner = FormatUid(st.st_uid); + Output(true, + "Running " MOZ_APP_DISPLAYNAME + " as root in a regular" + " user's session is not supported. ($%s is %s which is" + " owned by %s.)\n", + var, path, owner.get()); + return true; + } + } + } + } + return false; +} +#else // !XP_UNIX || ANDROID +static bool CheckForUserMismatch() { return false; } +#endif + +void mozilla::startup::IncreaseDescriptorLimits() { +#ifdef XP_UNIX + // Increase the fd limit to accomodate IPC resources like shared memory. + // See also the Darwin case in config/external/nspr/pr/moz.build + static const rlim_t kFDs = 4096; + struct rlimit rlim; + + if (getrlimit(RLIMIT_NOFILE, &rlim) != 0) { + Output(false, "getrlimit: %s\n", strerror(errno)); + return; + } + // Don't decrease the limit if it's already high enough, but don't + // try to go over the hard limit. (RLIM_INFINITY isn't required to + // be the numerically largest rlim_t, so don't assume that.) + if (rlim.rlim_cur != RLIM_INFINITY && rlim.rlim_cur < kFDs && + rlim.rlim_cur < rlim.rlim_max) { + if (rlim.rlim_max != RLIM_INFINITY && rlim.rlim_max < kFDs) { + rlim.rlim_cur = rlim.rlim_max; + } else { + rlim.rlim_cur = kFDs; + } + if (setrlimit(RLIMIT_NOFILE, &rlim) != 0) { + Output(false, "setrlimit: %s\n", strerror(errno)); + } + } +#endif +} + +#ifdef XP_WIN + +static uint32_t GetMicrocodeVersionByVendor(HKEY key, DWORD upper, + DWORD lower) { + WCHAR data[13]; // The CPUID vendor string is 12 characters long plus null + DWORD len = sizeof(data); + DWORD vtype; + + if (RegQueryValueExW(key, L"VendorIdentifier", nullptr, &vtype, + reinterpret_cast<LPBYTE>(data), &len) == ERROR_SUCCESS) { + if (wcscmp(L"GenuineIntel", data) == 0) { + // Intel reports the microcode version in the upper 32 bits of the MSR + return upper; + } + + if (wcscmp(L"AuthenticAMD", data) == 0) { + // AMD reports the microcode version in the lower 32 bits of the MSR + return lower; + } + + // Unknown CPU vendor, return whatever half is non-zero + return lower ? lower : upper; + } + + return 0; // No clue +} + +#endif // XP_WIN + +static void MaybeAddCPUMicrocodeCrashAnnotation() { +#ifdef XP_WIN + // Add CPU microcode version to the crash report as "CPUMicrocodeVersion". + // It feels like this code may belong in nsSystemInfo instead. + uint32_t cpuUpdateRevision = 0; + HKEY key; + static const WCHAR keyName[] = + L"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0"; + + if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, keyName, 0, KEY_QUERY_VALUE, &key) == + ERROR_SUCCESS) { + DWORD updateRevision[2]; + DWORD len = sizeof(updateRevision); + DWORD vtype; + + // Windows 7 uses "Update Signature", 8 uses "Update Revision". + // For AMD CPUs, "CurrentPatchLevel" is sometimes used. + // Take the first one we find. + LPCWSTR choices[] = {L"Update Signature", L"Update Revision", + L"CurrentPatchLevel"}; + for (const auto& oneChoice : choices) { + if (RegQueryValueExW(key, oneChoice, nullptr, &vtype, + reinterpret_cast<LPBYTE>(updateRevision), + &len) == ERROR_SUCCESS) { + if (vtype == REG_BINARY && len == sizeof(updateRevision)) { + cpuUpdateRevision = GetMicrocodeVersionByVendor( + key, updateRevision[1], updateRevision[0]); + break; + } + + if (vtype == REG_DWORD && len == sizeof(updateRevision[0])) { + cpuUpdateRevision = static_cast<int>(updateRevision[0]); + break; + } + } + } + } + + if (cpuUpdateRevision > 0) { + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::CPUMicrocodeVersion, + nsPrintfCString("0x%" PRIx32, cpuUpdateRevision)); + } +#endif +} + +#if defined(MOZ_BACKGROUNDTASKS) +static void SetupConsoleForBackgroundTask( + const nsCString& aBackgroundTaskName) { + // We do not suppress output on Windows because: + // 1. Background task subprocesses launched via LaunchApp() does not attach to + // the console. + // 2. Suppressing output intermittently causes failures on when running + // multiple tasks (see bug 1831631) +# ifndef XP_WIN + if (BackgroundTasks::IsNoOutputTaskName(aBackgroundTaskName) && + !CheckArg("attach-console") && + !EnvHasValue("MOZ_BACKGROUNDTASKS_IGNORE_NO_OUTPUT")) { + // Suppress output, somewhat crudely. We need to suppress stderr as well + // as stdout because assertions, of which there are many, write to stderr. + Unused << freopen("/dev/null", "w", stdout); + Unused << freopen("/dev/null", "w", stderr); + return; + } +# endif + printf_stderr("*** You are running in background task mode. ***\n"); +} +#endif + +/* + * XRE_mainInit - Initial setup and command line parameter processing. + * Main() will exit early if either return value != 0 or if aExitFlag is + * true. + */ +int XREMain::XRE_mainInit(bool* aExitFlag) { + if (!aExitFlag) return 1; + *aExitFlag = false; + + atexit(UnexpectedExit); + auto expectedShutdown = mozilla::MakeScopeExit([&] { MozExpectedExit(); }); + + StartupTimeline::Record(StartupTimeline::MAIN); + + if (CheckForUserMismatch()) { + return 1; + } + +#ifdef XP_MACOSX + mozilla::MacAutoreleasePool pool; + + DisableAppNap(); +#endif + +#ifdef MOZ_BACKGROUNDTASKS + Maybe<nsCString> backgroundTask = Nothing(); + const char* backgroundTaskName = nullptr; + if (ARG_FOUND == + CheckArg("backgroundtask", &backgroundTaskName, CheckArgFlag::None)) { + backgroundTask = Some(backgroundTaskName); + + SetupConsoleForBackgroundTask(backgroundTask.ref()); + } + + BackgroundTasks::Init(backgroundTask); +#endif + +#ifndef ANDROID + if (PR_GetEnv("MOZ_RUN_GTEST") +# ifdef FUZZING + || PR_GetEnv("FUZZER") +# endif +# ifdef MOZ_BACKGROUNDTASKS + || BackgroundTasks::IsBackgroundTaskMode() +# endif + ) { + // Enable headless mode and assert that it worked, since gfxPlatform + // uses a static bool set after the first call to `IsHeadless`. + // Note: Android gtests seem to require an Activity and fail to start + // with headless mode enabled. + PR_SetEnv("MOZ_HEADLESS=1"); + MOZ_ASSERT(gfxPlatform::IsHeadless()); + } +#endif // ANDROID + + if (PR_GetEnv("MOZ_CHAOSMODE")) { + ChaosFeature feature = ChaosFeature::Any; + long featureInt = strtol(PR_GetEnv("MOZ_CHAOSMODE"), nullptr, 16); + if (featureInt) { + // NOTE: MOZ_CHAOSMODE=0 or a non-hex value maps to Any feature. + feature = static_cast<ChaosFeature>(featureInt); + } + ChaosMode::SetChaosFeature(feature); + } + + if (CheckArgExists("fxr")) { + gFxREmbedded = true; + } + + if (ChaosMode::isActive(ChaosFeature::Any)) { + printf_stderr( + "*** You are running in chaos test mode. See ChaosMode.h. ***\n"); + } + + if (CheckArg("headless") || CheckArgExists("screenshot")) { + PR_SetEnv("MOZ_HEADLESS=1"); + } + + if (gfxPlatform::IsHeadless()) { +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) || defined(XP_MACOSX) + printf_stderr("*** You are running in headless mode.\n"); +#else + Output( + true, + "Error: headless mode is not currently supported on this platform.\n"); + return 1; +#endif + +#ifdef XP_MACOSX + // To avoid taking focus when running in headless mode immediately + // transition Firefox to a background application. + ProcessSerialNumber psn = {0, kCurrentProcess}; + OSStatus transformStatus = + TransformProcessType(&psn, kProcessTransformToBackgroundApplication); + if (transformStatus != noErr) { + NS_ERROR("Failed to make process a background application."); + return 1; + } +#endif + } + + nsresult rv; + ArgResult ar; + +#ifdef DEBUG + if (PR_GetEnv("XRE_MAIN_BREAK")) NS_BREAK(); +#endif + + mozilla::startup::IncreaseDescriptorLimits(); + + SetupErrorHandling(gArgv[0]); + +#ifdef CAIRO_HAS_DWRITE_FONT + { + // Bug 602792 - when DWriteCreateFactory is called the dwrite client dll + // starts the FntCache service if it isn't already running (it's set + // to manual startup by default in Windows 7 RTM). Subsequent DirectWrite + // calls cause the IDWriteFactory object to communicate with the FntCache + // service with a timeout; if there's no response after the timeout, the + // DirectWrite client library will assume the service isn't around and do + // manual font file I/O on _all_ system fonts. To avoid this, load the + // dwrite library and create a factory as early as possible so that the + // FntCache service is ready by the time it's needed. + + CreateThread(nullptr, 0, &InitDwriteBG, nullptr, 0, nullptr); + } +#endif + +#ifdef XP_UNIX + const char* home = PR_GetEnv("HOME"); + if (!home || !*home) { + struct passwd* pw = getpwuid(geteuid()); + if (!pw || !pw->pw_dir) { + Output(true, "Could not determine HOME directory"); + return 1; + } + SaveWordToEnv("HOME", nsDependentCString(pw->pw_dir)); + } +#endif + +#ifdef MOZ_ACCESSIBILITY_ATK + // Suppress atk-bridge init at startup, until mozilla accessibility is + // initialized. This works after gnome 2.24.2. + SaveToEnv("NO_AT_BRIDGE=1"); +#endif + + // Check for application.ini overrides + const char* override = nullptr; + ar = CheckArg("override", &override); + if (ar == ARG_BAD) { + Output(true, "Incorrect number of arguments passed to --override"); + return 1; + } + if (ar == ARG_FOUND) { + nsCOMPtr<nsIFile> overrideLF; + rv = XRE_GetFileFromPath(override, getter_AddRefs(overrideLF)); + if (NS_FAILED(rv)) { + Output(true, "Error: unrecognized override.ini path.\n"); + return 1; + } + + rv = XRE_ParseAppData(overrideLF, *mAppData); + if (NS_FAILED(rv)) { + Output(true, "Couldn't read override.ini"); + return 1; + } + } + + // Check sanity and correctness of app data. + + if (!mAppData->name) { + Output(true, "Error: App:Name not specified in application.ini\n"); + return 1; + } + if (!mAppData->buildID) { + Output(true, "Error: App:BuildID not specified in application.ini\n"); + return 1; + } + + // XXX Originally ScopedLogging was here? Now it's in XRE_main above + // XRE_mainInit. + + if (!mAppData->minVersion) { + Output(true, "Error: Gecko:MinVersion not specified in application.ini\n"); + return 1; + } + + if (!mAppData->maxVersion) { + // If no maxVersion is specified, we assume the app is only compatible + // with the initial preview release. Do not increment this number ever! + mAppData->maxVersion = "1.*"; + } + + if (mozilla::Version(mAppData->minVersion) > gToolkitVersion || + mozilla::Version(mAppData->maxVersion) < gToolkitVersion) { + Output(true, + "Error: Platform version '%s' is not compatible with\n" + "minVersion >= %s\nmaxVersion <= %s\n", + (const char*)gToolkitVersion, (const char*)mAppData->minVersion, + (const char*)mAppData->maxVersion); + return 1; + } + + rv = mDirProvider.Initialize(mAppData->directory, mAppData->xreDirectory); + if (NS_FAILED(rv)) return 1; + + if (EnvHasValue("MOZ_CRASHREPORTER")) { + mAppData->flags |= NS_XRE_ENABLE_CRASH_REPORTER; + } + + nsCOMPtr<nsIFile> xreBinDirectory; + xreBinDirectory = mDirProvider.GetGREBinDir(); + + if ((mAppData->flags & NS_XRE_ENABLE_CRASH_REPORTER) && + NS_SUCCEEDED(CrashReporter::SetExceptionHandler(xreBinDirectory))) { + nsCOMPtr<nsIFile> file; + rv = nsXREDirProvider::GetUserAppDataDirectory(getter_AddRefs(file)); + if (NS_SUCCEEDED(rv)) { + CrashReporter::SetUserAppDataDirectory(file); + } + if (mAppData->crashReporterURL) + CrashReporter::SetServerURL( + nsDependentCString(mAppData->crashReporterURL)); + + // We overwrite this once we finish starting up. + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::StartupCrash, + true); + + // pass some basic info from the app data + if (mAppData->vendor) + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::Vendor, + nsDependentCString(mAppData->vendor)); + if (mAppData->name) + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::ProductName, + nsDependentCString(mAppData->name)); + if (mAppData->ID) + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::ProductID, + nsDependentCString(mAppData->ID)); + if (mAppData->version) + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::Version, + nsDependentCString(mAppData->version)); + if (mAppData->buildID) + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::BuildID, + nsDependentCString(mAppData->buildID)); + + nsDependentCString releaseChannel(MOZ_STRINGIFY(MOZ_UPDATE_CHANNEL)); + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::ReleaseChannel, releaseChannel); +#ifdef MOZ_LINKER + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::CrashAddressLikelyWrong, + IsSignalHandlingBroken()); +#endif + +#ifdef XP_WIN + nsAutoString appInitDLLs; + if (widget::WinUtils::GetAppInitDLLs(appInitDLLs)) { + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AppInitDLLs, + NS_ConvertUTF16toUTF8(appInitDLLs)); + } + + nsString packageFamilyName = widget::WinUtils::GetPackageFamilyName(); + if (StringBeginsWith(packageFamilyName, u"Mozilla."_ns) || + StringBeginsWith(packageFamilyName, u"MozillaCorporation."_ns)) { + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::WindowsPackageFamilyName, + NS_ConvertUTF16toUTF8(packageFamilyName)); + } +#endif + + bool isBackgroundTaskMode = false; +#ifdef MOZ_BACKGROUNDTASKS + Maybe<nsCString> backgroundTasks = BackgroundTasks::GetBackgroundTasks(); + if (backgroundTasks.isSome()) { + isBackgroundTaskMode = true; + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::BackgroundTaskName, backgroundTasks.ref()); + } +#endif + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::BackgroundTaskMode, isBackgroundTaskMode); + + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::HeadlessMode, + gfxPlatform::IsHeadless()); + + CrashReporter::SetRestartArgs(gArgc, gArgv); + + // annotate other data (user id etc) + nsCOMPtr<nsIFile> userAppDataDir; + if (NS_SUCCEEDED(mDirProvider.GetUserAppDataDirectory( + getter_AddRefs(userAppDataDir)))) { + CrashReporter::SetupExtraData(userAppDataDir, + nsDependentCString(mAppData->buildID)); + + // see if we have a crashreporter-override.ini in the application + // directory + nsCOMPtr<nsIFile> overrideini; + if (NS_SUCCEEDED( + mDirProvider.GetAppDir()->Clone(getter_AddRefs(overrideini))) && + NS_SUCCEEDED( + overrideini->AppendNative("crashreporter-override.ini"_ns))) { +#ifdef XP_WIN + nsAutoString overridePathW; + overrideini->GetPath(overridePathW); + NS_ConvertUTF16toUTF8 overridePath(overridePathW); +#else + nsAutoCString overridePath; + overrideini->GetNativePath(overridePath); +#endif + + SaveWordToEnv("MOZ_CRASHREPORTER_STRINGS_OVERRIDE", overridePath); + } + } + } else { + // We might have registered a runtime exception module very early in process + // startup to catch early crashes. This is before we have access to ini file + // data, so unregister here if it turns out the crash reporter is disabled. + CrashReporter::UnregisterRuntimeExceptionModule(); + } + +#if defined(MOZ_SANDBOX) && defined(XP_WIN) + if (mAppData->sandboxBrokerServices) { + SandboxBroker::Initialize(mAppData->sandboxBrokerServices); + } else { +# if defined(MOZ_SANDBOX) + // If we're sandboxing content and we fail to initialize, then crashing here + // seems like the sensible option. + if (BrowserTabsRemoteAutostart()) { + MOZ_CRASH("Failed to initialize broker services, can't continue."); + } +# endif + // Otherwise just warn for the moment, as most things will work. + NS_WARNING( + "Failed to initialize broker services, sandboxed processes will " + "fail to start."); + } +#endif + +#ifdef XP_MACOSX + // Set up ability to respond to system (Apple) events. This must occur before + // ProcessUpdates to ensure that links clicked in external applications aren't + // lost when updates are pending. + SetupMacApplicationDelegate(&gRestartedByOS); + + if (EnvHasValue("MOZ_LAUNCHED_CHILD")) { + // This is needed, on relaunch, to force the OS to use the "Cocoa Dock + // API". Otherwise the call to ReceiveNextEvent() below will make it + // use the "Carbon Dock API". For more info see bmo bug 377166. + EnsureUseCocoaDockAPI(); + + // When the app relaunches, the original process exits. This causes + // the dock tile to stop bouncing, lose the "running" triangle, and + // if the tile does not permanently reside in the Dock, even disappear. + // This can be confusing to the user, who is expecting the app to launch. + // Calling ReceiveNextEvent without requesting any event is enough to + // cause a dock tile for the child process to appear. + const EventTypeSpec kFakeEventList[] = {{INT_MAX, INT_MAX}}; + EventRef event; + ::ReceiveNextEvent(GetEventTypeCount(kFakeEventList), kFakeEventList, + kEventDurationNoWait, false, &event); + } + + if (CheckArg("foreground")) { + // The original process communicates that it was in the foreground by + // adding this argument. This new process, which is taking over for + // the old one, should make itself the active application. + ProcessSerialNumber psn; + if (::GetCurrentProcess(&psn) == noErr) ::SetFrontProcess(&psn); + } +#endif + + SaveToEnv("MOZ_LAUNCHED_CHILD="); + + // On Windows, the -os-restarted command line switch lets us know when we are + // restarted via RegisterApplicationRestart. May be used for other OSes later. + if (CheckArg("os-restarted", nullptr, CheckArgFlag::RemoveArg) == ARG_FOUND) { + gRestartedByOS = true; + } + + gRestartArgc = gArgc; + gRestartArgv = + (char**)malloc(sizeof(char*) * (gArgc + 1 + (override ? 2 : 0))); + if (!gRestartArgv) { + return 1; + } + + int i; + for (i = 0; i < gArgc; ++i) { + gRestartArgv[i] = gArgv[i]; + } + + // Add the -override argument back (it is removed automatically be CheckArg) + // if there is one + if (override) { + gRestartArgv[gRestartArgc++] = const_cast<char*>("-override"); + gRestartArgv[gRestartArgc++] = const_cast<char*>(override); + } + + gRestartArgv[gRestartArgc] = nullptr; + + Maybe<bool> safeModeRequested = IsSafeModeRequested(gArgc, gArgv); + if (!safeModeRequested) { + return 1; + } +#ifdef MOZ_BACKGROUNDTASKS + if (BackgroundTasks::IsBackgroundTaskMode()) { + safeModeRequested = Some(false); + + // Remove the --backgroundtask arg now that it has been saved in + // gRestartArgv. + const char* tmpBackgroundTaskName = nullptr; + Unused << CheckArg("backgroundtask", &tmpBackgroundTaskName, + CheckArgFlag::RemoveArg); + } +#endif + + gSafeMode = safeModeRequested.value(); + + MaybeAddCPUMicrocodeCrashAnnotation(); + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::SafeMode, + gSafeMode); + +#if defined(MOZ_HAS_REMOTE) + // Handle --no-remote and --new-instance command line arguments. Setup + // the environment to better accommodate other components and various + // restart scenarios. + ar = CheckArg("no-remote"); + if (ar == ARG_FOUND || EnvHasValue("MOZ_NO_REMOTE")) { + mDisableRemoteClient = true; + mDisableRemoteServer = true; + gRestartWithoutRemote = true; + // We don't want to propagate MOZ_NO_REMOTE to potential child + // process. + SaveToEnv("MOZ_NO_REMOTE="); + } + + ar = CheckArg("new-instance"); + if (ar == ARG_FOUND || EnvHasValue("MOZ_NEW_INSTANCE")) { + mDisableRemoteClient = true; + } +#else + // These arguments do nothing in platforms with no remoting support but we + // should remove them from the command line anyway. + CheckArg("no-remote"); + CheckArg("new-instance"); +#endif + + ar = CheckArg("offline"); + if (ar || EnvHasValue("XRE_START_OFFLINE")) { + mStartOffline = true; + } + + // On Windows, to get working console arrangements so help/version/etc + // print something, we need to initialize the native app support. + rv = NS_CreateNativeAppSupport(getter_AddRefs(mNativeApp)); + if (NS_FAILED(rv)) return 1; + + // Handle --help, --full-version and --version command line arguments. + // They should return quickly, so we deal with them here. + if (CheckArg("h") || CheckArg("help") || CheckArg("?")) { + DumpHelp(); + *aExitFlag = true; + return 0; + } + + if (CheckArg("v") || CheckArg("version")) { + DumpVersion(); + *aExitFlag = true; + return 0; + } + + if (CheckArg("full-version")) { + DumpFullVersion(); + *aExitFlag = true; + return 0; + } + + rv = XRE_InitCommandLine(gArgc, gArgv); + NS_ENSURE_SUCCESS(rv, 1); + + return 0; +} + +#if defined(XP_LINUX) && !defined(ANDROID) + +static void AnnotateLSBRelease(void*) { + nsCString dist, desc, release, codename; + if (widget::lsb::GetLSBRelease(dist, desc, release, codename)) { + CrashReporter::AppendAppNotesToCrashReport(desc); + } +} + +#endif // defined(XP_LINUX) && !defined(ANDROID) + +#ifdef XP_WIN +static void ReadAheadSystemDll(const wchar_t* dllName) { + wchar_t dllPath[MAX_PATH]; + if (ConstructSystem32Path(dllName, dllPath, MAX_PATH)) { + ReadAheadLib(dllPath); + } +} + +static void ReadAheadPackagedDll(const wchar_t* dllName, + const wchar_t* aGREDir) { + wchar_t dllPath[MAX_PATH]; + swprintf(dllPath, MAX_PATH, L"%s\\%s", aGREDir, dllName); + ReadAheadLib(dllPath); +} + +static void PR_CALLBACK ReadAheadDlls_ThreadStart(void* arg) { + UniquePtr<wchar_t[]> greDir(static_cast<wchar_t*>(arg)); + + // In Bug 1628903, we investigated which DLLs we should prefetch in + // order to reduce disk I/O and improve startup on Windows machines. + // Our ultimate goal is to measure the impact of these improvements on + // retention (see Bug 1640087). Before we place this within a pref, + // we should ensure this feature only ships to the nightly channel + // and monitor results from that subset. + if (greDir) { + // Prefetch the DLLs shipped with firefox + ReadAheadPackagedDll(L"libegl.dll", greDir.get()); + ReadAheadPackagedDll(L"libGLESv2.dll", greDir.get()); + ReadAheadPackagedDll(L"nssckbi.dll", greDir.get()); + ReadAheadPackagedDll(L"freebl3.dll", greDir.get()); + ReadAheadPackagedDll(L"softokn3.dll", greDir.get()); + + // Prefetch the system DLLs + ReadAheadSystemDll(L"DWrite.dll"); + ReadAheadSystemDll(L"D3DCompiler_47.dll"); + } else { + // Load DataExchange.dll and twinapi.appcore.dll for + // nsWindow::EnableDragDrop + ReadAheadSystemDll(L"DataExchange.dll"); + ReadAheadSystemDll(L"twinapi.appcore.dll"); + + // Load twinapi.dll for WindowsUIUtils::UpdateTabletModeState + ReadAheadSystemDll(L"twinapi.dll"); + + // Load explorerframe.dll for WinTaskbar::Initialize + ReadAheadSystemDll(L"ExplorerFrame.dll"); + + // Load WinTypes.dll for nsOSHelperAppService::GetApplicationDescription + ReadAheadSystemDll(L"WinTypes.dll"); + } +} +#endif + +#if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID) +enum struct ShouldNotProcessUpdatesReason { + DevToolsLaunching, + NotAnUpdatingTask, + OtherInstanceRunning, +}; + +const char* ShouldNotProcessUpdatesReasonAsString( + ShouldNotProcessUpdatesReason aReason) { + switch (aReason) { + case ShouldNotProcessUpdatesReason::DevToolsLaunching: + return "DevToolsLaunching"; + case ShouldNotProcessUpdatesReason::NotAnUpdatingTask: + return "NotAnUpdatingTask"; + case ShouldNotProcessUpdatesReason::OtherInstanceRunning: + return "OtherInstanceRunning"; + default: + MOZ_CRASH("impossible value for ShouldNotProcessUpdatesReason"); + } +} + +Maybe<ShouldNotProcessUpdatesReason> ShouldNotProcessUpdates( + nsXREDirProvider& aDirProvider) { + // Do not process updates if we're launching devtools, as evidenced by + // "--chrome ..." with the browser toolbox chrome document URL. + + // Keep this synchronized with the value of the same name in + // devtools/client/framework/browser-toolbox/Launcher.sys.mjs. Or, for bonus + // points, lift this value to nsIXulRuntime or similar, so that it can be + // accessed in both locations. (The prefs service isn't available at this + // point so the simplest manner of sharing the value is not available to us.) + const char* BROWSER_TOOLBOX_WINDOW_URL = + "chrome://devtools/content/framework/browser-toolbox/window.html"; + + const char* chromeParam = nullptr; + if (ARG_FOUND == CheckArg("chrome", &chromeParam, CheckArgFlag::None)) { + if (!chromeParam || !strcmp(BROWSER_TOOLBOX_WINDOW_URL, chromeParam)) { + NS_WARNING("ShouldNotProcessUpdates(): DevToolsLaunching"); + return Some(ShouldNotProcessUpdatesReason::DevToolsLaunching); + } + } + +# ifdef MOZ_BACKGROUNDTASKS + // Do not process updates if we're running a background task mode and another + // instance is already running. This avoids periodic maintenance updating + // underneath a browsing session. + Maybe<nsCString> backgroundTasks = BackgroundTasks::GetBackgroundTasks(); + if (backgroundTasks.isSome()) { + // Only process updates for specific tasks: at this time, the + // `backgroundupdate` task and the test-only `shouldprocessupdates` task. + // + // Background tasks can be sparked by Firefox instances that are shutting + // down, which can cause races between the task startup trying to update and + // Firefox trying to invoke the updater. This happened when converting + // `pingsender` to a background task, since it is launched to send pings at + // shutdown: Bug 1736373. + // + // We'd prefer to have this be a property of the task definition sibling to + // `backgroundTaskTimeoutSec`, but when we reach this code we're well before + // we can load the task JSM. + if (!BackgroundTasks::IsUpdatingTaskName(backgroundTasks.ref())) { + NS_WARNING("ShouldNotProcessUpdates(): NotAnUpdatingTask"); + return Some(ShouldNotProcessUpdatesReason::NotAnUpdatingTask); + } + + // At this point we have a dir provider but no XPCOM directory service. We + // launch the update sync manager using that information so that it doesn't + // need to ask for (and fail to find) the directory service. + nsCOMPtr<nsIFile> anAppFile; + bool persistent; + nsresult rv = aDirProvider.GetFile(XRE_EXECUTABLE_FILE, &persistent, + getter_AddRefs(anAppFile)); + if (NS_FAILED(rv) || !anAppFile) { + // Strange, but not a reason to skip processing updates. + return Nothing(); + } + + auto updateSyncManager = new nsUpdateSyncManager(anAppFile); + + bool otherInstance = false; + updateSyncManager->IsOtherInstanceRunning(&otherInstance); + if (otherInstance) { + NS_WARNING("ShouldNotProcessUpdates(): OtherInstanceRunning"); + return Some(ShouldNotProcessUpdatesReason::OtherInstanceRunning); + } + } +# endif + + return Nothing(); +} +#endif + +namespace mozilla::startup { +Result<nsCOMPtr<nsIFile>, nsresult> GetIncompleteStartupFile(nsIFile* aProfLD) { + nsCOMPtr<nsIFile> crashFile; + MOZ_TRY(aProfLD->Clone(getter_AddRefs(crashFile))); + MOZ_TRY(crashFile->Append(FILE_STARTUP_INCOMPLETE)); + return std::move(crashFile); +} +} // namespace mozilla::startup + +// Check whether the last startup attempt resulted in a crash or hang. +// This is distinct from the definition of a startup crash from +// nsAppStartup::TrackStartupCrashBegin. +bool XREMain::CheckLastStartupWasCrash() { + Result<nsCOMPtr<nsIFile>, nsresult> crashFile = + GetIncompleteStartupFile(mProfLD); + if (crashFile.isErr()) { + return true; + } + + // Attempt to create the incomplete startup canary file. If the file already + // exists, this fails, and we know the last startup was a crash. If it + // doesn't already exist, it is created, and will be removed at the end of + // the startup crash detection window. + AutoFDClose fd; + Unused << crashFile.inspect()->OpenNSPRFileDesc( + PR_WRONLY | PR_CREATE_FILE | PR_EXCL, 0666, &fd.rwget()); + return !fd; +} + +/* + * XRE_mainStartup - Initializes the profile and various other services. + * Main() will exit early if either return value != 0 or if aExitFlag is + * true. + */ +int XREMain::XRE_mainStartup(bool* aExitFlag) { + nsresult rv; + + if (!aExitFlag) return 1; + *aExitFlag = false; + +#ifdef XP_MACOSX + mozilla::MacAutoreleasePool pool; +#endif + + // Enable Telemetry IO Reporting on DEBUG, nightly and local builds, + // but disable it on FUZZING builds and for ANDROID. +#ifndef FUZZING +# ifndef ANDROID +# ifdef DEBUG + mozilla::Telemetry::InitIOReporting(gAppData->xreDirectory); +# else + { + const char* releaseChannel = MOZ_STRINGIFY(MOZ_UPDATE_CHANNEL); + if (strcmp(releaseChannel, "nightly") == 0 || + strcmp(releaseChannel, "default") == 0) { + mozilla::Telemetry::InitIOReporting(gAppData->xreDirectory); + } + } +# endif /* DEBUG */ +# endif /* ANDROID */ +#endif /* FUZZING */ + +#if defined(XP_WIN) + // Enable the HeapEnableTerminationOnCorruption exploit mitigation. We ignore + // the return code because it always returns success, although it has no + // effect on Windows older than XP SP3. + HeapSetInformation(NULL, HeapEnableTerminationOnCorruption, NULL, 0); +#endif /* XP_WIN */ + +#ifdef MOZ_WIDGET_GTK + // Stash startup token in owned memory because gtk_init will clear + // DESKTOP_STARTUP_ID it. + if (const char* v = PR_GetEnv("DESKTOP_STARTUP_ID")) { + mDesktopStartupID.Assign(v); + } + if (const char* v = PR_GetEnv("XDG_ACTIVATION_TOKEN")) { + mXDGActivationToken.Assign(v); + } +#endif + +#if defined(XP_WIN) + { + // Save the shortcut path before lpTitle is replaced by an AUMID, + // such as by WinTaskbar + STARTUPINFOW si; + GetStartupInfoW(&si); + if (si.dwFlags & STARTF_TITLEISAPPID) { + NS_WARNING("AUMID was already set, shortcut may have been lost."); + } else if ((si.dwFlags & STARTF_TITLEISLINKNAME) && si.lpTitle) { + gProcessStartupShortcut.Assign(si.lpTitle); + } + } +#endif /* XP_WIN */ + +#if defined(MOZ_WIDGET_GTK) + // setup for private colormap. Ideally we'd like to do this + // in nsAppShell::Create, but we need to get in before gtk + // has been initialized to make sure everything is running + // consistently. + + // Set program name to the one defined in application.ini. + g_set_prgname(gAppData->remotingName); + + // Initialize GTK here for splash. + +# if defined(MOZ_WIDGET_GTK) && defined(MOZ_X11) + // Disable XInput2 multidevice support due to focus bugginess. + // See bugs 1182700, 1170342. + // gdk_disable_multidevice() affects Gdk X11 backend only, + // the multidevice support is always enabled on Wayland backend. + const char* useXI2 = PR_GetEnv("MOZ_USE_XINPUT2"); + if (!useXI2 || (*useXI2 == '0')) gdk_disable_multidevice(); +# endif + + // Open the display ourselves instead of using gtk_init, so that we can + // close it without fear that one day gtk might clean up the display it + // opens. + if (!gtk_parse_args(&gArgc, &gArgv)) return 1; +#endif /* MOZ_WIDGET_GTK */ + +#ifdef FUZZING + if (PR_GetEnv("FUZZER")) { + *aExitFlag = true; + return mozilla::fuzzerRunner->Run(&gArgc, &gArgv); + } +#endif + + if (PR_GetEnv("MOZ_RUN_GTEST")) { + int result; +#ifdef XP_WIN + UseParentConsole(); +#endif + // RunGTest will only be set if we're in xul-unit + if (mozilla::RunGTest) { + gIsGtest = true; + result = mozilla::RunGTest(&gArgc, gArgv); + gIsGtest = false; + } else { + result = 1; + printf("TEST-UNEXPECTED-FAIL | gtest | Not compiled with enable-tests\n"); + } + *aExitFlag = true; + return result; + } + + bool isBackgroundTaskMode = false; +#ifdef MOZ_BACKGROUNDTASKS + isBackgroundTaskMode = BackgroundTasks::IsBackgroundTaskMode(); +#endif + +#ifdef MOZ_HAS_REMOTE + if (gfxPlatform::IsHeadless()) { + mDisableRemoteClient = true; + mDisableRemoteServer = true; + } +#endif + +#ifdef MOZ_X11 + // Init X11 in thread-safe mode. Must be called prior to the first call to + // XOpenDisplay (called inside gdk_display_open). This is a requirement for + // off main tread compositing. + if (!isBackgroundTaskMode && !gfxPlatform::IsHeadless()) { + XInitThreads(); + } +#endif +#if defined(MOZ_WIDGET_GTK) + if (!isBackgroundTaskMode && !gfxPlatform::IsHeadless()) { + const char* display_name = nullptr; + bool saveDisplayArg = false; + + // display_name is owned by gdk. + display_name = gdk_get_display_arg_name(); + // if --display argument is given make sure it's + // also passed to ContentChild::Init() by MOZ_GDK_DISPLAY. + if (display_name) { + SaveWordToEnv("MOZ_GDK_DISPLAY", nsDependentCString(display_name)); + saveDisplayArg = true; + } + + bool waylandEnabled = IsWaylandEnabled(); + // On Wayland disabled builds read X11 DISPLAY env exclusively + // and don't care about different displays. + if (!waylandEnabled && !display_name) { + display_name = PR_GetEnv("DISPLAY"); + if (!display_name) { + PR_fprintf(PR_STDERR, + "Error: no DISPLAY environment variable specified\n"); + return 1; + } + } + + if (display_name) { + GdkDisplay* disp = gdk_display_open(display_name); + if (!disp) { + PR_fprintf(PR_STDERR, "Error: cannot open display: %s\n", display_name); + return 1; + } + if (saveDisplayArg) { + if (GdkIsX11Display(disp)) { + SaveWordToEnv("DISPLAY", nsDependentCString(display_name)); + } +# ifdef MOZ_WAYLAND + else if (GdkIsWaylandDisplay(disp)) { + SaveWordToEnv("WAYLAND_DISPLAY", nsDependentCString(display_name)); + } +# endif + } + } +# ifdef MOZ_WIDGET_GTK + else { + gdk_display_manager_open_display(gdk_display_manager_get(), nullptr); + } +# endif + } +#endif +#if defined(MOZ_HAS_REMOTE) + // handle --remote now that xpcom is fired up + mRemoteService = new nsRemoteService(gAppData->remotingName); + if (mRemoteService && !mDisableRemoteServer) { + mRemoteService->LockStartup(); + gRemoteService = mRemoteService; + } +#endif +#if defined(MOZ_WIDGET_GTK) + g_set_application_name(mAppData->name); + +#endif /* defined(MOZ_WIDGET_GTK) */ +#ifdef MOZ_X11 + // Do this after initializing GDK, or GDK will install its own handler. + XRE_InstallX11ErrorHandler(); +#endif + + // Call the code to install our handler +#ifdef MOZ_JPROF + setupProfilingStuff(); +#endif + + bool canRun = false; + rv = mNativeApp->Start(&canRun); + if (NS_FAILED(rv) || !canRun) { + return 1; + } + +#ifdef MOZ_WIDGET_GTK + // startup token might be cleared now, we recover it in case we need a + // restart. + if (!mDesktopStartupID.IsEmpty()) { + // Leak it with extreme prejudice! + PR_SetEnv(ToNewCString("DESKTOP_STARTUP_ID="_ns + mDesktopStartupID)); + } + + if (!mXDGActivationToken.IsEmpty()) { + // Leak it with extreme prejudice! + PR_SetEnv(ToNewCString("XDG_ACTIVATION_TOKEN="_ns + mXDGActivationToken)); + } +#endif + + // Support exiting early for testing startup sequence. Bug 1360493 + if (CheckArg("test-launch-without-hang")) { + *aExitFlag = true; + return 0; + } + + mProfileSvc = NS_GetToolkitProfileService(); + if (!mProfileSvc) { + // We failed to choose or create profile - notify user and quit + ProfileMissingDialog(mNativeApp); + return 1; + } + + bool wasDefaultSelection; + nsCOMPtr<nsIToolkitProfile> profile; + rv = SelectProfile(mProfileSvc, mNativeApp, getter_AddRefs(mProfD), + getter_AddRefs(mProfLD), getter_AddRefs(profile), + &wasDefaultSelection); + if (rv == NS_ERROR_LAUNCHED_CHILD_PROCESS || rv == NS_ERROR_ABORT) { + *aExitFlag = true; + return 0; + } + + if (NS_FAILED(rv)) { + // We failed to choose or create profile - notify user and quit + ProfileMissingDialog(mNativeApp); + return 1; + } + +#if defined(MOZ_HAS_REMOTE) + if (mRemoteService) { + // We want a unique profile name to identify the remote instance. + nsCString profileName; + if (profile) { + rv = profile->GetName(profileName); + } + if (!profile || NS_FAILED(rv) || profileName.IsEmpty()) { + // Couldn't get a name from the profile. Use the directory name? + nsString leafName; + rv = mProfD->GetLeafName(leafName); + if (NS_SUCCEEDED(rv)) { + CopyUTF16toUTF8(leafName, profileName); + } + } + + mRemoteService->SetProfile(profileName); + + if (!mDisableRemoteClient) { + // Try to remote the entire command line. If this fails, start up + // normally. +# ifdef MOZ_WIDGET_GTK + const auto& startupToken = + GdkIsWaylandDisplay() ? mXDGActivationToken : mDesktopStartupID; +# else + const nsCString startupToken; +# endif + RemoteResult rr = mRemoteService->StartClient( + startupToken.IsEmpty() ? nullptr : startupToken.get()); + if (rr == REMOTE_FOUND) { + *aExitFlag = true; + mRemoteService->UnlockStartup(); + return 0; + } + if (rr == REMOTE_ARG_BAD) { + mRemoteService->UnlockStartup(); + return 1; + } + } + } +#endif + +#if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID) + Maybe<ShouldNotProcessUpdatesReason> shouldNotProcessUpdatesReason = + ShouldNotProcessUpdates(mDirProvider); + if (shouldNotProcessUpdatesReason.isNothing()) { + // Check for and process any available updates + nsCOMPtr<nsIFile> updRoot; + bool persistent; + rv = mDirProvider.GetFile(XRE_UPDATE_ROOT_DIR, &persistent, + getter_AddRefs(updRoot)); + // XRE_UPDATE_ROOT_DIR may fail. Fallback to appDir if failed + if (NS_FAILED(rv)) { + updRoot = mDirProvider.GetAppDir(); + } + + // If the MOZ_TEST_PROCESS_UPDATES environment variable already exists, then + // we are being called from the callback application. + if (EnvHasValue("MOZ_TEST_PROCESS_UPDATES")) { + // If the caller has asked us to log our arguments, do so. This is used + // to make sure that the maintenance service successfully launches the + // callback application. + const char* logFile = nullptr; + if (ARG_FOUND == CheckArg("dump-args", &logFile)) { + FILE* logFP = fopen(logFile, "wb"); + if (logFP) { + for (int i = 1; i < gRestartArgc; ++i) { + fprintf(logFP, "%s\n", gRestartArgv[i]); + } + fclose(logFP); + } + } + *aExitFlag = true; + return 0; + } + + // Support for processing an update and exiting. The + // MOZ_TEST_PROCESS_UPDATES environment variable will be part of the + // updater's environment and the application that is relaunched by the + // updater. When the application is relaunched by the updater it will be + // removed below and the application will exit. + if (CheckArg("test-process-updates")) { + SaveToEnv("MOZ_TEST_PROCESS_UPDATES=1"); + } + nsCOMPtr<nsIFile> exeFile, exeDir; + rv = mDirProvider.GetFile(XRE_EXECUTABLE_FILE, &persistent, + getter_AddRefs(exeFile)); + NS_ENSURE_SUCCESS(rv, 1); + rv = exeFile->GetParent(getter_AddRefs(exeDir)); + NS_ENSURE_SUCCESS(rv, 1); + ProcessUpdates(mDirProvider.GetGREDir(), exeDir, updRoot, gRestartArgc, + gRestartArgv, mAppData->version); + if (EnvHasValue("MOZ_TEST_PROCESS_UPDATES")) { + SaveToEnv("MOZ_TEST_PROCESS_UPDATES="); + *aExitFlag = true; + return 0; + } + } else { + if (CheckArg("test-process-updates") || + EnvHasValue("MOZ_TEST_PROCESS_UPDATES")) { + // Support for testing *not* processing an update. The launched process + // can witness this environment variable and conclude that its runtime + // environment resulted in not processing updates. + + SaveToEnv(nsPrintfCString( + "MOZ_TEST_PROCESS_UPDATES=ShouldNotProcessUpdates(): %s", + ShouldNotProcessUpdatesReasonAsString( + shouldNotProcessUpdatesReason.value())) + .get()); + } + } +#endif + + // We now know there is no existing instance using the selected profile. If + // the profile wasn't selected by specific command line arguments and the + // user has chosen to show the profile manager on startup then do that. + if (wasDefaultSelection) { + bool useSelectedProfile; + rv = mProfileSvc->GetStartWithLastProfile(&useSelectedProfile); + NS_ENSURE_SUCCESS(rv, 1); + + if (!useSelectedProfile) { + rv = ShowProfileManager(mProfileSvc, mNativeApp); + if (rv == NS_ERROR_LAUNCHED_CHILD_PROCESS || rv == NS_ERROR_ABORT) { + *aExitFlag = true; + return 0; + } + if (NS_FAILED(rv)) { + return 1; + } + } + } + + // We always want to lock the profile even if we're actually going to reset + // it later. + rv = LockProfile(mNativeApp, mProfD, mProfLD, profile, + getter_AddRefs(mProfileLock)); + if (rv == NS_ERROR_LAUNCHED_CHILD_PROCESS || rv == NS_ERROR_ABORT) { + *aExitFlag = true; + return 0; + } else if (NS_FAILED(rv)) { + return 1; + } + + if (gDoProfileReset) { + if (EnvHasValue("MOZ_RESET_PROFILE_RESTART")) { + SaveToEnv("MOZ_RESET_PROFILE_RESTART="); + // We only want to restore the previous session if the profile refresh was + // triggered by user. And if it was a user-triggered profile refresh + // through, say, the safeMode dialog or the troubleshooting page, the + // MOZ_RESET_PROFILE_RESTART env variable would be set. Hence we set + // MOZ_RESET_PROFILE_MIGRATE_SESSION here so that Firefox profile migrator + // would migrate old session data later. + SaveToEnv("MOZ_RESET_PROFILE_MIGRATE_SESSION=1"); + } + // Unlock the source profile. + mProfileLock->Unlock(); + + // If we're resetting a profile, create a new one and use it to startup. + gResetOldProfile = profile; + rv = mProfileSvc->CreateResetProfile(getter_AddRefs(profile)); + if (NS_SUCCEEDED(rv)) { + rv = profile->GetRootDir(getter_AddRefs(mProfD)); + NS_ENSURE_SUCCESS(rv, 1); + SaveFileToEnv("XRE_PROFILE_PATH", mProfD); + + rv = profile->GetLocalDir(getter_AddRefs(mProfLD)); + NS_ENSURE_SUCCESS(rv, 1); + SaveFileToEnv("XRE_PROFILE_LOCAL_PATH", mProfLD); + + // Lock the new profile + rv = LockProfile(mNativeApp, mProfD, mProfLD, profile, + getter_AddRefs(mProfileLock)); + if (rv == NS_ERROR_LAUNCHED_CHILD_PROCESS || rv == NS_ERROR_ABORT) { + *aExitFlag = true; + return 0; + } else if (NS_FAILED(rv)) { + return 1; + } + } else { + NS_WARNING("Profile reset failed."); + return 1; + } + } + + gProfileLock = mProfileLock; + + nsAutoCString version; + BuildVersion(version); + +#ifdef TARGET_OS_ABI + constexpr auto osABI = nsLiteralCString{TARGET_OS_ABI}; +#else + // No TARGET_XPCOM_ABI, but at least the OS is known + constexpr auto osABI = nsLiteralCString{OS_TARGET "_UNKNOWN"}; +#endif + + // Check for version compatibility with the last version of the app this + // profile was started with. The format of the version stamp is defined + // by the BuildVersion function. + // Also check to see if something has happened to invalidate our + // fastload caches, like an app upgrade. + + // If we see .purgecaches, that means someone did a make. + // Re-register components to catch potential changes. + nsCOMPtr<nsIFile> flagFile; + if (mAppData->directory) { + Unused << mAppData->directory->Clone(getter_AddRefs(flagFile)); + } + if (flagFile) { + flagFile->AppendNative(FILE_INVALIDATE_CACHES); + } + + bool cachesOK; + bool isDowngrade; + nsCString lastVersion; + bool versionOK = CheckCompatibility( + mProfD, version, osABI, mDirProvider.GetGREDir(), mAppData->directory, + flagFile, &cachesOK, &isDowngrade, lastVersion); + + MOZ_RELEASE_ASSERT(!cachesOK || lastVersion.Equals(version), + "Caches cannot be good if the version has changed."); + +#ifdef MOZ_BLOCK_PROFILE_DOWNGRADE + // The argument check must come first so the argument is always removed from + // the command line regardless of whether this is a downgrade or not. + if (!CheckArg("allow-downgrade") && isDowngrade && + !EnvHasValue("MOZ_ALLOW_DOWNGRADE")) { + rv = CheckDowngrade(mProfD, mNativeApp, mProfileSvc, lastVersion); + if (rv == NS_ERROR_LAUNCHED_CHILD_PROCESS || rv == NS_ERROR_ABORT) { + *aExitFlag = true; + return 0; + } + } +#endif + + rv = mDirProvider.SetProfile(mProfD, mProfLD); + NS_ENSURE_SUCCESS(rv, 1); + + //////////////////////// NOW WE HAVE A PROFILE //////////////////////// + + mozilla::Telemetry::SetProfileDir(mProfD); + + if (mAppData->flags & NS_XRE_ENABLE_CRASH_REPORTER) { + MakeOrSetMinidumpPath(mProfD); + } + + CrashReporter::SetProfileDirectory(mProfD); + +#ifdef MOZ_ASAN_REPORTER + // In ASan reporter builds, we need to set ASan's log_path as early as + // possible, so it dumps its errors into files there instead of using + // the default stderr location. Since this is crucial for ASan reporter + // to work at all (and we don't want people to use a non-functional + // ASan reporter build), all failures while setting log_path are fatal. + setASanReporterPath(mProfD); + + // Export to env for child processes + SaveFileToEnv("ASAN_REPORTER_PATH", mProfD); +#endif + + bool lastStartupWasCrash = CheckLastStartupWasCrash(); + + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::LastStartupWasCrash, lastStartupWasCrash); + + if (CheckArg("purgecaches") || PR_GetEnv("MOZ_PURGE_CACHES") || + lastStartupWasCrash || gSafeMode) { + cachesOK = false; + } + + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::StartupCacheValid, cachesOK && versionOK); + + // Every time a profile is loaded by a build with a different version, + // it updates the compatibility.ini file saying what version last wrote + // the fastload caches. On subsequent launches if the version matches, + // there is no need for re-registration. If the user loads the same + // profile in different builds the component registry must be + // re-generated to prevent mysterious component loading failures. + // + bool startupCacheValid = true; + + if (!cachesOK || !versionOK) { + QuotaManager::InvalidateQuotaCache(); + + startupCacheValid = RemoveComponentRegistries(mProfD, mProfLD, false); + + // Rewrite compatibility.ini to match the current build. The next run + // should attempt to invalidate the caches if either this run is safe mode + // or the attempt to invalidate the caches this time failed. + WriteVersion(mProfD, version, osABI, mDirProvider.GetGREDir(), + mAppData->directory, gSafeMode || !startupCacheValid); + } + + if (!startupCacheValid) StartupCache::IgnoreDiskCache(); + + if (flagFile) { + flagFile->Remove(true); + } + + // Flush any pending page load events. + mozilla::glean_pings::Pageload.Submit("startup"_ns); + + if (!isBackgroundTaskMode) { +#ifdef USE_GLX_TEST + GfxInfo::FireGLXTestProcess(); +#endif +#ifdef MOZ_WAYLAND + // Make sure we have wayland connection for main thread. + // It's used as template to create display connections + // for different threads. + if (IsWaylandEnabled()) { + MOZ_UNUSED(WaylandDisplayGet()); + } +#endif + } + + return 0; +} + +#if defined(MOZ_SANDBOX) +void AddSandboxAnnotations() { + { + // Include the sandbox content level, regardless of platform + int level = GetEffectiveContentSandboxLevel(); + + nsAutoCString levelString; + levelString.AppendInt(level); + + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::ContentSandboxLevel, levelString); + } + + { + int level = GetEffectiveGpuSandboxLevel(); + + nsAutoCString levelString; + levelString.AppendInt(level); + + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::GpuSandboxLevel, levelString); + } + + // Include whether or not this instance is capable of content sandboxing + bool sandboxCapable = false; + +# if defined(XP_WIN) + // All supported Windows versions support some level of content sandboxing + sandboxCapable = true; +# elif defined(XP_MACOSX) + // All supported OS X versions are capable + sandboxCapable = true; +# elif defined(XP_LINUX) + sandboxCapable = SandboxInfo::Get().CanSandboxContent(); +# elif defined(__OpenBSD__) + sandboxCapable = true; + StartOpenBSDSandbox(GeckoProcessType_Default); +# endif + + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::ContentSandboxCapable, sandboxCapable); +} +#endif /* MOZ_SANDBOX */ + +/* + * XRE_mainRun - Command line startup, profile migration, and + * the calling of appStartup->Run(). + */ +nsresult XREMain::XRE_mainRun() { + nsresult rv = NS_OK; + NS_ASSERTION(mScopedXPCOM, "Scoped xpcom not initialized."); + +#if defined(XP_WIN) + RefPtr<mozilla::DllServices> dllServices(mozilla::DllServices::Get()); + dllServices->StartUntrustedModulesProcessor(false); + auto dllServicesDisable = + MakeScopeExit([&dllServices]() { dllServices->DisableFull(); }); + + mozilla::mscom::InitProfilerMarkers(); +#endif // defined(XP_WIN) + + // We need the appStartup pointer to span multiple scopes, so we declare + // it here. + nsCOMPtr<nsIAppStartup> appStartup; + // Ditto with the command line. + nsCOMPtr<nsICommandLineRunner> cmdLine; + + { +#ifdef XP_MACOSX + // In this scope, create an autorelease pool that will leave scope with + // it just before entering our event loop. + mozilla::MacAutoreleasePool pool; +#endif + + rv = mScopedXPCOM->SetWindowCreator(mNativeApp); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + // tell the crash reporter to also send the release channel + nsCOMPtr<nsIPrefService> prefs = + do_GetService("@mozilla.org/preferences-service;1", &rv); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIPrefBranch> defaultPrefBranch; + rv = prefs->GetDefaultBranch(nullptr, getter_AddRefs(defaultPrefBranch)); + + if (NS_SUCCEEDED(rv)) { + nsAutoCString sval; + rv = defaultPrefBranch->GetCharPref("app.update.channel", sval); + if (NS_SUCCEEDED(rv)) { + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::ReleaseChannel, sval); + } + } + } + // Needs to be set after xpcom initialization. + bool includeContextHeap = Preferences::GetBool( + "toolkit.crashreporter.include_context_heap", false); + CrashReporter::SetIncludeContextHeap(includeContextHeap); + +#if defined(XP_LINUX) && !defined(ANDROID) + PR_CreateThread(PR_USER_THREAD, AnnotateLSBRelease, 0, PR_PRIORITY_LOW, + PR_GLOBAL_THREAD, PR_UNJOINABLE_THREAD, 0); +#endif + + if (mStartOffline) { + nsCOMPtr<nsIIOService> io( + do_GetService("@mozilla.org/network/io-service;1")); + NS_ENSURE_TRUE(io, NS_ERROR_FAILURE); + io->SetManageOfflineStatus(false); + io->SetOffline(true); + } + +#ifdef XP_WIN + mozilla::DllPrefetchExperimentRegistryInfo prefetchRegInfo; + mozilla::AlteredDllPrefetchMode dllPrefetchMode = + prefetchRegInfo.GetAlteredDllPrefetchMode(); + + if (!PR_GetEnv("XRE_NO_DLL_READAHEAD") && + dllPrefetchMode != mozilla::AlteredDllPrefetchMode::NoPrefetch) { + nsCOMPtr<nsIFile> greDir = mDirProvider.GetGREDir(); + nsAutoString path; + rv = greDir->GetPath(path); + if (NS_SUCCEEDED(rv)) { + PRThread* readAheadThread; + wchar_t* pathRaw; + + // We use the presence of a path argument inside the thread to determine + // which list of Dlls to use. The old list does not need access to the + // GRE dir, so the path argument is set to a null pointer. + if (dllPrefetchMode == + mozilla::AlteredDllPrefetchMode::OptimizedPrefetch) { + pathRaw = new wchar_t[MAX_PATH]; + wcscpy_s(pathRaw, MAX_PATH, path.get()); + } else { + pathRaw = nullptr; + } + readAheadThread = PR_CreateThread( + PR_USER_THREAD, ReadAheadDlls_ThreadStart, (void*)pathRaw, + PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD, PR_UNJOINABLE_THREAD, 0); + if (readAheadThread == NULL) { + delete[] pathRaw; + } + } + } +#endif + + if (gDoMigration) { + nsCOMPtr<nsIFile> file; + mDirProvider.GetAppDir()->Clone(getter_AddRefs(file)); + file->AppendNative("override.ini"_ns); + nsINIParser parser; + nsresult rv = parser.Init(file); + // if override.ini doesn't exist, also check for distribution.ini + if (NS_FAILED(rv)) { + bool persistent; + mDirProvider.GetFile(XRE_APP_DISTRIBUTION_DIR, &persistent, + getter_AddRefs(file)); + file->AppendNative("distribution.ini"_ns); + rv = parser.Init(file); + } + if (NS_SUCCEEDED(rv)) { + nsAutoCString buf; + rv = parser.GetString("XRE", "EnableProfileMigrator", buf); + if (NS_SUCCEEDED(rv)) { + if (buf[0] == '0' || buf[0] == 'f' || buf[0] == 'F') { + gDoMigration = false; + } + } + } + } + + // We'd like to initialize the JSContext *after* reading the user prefs. + // Unfortunately that's not possible if we have to do profile migration + // because that requires us to execute JS before reading user prefs. + // Restarting the browser after profile migration would fix this. See + // bug 1592523. + bool initializedJSContext = false; + + { + // Profile Migration + if (mAppData->flags & NS_XRE_ENABLE_PROFILE_MIGRATOR && gDoMigration) { + gDoMigration = false; + + xpc::InitializeJSContext(); + initializedJSContext = true; + + nsCOMPtr<nsIProfileMigrator> pm( + do_CreateInstance(NS_PROFILEMIGRATOR_CONTRACTID)); + if (pm) { + nsAutoCString aKey; + nsAutoCString aName; + if (gDoProfileReset) { + // Automatically migrate from the current application if we just + // reset the profile. + aKey = MOZ_APP_NAME; + gResetOldProfile->GetName(aName); + } + pm->Migrate(&mDirProvider, aKey, aName); + } + } + + if (gDoProfileReset) { + if (!initializedJSContext) { + xpc::InitializeJSContext(); + initializedJSContext = true; + } + + nsresult backupCreated = + ProfileResetCleanup(mProfileSvc, gResetOldProfile); + if (NS_FAILED(backupCreated)) { + NS_WARNING("Could not cleanup the profile that was reset"); + } + } + } + + // Initialize user preferences before notifying startup observers so they're + // ready in time for early consumers, such as the component loader. + mDirProvider.InitializeUserPrefs(); + + // Now that all (user) prefs have been loaded we can initialize the main + // thread's JSContext. + if (!initializedJSContext) { + xpc::InitializeJSContext(); + } + + // Finally, now that JS has been initialized, we can finish pref loading. + // This needs to happen after JS and XPConnect initialization because + // AutoConfig files require JS execution. Note that this means AutoConfig + // files can't override JS engine start-up prefs. + mDirProvider.FinishInitializingUserPrefs(); + +#if defined(MOZ_SANDBOX) && defined(XP_WIN) + // Now that we have preferences and the directory provider, we can + // finish initializing SandboxBroker. This must happen before the GFX + // platform is initialized (which will launch the GPU process), which + // occurs when the "app-startup" category is started up below + // + // After this completes, we are ready to launch sandboxed processes + mozilla::SandboxBroker::GeckoDependentInitialize(); +#endif + + nsCOMPtr<nsIFile> workingDir; + rv = NS_GetSpecialDirectory(NS_OS_CURRENT_WORKING_DIR, + getter_AddRefs(workingDir)); + if (NS_FAILED(rv)) { + // No working dir? This can happen if it gets deleted before we start. + workingDir = nullptr; + } + + cmdLine = new nsCommandLine(); + + rv = cmdLine->Init(gArgc, gArgv, workingDir, + nsICommandLine::STATE_INITIAL_LAUNCH); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + // "app-startup" is the name of both the category and the event + NS_CreateServicesFromCategory("app-startup", cmdLine, "app-startup", + nullptr); + + appStartup = components::AppStartup::Service(); + NS_ENSURE_TRUE(appStartup, NS_ERROR_FAILURE); + + mDirProvider.DoStartup(); + +#ifdef XP_WIN + // It needs to be called on the main thread because it has to use + // nsObserverService. + EnsureWin32kInitialized(); +#endif + + // As FilePreferences need the profile directory, we must initialize right + // here. + mozilla::FilePreferences::InitDirectoriesAllowlist(); + mozilla::FilePreferences::InitPrefs(); + + OverrideDefaultLocaleIfNeeded(); + + nsCString userAgentLocale; + LocaleService::GetInstance()->GetAppLocaleAsBCP47(userAgentLocale); + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::useragent_locale, userAgentLocale); + + if (!AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + /* Special-case services that need early access to the command + line. */ + nsCOMPtr<nsIObserverService> obsService = + mozilla::services::GetObserverService(); + if (obsService) { + obsService->NotifyObservers(cmdLine, "command-line-startup", nullptr); + } + } + +#ifdef XP_WIN + // Hack to sync up the various environment storages. XUL_APP_FILE is special + // in that it comes from a different CRT (firefox.exe's static-linked copy). + // Ugly details in http://bugzil.la/1175039#c27 + char appFile[MAX_PATH]; + if (GetEnvironmentVariableA("XUL_APP_FILE", appFile, sizeof(appFile))) { + SmprintfPointer saved = mozilla::Smprintf("XUL_APP_FILE=%s", appFile); + // We intentionally leak the string here since it is required by + // PR_SetEnv. + PR_SetEnv(saved.release()); + } +#endif + + mozilla::AppShutdown::SaveEnvVarsForPotentialRestart(); + + // clear out any environment variables which may have been set + // during the relaunch process now that we know we won't be relaunching. + SaveToEnv("XRE_PROFILE_PATH="); + SaveToEnv("XRE_PROFILE_LOCAL_PATH="); + SaveToEnv("XRE_START_OFFLINE="); + SaveToEnv("XUL_APP_FILE="); + SaveToEnv("XRE_BINARY_PATH="); + SaveToEnv("XRE_RESTARTED_BY_PROFILE_MANAGER="); + + if (!AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { +#ifdef XP_MACOSX + bool lazyHiddenWindow = false; +#else + bool lazyHiddenWindow = + Preferences::GetBool("toolkit.lazyHiddenWindow", false); +#endif + +#ifdef MOZ_BACKGROUNDTASKS + if (BackgroundTasks::IsBackgroundTaskMode()) { + // Background tasks aren't going to load a chrome XUL document. + lazyHiddenWindow = true; + } +#endif + + if (!lazyHiddenWindow) { + rv = appStartup->CreateHiddenWindow(); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + } + +#ifdef XP_WIN + Preferences::RegisterCallbackAndCall( + RegisterApplicationRestartChanged, + PREF_WIN_REGISTER_APPLICATION_RESTART); + SetupAlteredPrefetchPref(); + SetupSkeletonUIPrefs(); +# if defined(MOZ_LAUNCHER_PROCESS) + SetupLauncherProcessPref(); +# endif // defined(MOZ_LAUNCHER_PROCESS) +# if defined(MOZ_DEFAULT_BROWSER_AGENT) +# if defined(MOZ_BACKGROUNDTASKS) + // The backgroundtask profile is not a browsing profile, let alone the new + // default profile, so don't mirror its properties into the registry. + if (!BackgroundTasks::IsBackgroundTaskMode()) +# endif // defined(MOZ_BACKGROUNDTASKS) + { + Preferences::RegisterCallbackAndCall( + &OnDefaultAgentTelemetryPrefChanged, + kPrefHealthReportUploadEnabled); + Preferences::RegisterCallbackAndCall( + &OnDefaultAgentTelemetryPrefChanged, kPrefDefaultAgentEnabled); + + Preferences::RegisterCallbackAndCall( + &OnDefaultAgentRemoteSettingsPrefChanged, + kPrefServicesSettingsServer); + Preferences::RegisterCallbackAndCall( + &OnDefaultAgentRemoteSettingsPrefChanged, + kPrefSecurityContentSignatureRootHash); + + Preferences::RegisterCallbackAndCall( + &OnSetDefaultBrowserUserChoicePrefChanged, + kPrefSetDefaultBrowserUserChoicePref); + + SetDefaultAgentLastRunTime(); + } +# endif // defined(MOZ_DEFAULT_BROWSER_AGENT) +#endif + +#if defined(MOZ_WIDGET_GTK) + // Clear the environment variables so they won't be inherited by child + // processes and confuse things. + for (const auto& name : kStartupTokenNames) { + g_unsetenv(name.get()); + } +#endif + +#ifdef XP_MACOSX + // we re-initialize the command-line service and do appleevents munging + // after we are sure that we're not restarting + cmdLine = new nsCommandLine(); + + char** tempArgv = static_cast<char**>(malloc(gArgc * sizeof(char*))); + for (int i = 0; i < gArgc; i++) { + tempArgv[i] = strdup(gArgv[i]); + } + CommandLineServiceMac::SetupMacCommandLine(gArgc, tempArgv, false); + rv = cmdLine->Init(gArgc, tempArgv, workingDir, + nsICommandLine::STATE_INITIAL_LAUNCH); + free(tempArgv); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + +# ifdef MOZILLA_OFFICIAL + // Check if we're running from a DMG or an app translocated location and + // allow the user to install to the Applications directory. + if (MacRunFromDmgUtils::MaybeInstallAndRelaunch()) { + bool userAllowedQuit = true; + appStartup->Quit(nsIAppStartup::eForceQuit, 0, &userAllowedQuit); + } +# endif +#endif + + nsCOMPtr<nsIObserverService> obsService = + mozilla::services::GetObserverService(); + if (obsService) + obsService->NotifyObservers(nullptr, "final-ui-startup", nullptr); + + (void)appStartup->DoneStartingUp(); + + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::StartupCrash, false); + + AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed); + } + + if (!AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + rv = cmdLine->Run(); + NS_ENSURE_SUCCESS_LOG(rv, NS_ERROR_FAILURE); + } + + if (!AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { +#if defined(MOZ_HAS_REMOTE) + // if we have X remote support, start listening for requests on the + // proxy window. + if (mRemoteService && !mDisableRemoteServer) { + mRemoteService->StartupServer(); + mRemoteService->UnlockStartup(); + gRemoteService = nullptr; + } +#endif /* MOZ_WIDGET_GTK */ + + mNativeApp->Enable(); + } + +#ifdef MOZ_INSTRUMENT_EVENT_LOOP + if (PR_GetEnv("MOZ_INSTRUMENT_EVENT_LOOP")) { + bool logToConsole = true; + mozilla::InitEventTracing(logToConsole); + } +#endif /* MOZ_INSTRUMENT_EVENT_LOOP */ + + // Send Telemetry about Gecko version and buildid + Telemetry::ScalarSet(Telemetry::ScalarID::GECKO_VERSION, + NS_ConvertASCIItoUTF16(gAppData->version)); + Telemetry::ScalarSet(Telemetry::ScalarID::GECKO_BUILD_ID, + NS_ConvertASCIItoUTF16(gAppData->buildID)); + +#if defined(MOZ_SANDBOX) && defined(XP_LINUX) + // If we're on Linux, we now have information about the OS capabilities + // available to us. + SandboxInfo sandboxInfo = SandboxInfo::Get(); + Telemetry::Accumulate(Telemetry::SANDBOX_HAS_SECCOMP_BPF, + sandboxInfo.Test(SandboxInfo::kHasSeccompBPF)); + Telemetry::Accumulate(Telemetry::SANDBOX_HAS_SECCOMP_TSYNC, + sandboxInfo.Test(SandboxInfo::kHasSeccompTSync)); + Telemetry::Accumulate( + Telemetry::SANDBOX_HAS_USER_NAMESPACES_PRIVILEGED, + sandboxInfo.Test(SandboxInfo::kHasPrivilegedUserNamespaces)); + Telemetry::Accumulate(Telemetry::SANDBOX_HAS_USER_NAMESPACES, + sandboxInfo.Test(SandboxInfo::kHasUserNamespaces)); + Telemetry::Accumulate(Telemetry::SANDBOX_CONTENT_ENABLED, + sandboxInfo.Test(SandboxInfo::kEnabledForContent)); + Telemetry::Accumulate(Telemetry::SANDBOX_MEDIA_ENABLED, + sandboxInfo.Test(SandboxInfo::kEnabledForMedia)); + nsAutoCString flagsString; + flagsString.AppendInt(sandboxInfo.AsInteger()); + + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::ContentSandboxCapabilities, flagsString); +#endif /* MOZ_SANDBOX && XP_LINUX */ + +#if defined(XP_WIN) + LauncherResult<bool> isAdminWithoutUac = IsAdminWithoutUac(); + if (isAdminWithoutUac.isOk()) { + Telemetry::ScalarSet( + Telemetry::ScalarID::OS_ENVIRONMENT_IS_ADMIN_WITHOUT_UAC, + isAdminWithoutUac.unwrap()); + } +#endif /* XP_WIN */ + +#if defined(MOZ_SANDBOX) + AddSandboxAnnotations(); +#endif /* MOZ_SANDBOX */ + + mProfileSvc->CompleteStartup(); + } + +#ifdef MOZ_BACKGROUNDTASKS + if (BackgroundTasks::IsBackgroundTaskMode()) { + // In background task mode, we don't fire various delayed initialization + // notifications, which in the regular browser is how startup crash tracking + // is marked as finished. Here, getting this far means we don't have a + // startup crash. + rv = appStartup->TrackStartupCrashEnd(); + NS_ENSURE_SUCCESS(rv, rv); + + // We never open a window, but don't want to exit immediately. + rv = appStartup->EnterLastWindowClosingSurvivalArea(); + NS_ENSURE_SUCCESS(rv, rv); + + // Avoid some small differences in initialization order across platforms. + nsCOMPtr<nsIPowerManagerService> powerManagerService = + do_GetService(POWERMANAGERSERVICE_CONTRACTID); + nsCOMPtr<nsIStringBundleService> stringBundleService = + do_GetService(NS_STRINGBUNDLE_CONTRACTID); + + rv = BackgroundTasks::RunBackgroundTask(cmdLine); + NS_ENSURE_SUCCESS(rv, rv); + } +#endif + + { + rv = appStartup->Run(); + if (NS_FAILED(rv)) { + NS_ERROR("failed to run appstartup"); + gLogConsoleErrors = true; + } + } + + return rv; +} + +#if defined(MOZ_WIDGET_ANDROID) +static already_AddRefed<nsIFile> GreOmniPath(int argc, char** argv) { + nsresult rv; + + const char* path = nullptr; + ArgResult ar = CheckArg(argc, argv, "greomni", &path, CheckArgFlag::None); + if (ar == ARG_BAD) { + PR_fprintf(PR_STDERR, + "Error: argument --greomni requires a path argument\n"); + return nullptr; + } + + if (!path) return nullptr; + + nsCOMPtr<nsIFile> greOmni; + rv = XRE_GetFileFromPath(path, getter_AddRefs(greOmni)); + if (NS_FAILED(rv)) { + PR_fprintf(PR_STDERR, "Error: argument --greomni requires a valid path\n"); + return nullptr; + } + + return greOmni.forget(); +} +#endif + +/* + * XRE_main - A class based main entry point used by most platforms. + * Note that on OSX, aAppData->xreDirectory will point to + * .app/Contents/Resources. + */ +int XREMain::XRE_main(int argc, char* argv[], const BootstrapConfig& aConfig) { + gArgc = argc; + gArgv = argv; + + ScopedLogging log; + + mozilla::LogModule::Init(gArgc, gArgv); + +#ifndef XP_LINUX + NS_SetCurrentThreadName("MainThread"); +#endif + + AUTO_BASE_PROFILER_LABEL("XREMain::XRE_main (around Gecko Profiler)", OTHER); + AUTO_PROFILER_INIT; + AUTO_PROFILER_LABEL("XREMain::XRE_main", OTHER); + +#ifdef XP_MACOSX + // We call this early because it will kick off a background-thread task + // to register the fonts, and we'd like it to have a chance to complete + // before gfxPlatform initialization actually requires it. + gfxPlatformMac::RegisterSupplementalFonts(); +#endif + +#ifdef MOZ_CODE_COVERAGE + CodeCoverageHandler::Init(); +#endif + + nsresult rv = NS_OK; + + if (aConfig.appData) { + mAppData = MakeUnique<XREAppData>(*aConfig.appData); + } else { + MOZ_RELEASE_ASSERT(aConfig.appDataPath); + nsCOMPtr<nsIFile> appini; + rv = XRE_GetFileFromPath(aConfig.appDataPath, getter_AddRefs(appini)); + if (NS_FAILED(rv)) { + Output(true, "Error: unrecognized path: %s\n", aConfig.appDataPath); + return 1; + } + + mAppData = MakeUnique<XREAppData>(); + rv = XRE_ParseAppData(appini, *mAppData); + if (NS_FAILED(rv)) { + Output(true, "Couldn't read application.ini"); + return 1; + } + + appini->GetParent(getter_AddRefs(mAppData->directory)); + } + + if (!mAppData->remotingName) { + mAppData->remotingName = mAppData->name; + } + // used throughout this file + gAppData = mAppData.get(); + + nsCOMPtr<nsIFile> binFile; + rv = XRE_GetBinaryPath(getter_AddRefs(binFile)); + NS_ENSURE_SUCCESS(rv, 1); + + rv = binFile->GetPath(gAbsoluteArgv0Path); + NS_ENSURE_SUCCESS(rv, 1); + + if (!mAppData->xreDirectory) { + nsCOMPtr<nsIFile> greDir; + +#if defined(MOZ_WIDGET_ANDROID) + greDir = GreOmniPath(argc, argv); + if (!greDir) { + return 2; + } +#else + rv = binFile->GetParent(getter_AddRefs(greDir)); + if (NS_FAILED(rv)) return 2; +#endif + +#ifdef XP_MACOSX + nsCOMPtr<nsIFile> parent; + greDir->GetParent(getter_AddRefs(parent)); + greDir = parent.forget(); + greDir->AppendNative("Resources"_ns); +#endif + + mAppData->xreDirectory = greDir; + } + +#if defined(MOZ_WIDGET_ANDROID) + nsCOMPtr<nsIFile> dataDir; + rv = binFile->GetParent(getter_AddRefs(dataDir)); + if (NS_FAILED(rv)) return 2; + + mAppData->directory = dataDir; +#else + if (aConfig.appData && aConfig.appDataPath) { + mAppData->xreDirectory->Clone(getter_AddRefs(mAppData->directory)); + mAppData->directory->AppendNative(nsDependentCString(aConfig.appDataPath)); + } +#endif + + if (!mAppData->directory) { + mAppData->directory = mAppData->xreDirectory; + } + +#if defined(XP_WIN) +# if defined(MOZ_SANDBOX) + mAppData->sandboxBrokerServices = aConfig.sandboxBrokerServices; +# endif // defined(MOZ_SANDBOX) + + { + DebugOnly<bool> result = WindowsBCryptInitialization(); + MOZ_ASSERT(result); + } +#endif // defined(XP_WIN) + + // Once we unset the exception handler, we lose the ability to properly + // detect hangs -- they show up as crashes. We do this as late as possible. + // In particular, after ProcessRuntime is destroyed on Windows. + auto unsetExceptionHandler = MakeScopeExit([&] { + if (mAppData->flags & NS_XRE_ENABLE_CRASH_REPORTER) + return CrashReporter::UnsetExceptionHandler(); + return NS_OK; + }); + + mozilla::IOInterposerInit ioInterposerGuard; + +#if defined(XP_WIN) + // We should have already done this when we created the skeleton UI. However, + // there is code in here which needs xul in order to work, like EnsureMTA. It + // should be setup that running it again is safe. + mozilla::mscom::ProcessRuntime msCOMRuntime; +#endif + + // init + bool exit = false; + int result = XRE_mainInit(&exit); + if (result != 0 || exit) return result; + + // If we exit gracefully, remove the startup crash canary file. + auto cleanup = MakeScopeExit([&]() -> nsresult { + if (mProfLD) { + nsCOMPtr<nsIFile> crashFile; + MOZ_TRY_VAR(crashFile, GetIncompleteStartupFile(mProfLD)); + crashFile->Remove(false); + } + return NS_OK; + }); + + // startup + result = XRE_mainStartup(&exit); + if (result != 0 || exit) return result; + + // Start the real application. We use |aInitJSContext = false| because + // XRE_mainRun wants to initialize the JSContext after reading user prefs. + + mScopedXPCOM = MakeUnique<ScopedXPCOMStartup>(); + + rv = mScopedXPCOM->Initialize(/* aInitJSContext = */ false); + NS_ENSURE_SUCCESS(rv, 1); + + // run! + rv = XRE_mainRun(); + +#ifdef MOZ_X11 + XRE_CleanupX11ErrorHandler(); +#endif + +#ifdef MOZ_INSTRUMENT_EVENT_LOOP + mozilla::ShutdownEventTracing(); +#endif + + gAbsoluteArgv0Path.Truncate(); + +#if defined(MOZ_HAS_REMOTE) + // Shut down the remote service. We must do this before calling LaunchChild + // if we're restarting because otherwise the new instance will attempt to + // remote to this instance. + if (mRemoteService && !mDisableRemoteServer) { + mRemoteService->ShutdownServer(); + } +#endif /* MOZ_WIDGET_GTK */ + + mScopedXPCOM = nullptr; + + // unlock the profile after ScopedXPCOMStartup object (xpcom) + // has gone out of scope. see bug #386739 for more details + mProfileLock->Unlock(); + gProfileLock = nullptr; + + gLastAppVersion.Truncate(); + gLastAppBuildID.Truncate(); + + mozilla::AppShutdown::MaybeDoRestart(); + +#ifdef MOZ_WIDGET_GTK + // gdk_display_close also calls gdk_display_manager_set_default_display + // appropriately when necessary. + if (!gfxPlatform::IsHeadless()) { +# ifdef MOZ_WAYLAND + WaylandDisplayRelease(); +# endif + } +#endif + + XRE_DeinitCommandLine(); + + if (NS_FAILED(rv)) { + return 1; + } + return mozilla::AppShutdown::GetExitCode(); +} + +void XRE_StopLateWriteChecks(void) { mozilla::StopLateWriteChecks(); } + +int XRE_main(int argc, char* argv[], const BootstrapConfig& aConfig) { + XREMain main; + + int result = main.XRE_main(argc, argv, aConfig); + mozilla::RecordShutdownEndTimeStamp(); +#ifdef MOZ_BACKGROUNDTASKS + // This is well after the profile has been unlocked, so it's okay if this does + // delete this background task's temporary profile. + mozilla::BackgroundTasks::Shutdown(); +#endif + return result; +} + +nsresult XRE_InitCommandLine(int aArgc, char* aArgv[]) { + nsresult rv = NS_OK; + +#if defined(OS_WIN) + CommandLine::Init(aArgc, aArgv); +#else + + // these leak on error, but that's OK: we'll just exit() + char** canonArgs = new char*[aArgc]; + + // get the canonical version of the binary's path + nsCOMPtr<nsIFile> binFile; + rv = XRE_GetBinaryPath(getter_AddRefs(binFile)); + if (NS_FAILED(rv)) return NS_ERROR_FAILURE; + + nsAutoCString canonBinPath; + rv = binFile->GetNativePath(canonBinPath); + if (NS_FAILED(rv)) return NS_ERROR_FAILURE; + + canonArgs[0] = strdup(canonBinPath.get()); + + for (int i = 1; i < aArgc; ++i) { + if (aArgv[i]) { + canonArgs[i] = strdup(aArgv[i]); + } + } + + NS_ASSERTION(!CommandLine::IsInitialized(), "Bad news!"); + CommandLine::Init(aArgc, canonArgs); + + for (int i = 0; i < aArgc; ++i) free(canonArgs[i]); + delete[] canonArgs; +#endif + +#if defined(MOZ_WIDGET_ANDROID) + nsCOMPtr<nsIFile> greOmni = + gAppData ? gAppData->xreDirectory : GreOmniPath(aArgc, aArgv); + if (!greOmni) { + return NS_ERROR_FAILURE; + } + mozilla::Omnijar::Init(greOmni, greOmni); +#endif + + return rv; +} + +nsresult XRE_DeinitCommandLine() { + nsresult rv = NS_OK; + + CommandLine::Terminate(); + + return rv; +} + +GeckoProcessType XRE_GetProcessType() { return GetGeckoProcessType(); } + +const char* XRE_GetProcessTypeString() { + return XRE_GeckoProcessTypeToString(XRE_GetProcessType()); +} + +bool XRE_IsE10sParentProcess() { +#ifdef MOZ_WIDGET_ANDROID + return XRE_IsParentProcess() && BrowserTabsRemoteAutostart() && + mozilla::jni::IsAvailable(); +#else + return XRE_IsParentProcess() && BrowserTabsRemoteAutostart(); +#endif +} + +#define GECKO_PROCESS_TYPE(enum_value, enum_name, string_name, proc_typename, \ + process_bin_type, procinfo_typename, \ + webidl_typename, allcaps_name) \ + bool XRE_Is##proc_typename##Process() { \ + return XRE_GetProcessType() == GeckoProcessType_##enum_name; \ + } +#include "mozilla/GeckoProcessTypes.h" +#undef GECKO_PROCESS_TYPE + +bool XRE_UseNativeEventProcessing() { + switch (XRE_GetProcessType()) { +#if defined(XP_MACOSX) || defined(XP_WIN) + case GeckoProcessType_RDD: + case GeckoProcessType_Socket: + return false; + case GeckoProcessType_Utility: { +# if defined(XP_WIN) + auto upc = mozilla::ipc::UtilityProcessChild::Get(); + MOZ_ASSERT(upc); + // WindowsUtils is for Windows APIs, which typically require a Windows + // native event loop. + return upc->mSandbox == mozilla::ipc::SandboxingKind::WINDOWS_UTILS; +# else + return false; +# endif // defined(XP_WIN) + } +#endif // defined(XP_MACOSX) || defined(XP_WIN) + case GeckoProcessType_Content: + return StaticPrefs::dom_ipc_useNativeEventProcessing_content(); + default: + return true; + } +} + +namespace mozilla { + +uint32_t GetMaxWebProcessCount() { + // multiOptOut is in int to allow us to run multiple experiments without + // introducing multiple prefs a la the autostart.N prefs. + if (Preferences::GetInt("dom.ipc.multiOptOut", 0) >= + nsIXULRuntime::E10S_MULTI_EXPERIMENT) { + return 1; + } + + const char* optInPref = "dom.ipc.processCount"; + uint32_t optInPrefValue = Preferences::GetInt(optInPref, 1); + return std::max(1u, optInPrefValue); +} + +const char* PlatformBuildID() { return gToolkitBuildID; } + +} // namespace mozilla + +void SetupErrorHandling(const char* progname) { +#ifdef XP_WIN + /* On Windows XPSP3 and Windows Vista if DEP is configured off-by-default + we still want DEP protection: enable it explicitly and programmatically. + + This function is not available on WinXPSP2 so we dynamically load it. + */ + + HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll"); + SetProcessDEPPolicyFunc _SetProcessDEPPolicy = + (SetProcessDEPPolicyFunc)GetProcAddress(kernel32, "SetProcessDEPPolicy"); + if (_SetProcessDEPPolicy) _SetProcessDEPPolicy(PROCESS_DEP_ENABLE); +#endif + +#ifdef XP_WIN + // Suppress the "DLL Foo could not be found" dialog, such that if dependent + // libraries (such as GDI+) are not preset, we gracefully fail to load those + // XPCOM components, instead of being ungraceful. + UINT realMode = SetErrorMode(0); + realMode |= SEM_FAILCRITICALERRORS; + // If XRE_NO_WINDOWS_CRASH_DIALOG is set, suppress displaying the "This + // application has crashed" dialog box. This is mainly useful for + // automated testing environments, e.g. tinderbox, where there's no need + // for a dozen of the dialog boxes to litter the console + if (getenv("XRE_NO_WINDOWS_CRASH_DIALOG")) + realMode |= SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX; + + SetErrorMode(realMode); + +#endif + + InstallSignalHandlers(progname); + + // Unbuffer stdout, needed for tinderbox tests. + setbuf(stdout, 0); +} + +// Note: This function should not be needed anymore. See Bug 818634 for details. +void OverrideDefaultLocaleIfNeeded() { + // Read pref to decide whether to override default locale with US English. + if (mozilla::Preferences::GetBool("javascript.use_us_english_locale", + false)) { + // Set the application-wide C-locale. Needed to resist fingerprinting + // of Date.toLocaleFormat(). We use the locale to "C.UTF-8" if possible, + // to avoid interfering with non-ASCII keyboard input on some Linux + // desktops. Otherwise fall back to the "C" locale, which is available on + // all platforms. + setlocale(LC_ALL, "C.UTF-8") || setlocale(LC_ALL, "C"); + } +} + +static bool gRunSelfAsContentProc = false; + +void XRE_EnableSameExecutableForContentProc() { + if (!PR_GetEnv("MOZ_SEPARATE_CHILD_PROCESS")) { + gRunSelfAsContentProc = true; + } +} + +mozilla::BinPathType XRE_GetChildProcBinPathType( + GeckoProcessType aProcessType) { + MOZ_ASSERT(aProcessType != GeckoProcessType_Default); + + if (!gRunSelfAsContentProc) { + return BinPathType::PluginContainer; + } + + switch (aProcessType) { +#define GECKO_PROCESS_TYPE(enum_value, enum_name, string_name, proc_typename, \ + process_bin_type, procinfo_typename, \ + webidl_typename, allcaps_name) \ + case GeckoProcessType_##enum_name: \ + return BinPathType::process_bin_type; +#include "mozilla/GeckoProcessTypes.h" +#undef GECKO_PROCESS_TYPE + default: + return BinPathType::PluginContainer; + } +} + +// From mozglue/static/rust/lib.rs +extern "C" void install_rust_hooks(); + +struct InstallRustHooks { + InstallRustHooks() { install_rust_hooks(); } +}; + +InstallRustHooks sInstallRustHooks; + +#ifdef MOZ_ASAN_REPORTER +void setASanReporterPath(nsIFile* aDir) { + nsCOMPtr<nsIFile> dir; + aDir->Clone(getter_AddRefs(dir)); + + dir->Append(u"asan"_ns); + nsresult rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0700); + if (NS_WARN_IF(NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS)) { + MOZ_CRASH("[ASan Reporter] Unable to create crash directory."); + } + + dir->Append(u"ff_asan_log"_ns); + +# ifdef XP_WIN + nsAutoString nspathW; + rv = dir->GetPath(nspathW); + NS_ConvertUTF16toUTF8 nspath(nspathW); +# else + nsAutoCString nspath; + rv = dir->GetNativePath(nspath); +# endif + if (NS_FAILED(rv)) { + MOZ_CRASH("[ASan Reporter] Unable to get native path for crash directory."); + } + + __sanitizer_set_report_path(nspath.get()); +} +#endif diff --git a/toolkit/xre/nsAppRunner.h b/toolkit/xre/nsAppRunner.h new file mode 100644 index 0000000000..2b0315f3a3 --- /dev/null +++ b/toolkit/xre/nsAppRunner.h @@ -0,0 +1,166 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsAppRunner_h__ +#define nsAppRunner_h__ + +#ifdef XP_WIN +# include <windows.h> +# include "mozilla/WindowsConsole.h" +#else +# include <limits.h> +#endif + +#ifndef MAXPATHLEN +# ifdef PATH_MAX +# define MAXPATHLEN PATH_MAX +# elif defined(_MAX_PATH) +# define MAXPATHLEN _MAX_PATH +# elif defined(CCHMAXPATH) +# define MAXPATHLEN CCHMAXPATH +# else +# define MAXPATHLEN 1024 +# endif +#endif + +#include "nsCOMPtr.h" +#include "nsStringFwd.h" +#include "nsXULAppAPI.h" + +class nsINativeAppSupport; +class nsXREDirProvider; +class nsIToolkitProfileService; +class nsIFile; +class nsIProfileLock; +class nsIProfileUnlocker; +class nsIFactory; + +extern nsXREDirProvider* gDirServiceProvider; + +// NOTE: gAppData will be null in embedded contexts. +extern const mozilla::XREAppData* gAppData; +extern bool gSafeMode; +extern bool gFxREmbedded; + +extern int gArgc; +extern char** gArgv; +extern int gRestartArgc; +extern char** gRestartArgv; +extern bool gRestartedByOS; +extern bool gLogConsoleErrors; +extern nsString gAbsoluteArgv0Path; + +extern bool gIsGtest; + +namespace mozilla { +nsresult AppInfoConstructor(const nsID& aIID, void** aResult); +} // namespace mozilla + +// Exported for gtests. +void BuildCompatVersion(const char* aAppVersion, const char* aAppBuildID, + const char* aToolkitBuildID, nsACString& aBuf); + +/** + * Compares the provided compatibility versions. Returns 0 if they match, + * < 0 if the new version is considered an upgrade from the old version and + * > 0 if the new version is considered a downgrade from the old version. + */ +int32_t CompareCompatVersions(const nsACString& aOldCompatVersion, + const nsACString& aNewCompatVersion); + +/** + * Create the nativeappsupport implementation. + * + * @note XPCOMInit has not happened yet. + */ +nsresult NS_CreateNativeAppSupport(nsINativeAppSupport** aResult); +already_AddRefed<nsINativeAppSupport> NS_GetNativeAppSupport(); + +/** + * Try to acquire exclusive access to the specified profile directory. + * + * @param aPath + * The profile directory to lock. + * @param aTempPath + * The corresponding profile temporary directory. + * @param aUnlocker + * A callback interface used to attempt to unlock a profile that + * appears to be locked. + * @param aResult + * The resulting profile lock object (or null if the profile could + * not be locked). + * + * @return NS_ERROR_FILE_ACCESS_DENIED to indicate that the profile + * directory cannot be unlocked. + */ +nsresult NS_LockProfilePath(nsIFile* aPath, nsIFile* aTempPath, + nsIProfileUnlocker** aUnlocker, + nsIProfileLock** aResult); + +void WriteConsoleLog(); + +void OverrideDefaultLocaleIfNeeded(); + +/** + * Allow exit() calls to complete. This should be done from a proper Gecko + * shutdown path. Otherwise we aim to catch improper shutdowns. + */ +void MozExpectedExit(); + +class nsINativeAppSupport; + +// If aBlankCommandLine is true, then the application will be launched with a +// blank command line instead of being launched with the same command line that +// it was initially started with. +// If aTryExec is true then we use exec on platforms that support it to +// remain in the foreground. +nsresult LaunchChild(bool aBlankCommandLine, bool aTryExec = false); + +void UnlockProfile(); + +#ifdef XP_WIN + +BOOL WinLaunchChild(const wchar_t* exePath, int argc, char** argv, + HANDLE userToken = nullptr, HANDLE* hProcess = nullptr); + +# define PREF_WIN_REGISTER_APPLICATION_RESTART \ + "toolkit.winRegisterApplicationRestart" + +# define PREF_WIN_ALTERED_DLL_PREFETCH "startup.experiments.alteredDllPrefetch" + +# if defined(MOZ_LAUNCHER_PROCESS) +# define PREF_WIN_LAUNCHER_PROCESS_ENABLED "browser.launcherProcess.enabled" +# endif // defined(MOZ_LAUNCHER_PROCESS) +#endif + +namespace mozilla { +namespace startup { +Result<nsCOMPtr<nsIFile>, nsresult> GetIncompleteStartupFile(nsIFile* aProfLD); + +void IncreaseDescriptorLimits(); +} // namespace startup + +const char* PlatformBuildID(); + +bool RunningGTest(); + +} // namespace mozilla + +/** + * Set up platform specific error handling such as suppressing DLL load dialog + * and the JIT debugger on Windows, and install unix signal handlers. + */ +void SetupErrorHandling(const char* progname); + +#ifdef MOZ_ASAN_REPORTER +extern "C" { +void MOZ_EXPORT __sanitizer_set_report_path(const char* path); +} +void setASanReporterPath(nsIFile* aDir); +#endif + +bool IsWaylandEnabled(); + +#endif // nsAppRunner_h__ diff --git a/toolkit/xre/nsCommandLineServiceMac.h b/toolkit/xre/nsCommandLineServiceMac.h new file mode 100644 index 0000000000..caa37fdc52 --- /dev/null +++ b/toolkit/xre/nsCommandLineServiceMac.h @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsCommandLineServiceMac_h_ +#define nsCommandLineServiceMac_h_ + +#include "nscore.h" + +namespace CommandLineServiceMac { +void SetupMacCommandLine(int& argc, char**& argv, bool forRestart); + +// Add a URL to the command line currently being set up via +// SetupMacCommandLine. Returns false if no command line is +// being set up or the addition fails for any other reason. +bool AddURLToCurrentCommandLine(const char* aURL); +} // namespace CommandLineServiceMac + +#endif // nsCommandLineServiceMac_h_ diff --git a/toolkit/xre/nsCommandLineServiceMac.mm b/toolkit/xre/nsCommandLineServiceMac.mm new file mode 100644 index 0000000000..e143a41fad --- /dev/null +++ b/toolkit/xre/nsCommandLineServiceMac.mm @@ -0,0 +1,94 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsCommandLineServiceMac.h" +#include "MacApplicationDelegate.h" +#include <cstring> +#include <Cocoa/Cocoa.h> + +namespace CommandLineServiceMac { + +static const int kArgsGrowSize = 20; + +static char** sArgs = nullptr; +static int sArgsAllocated = 0; +static int sArgsUsed = 0; + +static bool sBuildingCommandLine = false; + +void AddToCommandLine(const char* inArgText) { + if (sArgsUsed >= sArgsAllocated - 1) { + // realloc does not free the given pointer if allocation fails + char** temp = + static_cast<char**>(realloc(sArgs, (sArgsAllocated + kArgsGrowSize) * sizeof(char*))); + if (!temp) return; + sArgs = temp; + sArgsAllocated += kArgsGrowSize; + } + + char* temp2 = strdup(inArgText); + if (!temp2) return; + + sArgs[sArgsUsed++] = temp2; + sArgs[sArgsUsed] = nullptr; + + return; +} + +// Caller has ownership of argv and is responsible for freeing the allocated +// memory. +void SetupMacCommandLine(int& argc, char**& argv, bool forRestart) { + sArgs = static_cast<char**>(malloc(kArgsGrowSize * sizeof(char*))); + if (!sArgs) return; + sArgsAllocated = kArgsGrowSize; + sArgs[0] = nullptr; + sArgsUsed = 0; + + sBuildingCommandLine = true; + + // Copy args, stripping anything we don't want. + for (int arg = 0; arg < argc; arg++) { + char* flag = argv[arg]; + // Don't pass on the psn (Process Serial Number) flag from the OS, or + // the "-foreground" flag since it will be set below if necessary. + if (strncmp(flag, "-psn_", 5) != 0 && strncmp(flag, "-foreground", 11) != 0) + AddToCommandLine(flag); + } + + // Force processing of any pending Apple GetURL Events while we're building + // the command line. The handlers will append to the command line rather than + // act directly so there is no chance we'll process them during a XUL window + // load and accidentally open unnecessary windows and home pages. + ProcessPendingGetURLAppleEvents(); + + // If the process will be relaunched, the child should be in the foreground + // if the parent is in the foreground. This will be communicated in a + // command-line argument to the child. + if (forRestart) { + NSRunningApplication* frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; + if ([frontApp isEqual:[NSRunningApplication currentApplication]]) { + AddToCommandLine("-foreground"); + } + } + + sBuildingCommandLine = false; + + free(argv); + argc = sArgsUsed; + argv = sArgs; +} + +bool AddURLToCurrentCommandLine(const char* aURL) { + if (!sBuildingCommandLine) { + return false; + } + + AddToCommandLine("-url"); + AddToCommandLine(aURL); + + return true; +} + +} // namespace CommandLineServiceMac diff --git a/toolkit/xre/nsConsoleWriter.cpp b/toolkit/xre/nsConsoleWriter.cpp new file mode 100644 index 0000000000..d89ea3bde3 --- /dev/null +++ b/toolkit/xre/nsConsoleWriter.cpp @@ -0,0 +1,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 "nsAppRunner.h" + +#include "prio.h" +#include "prprf.h" +#include "prenv.h" + +#include "nsCRT.h" +#include "nsNativeCharsetUtils.h" +#include "nsString.h" +#include "nsXREDirProvider.h" +#include "nsXULAppAPI.h" + +#include "nsIConsoleService.h" +#include "nsIConsoleMessage.h" + +void WriteConsoleLog() { + nsresult rv; + + nsCOMPtr<nsIFile> lfile; + + char* logFileEnv = PR_GetEnv("XRE_CONSOLE_LOG"); + if (logFileEnv && *logFileEnv) { + rv = XRE_GetFileFromPath(logFileEnv, getter_AddRefs(lfile)); + if (NS_FAILED(rv)) return; + } else { + if (!gLogConsoleErrors) return; + + rv = nsXREDirProvider::GetUserAppDataDirectory(getter_AddRefs(lfile)); + if (NS_FAILED(rv)) return; + + lfile->AppendNative("console.log"_ns); + } + + PRFileDesc* file; + rv = lfile->OpenNSPRFileDesc(PR_WRONLY | PR_APPEND | PR_CREATE_FILE, 0660, + &file); + if (NS_FAILED(rv)) return; + + nsCOMPtr<nsIConsoleService> csrv(do_GetService(NS_CONSOLESERVICE_CONTRACTID)); + if (!csrv) { + PR_Close(file); + return; + } + + nsTArray<RefPtr<nsIConsoleMessage>> messages; + + rv = csrv->GetMessageArray(messages); + if (NS_FAILED(rv)) { + PR_Close(file); + return; + } + + if (!messages.IsEmpty()) { + PRExplodedTime etime; + PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &etime); + char datetime[512]; + PR_FormatTimeUSEnglish(datetime, sizeof(datetime), "%Y-%m-%d %H:%M:%S", + &etime); + + PR_fprintf(file, NS_LINEBREAK "*** Console log: %s ***" NS_LINEBREAK, + datetime); + } + + nsString msg; + nsAutoCString nativemsg; + + for (auto& message : messages) { + rv = message->GetMessageMoz(msg); + if (NS_SUCCEEDED(rv)) { + NS_CopyUnicodeToNative(msg, nativemsg); + PR_fprintf(file, "%s" NS_LINEBREAK, nativemsg.get()); + } + } + + PR_Close(file); +} diff --git a/toolkit/xre/nsEmbedFunctions.cpp b/toolkit/xre/nsEmbedFunctions.cpp new file mode 100644 index 0000000000..fa236e0f86 --- /dev/null +++ b/toolkit/xre/nsEmbedFunctions.cpp @@ -0,0 +1,833 @@ +/* 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 "mozilla/DebugOnly.h" + +#include "nsXULAppAPI.h" + +#include <stdlib.h> +#if defined(MOZ_WIDGET_GTK) +# include <glib.h> +#endif + +#include "prenv.h" + +#include "nsIAppShell.h" +#include "nsIToolkitProfile.h" + +#ifdef XP_WIN +# include <process.h> +# include <shobjidl.h> +# include "mozilla/ipc/WindowsMessageLoop.h" +# ifdef MOZ_SANDBOX +# include "mozilla/RandomNum.h" +# endif +# include "mozilla/ScopeExit.h" +# include "mozilla/WinDllServices.h" +# include "mozilla/WindowsBCryptInitialization.h" +# include "WinUtils.h" +#endif + +#include "nsAppRunner.h" +#include "nsExceptionHandler.h" +#include "mozilla/RuntimeExceptionModule.h" +#include "nsThreadUtils.h" +#include "nsJSUtils.h" +#include "nsWidgetsCID.h" +#include "nsXREDirProvider.h" +#ifdef MOZ_ASAN_REPORTER +# include "CmdLineAndEnvUtils.h" +# include "nsIFile.h" +#endif + +#include "mozilla/Omnijar.h" +#if defined(XP_MACOSX) +# include <mach/mach.h> +# include <servers/bootstrap.h> +# include "nsVersionComparator.h" +# include "chrome/common/mach_ipc_mac.h" +# include "gfxPlatformMac.h" +#endif +#include "nsX11ErrorHandler.h" +#include "nsGDKErrorHandler.h" +#include "base/at_exit.h" +#include "base/message_loop.h" +#include "base/process_util.h" +#if defined(MOZ_WIDGET_ANDROID) +# include "chrome/common/ipc_channel.h" +# include "mozilla/jni/Utils.h" +# include "mozilla/ipc/ProcessUtils.h" +#endif // defined(MOZ_WIDGET_ANDROID) + +#include "mozilla/AbstractThread.h" +#include "mozilla/FilePreferences.h" +#include "mozilla/IOInterposer.h" +#include "mozilla/ProcessType.h" +#include "mozilla/RDDProcessImpl.h" +#include "mozilla/ipc/UtilityProcessImpl.h" +#include "mozilla/UniquePtr.h" + +#include "mozilla/ipc/BrowserProcessSubThread.h" +#include "mozilla/ipc/IOThreadChild.h" +#include "mozilla/ipc/ProcessChild.h" + +#include "mozilla/dom/ContentProcess.h" +#include "mozilla/dom/ContentParent.h" + +#include "mozilla/ipc/TestShellParent.h" +#if defined(XP_WIN) +# include "mozilla/WindowsConsole.h" +# include "mozilla/WindowsDllBlocklist.h" +#endif + +#include "GMPProcessChild.h" +#include "mozilla/gfx/GPUProcessImpl.h" +#include "mozilla/net/SocketProcessImpl.h" + +#include "ProfilerControl.h" + +#if defined(MOZ_SANDBOX) && defined(XP_WIN) +# include "mozilla/sandboxTarget.h" +# include "mozilla/sandboxing/loggingCallbacks.h" +# include "mozilla/RemoteSandboxBrokerProcessChild.h" +#endif + +#if defined(MOZ_SANDBOX) +# include "XREChildData.h" +# include "mozilla/SandboxSettings.h" +#endif + +#if defined(XP_LINUX) && defined(MOZ_SANDBOX) +# include "mozilla/Sandbox.h" +#endif + +#if defined(XP_LINUX) +# include <sys/prctl.h> +# ifndef PR_SET_PTRACER +# define PR_SET_PTRACER 0x59616d61 +# endif +# ifndef PR_SET_PTRACER_ANY +# define PR_SET_PTRACER_ANY ((unsigned long)-1) +# endif +#endif + +#ifdef MOZ_JPROF +# include "jprof.h" +#endif + +#if defined(XP_WIN) && defined(MOZ_SANDBOX) +# include "mozilla/sandboxing/SandboxInitialization.h" +# include "mozilla/sandboxing/sandboxLogging.h" +#endif + +#if defined(MOZ_ENABLE_FORKSERVER) +# include "mozilla/ipc/ForkServer.h" +#endif + +#if defined(MOZ_X11) +# include <X11/Xlib.h> +#endif + +#include "VRProcessChild.h" + +using namespace mozilla; + +using mozilla::ipc::BrowserProcessSubThread; +using mozilla::ipc::GeckoChildProcessHost; +using mozilla::ipc::IOThreadChild; +using mozilla::ipc::ProcessChild; + +using mozilla::dom::ContentParent; +using mozilla::dom::ContentProcess; + +using mozilla::gmp::GMPProcessChild; + +using mozilla::ipc::TestShellCommandParent; +using mozilla::ipc::TestShellParent; + +namespace mozilla::_ipdltest { +// Set in IPDLUnitTest.cpp when running gtests. +UniquePtr<mozilla::ipc::ProcessChild> (*gMakeIPDLUnitTestProcessChild)( + base::ProcessId, const nsID&) = nullptr; +} // namespace mozilla::_ipdltest + +static NS_DEFINE_CID(kAppShellCID, NS_APPSHELL_CID); + +const char* XRE_GeckoProcessTypeToString(GeckoProcessType aProcessType) { + switch (aProcessType) { +#define GECKO_PROCESS_TYPE(enum_value, enum_name, string_name, proc_typename, \ + process_bin_type, procinfo_typename, \ + webidl_typename, allcaps_name) \ + case GeckoProcessType::GeckoProcessType_##enum_name: \ + return string_name; +#include "mozilla/GeckoProcessTypes.h" +#undef GECKO_PROCESS_TYPE + default: + return "invalid"; + } +} + +const char* XRE_ChildProcessTypeToAnnotation(GeckoProcessType aProcessType) { + switch (aProcessType) { + case GeckoProcessType_GMPlugin: + return "plugin"; + case GeckoProcessType_Default: + return ""; + case GeckoProcessType_Content: + return "content"; + default: + return XRE_GeckoProcessTypeToString(aProcessType); + } +} + +#if defined(MOZ_WIDGET_ANDROID) +void XRE_SetAndroidChildFds(JNIEnv* env, const XRE_AndroidChildFds& fds) { + mozilla::jni::SetGeckoThreadEnv(env); + mozilla::ipc::SetPrefsFd(fds.mPrefsFd); + mozilla::ipc::SetPrefMapFd(fds.mPrefMapFd); + IPC::Channel::SetClientChannelFd(fds.mIpcFd); + CrashReporter::SetNotificationPipeForChild(fds.mCrashFd); + CrashReporter::SetCrashAnnotationPipeForChild(fds.mCrashAnnotationFd); +} +#endif // defined(MOZ_WIDGET_ANDROID) + +void XRE_SetProcessType(const char* aProcessTypeString) { + SetGeckoProcessType(aProcessTypeString); +} + +#if defined(XP_WIN) +void SetTaskbarGroupId(const nsString& aId) { + if (FAILED(SetCurrentProcessExplicitAppUserModelID(aId.get()))) { + NS_WARNING( + "SetCurrentProcessExplicitAppUserModelID failed for child process."); + } +} +#endif + +#if defined(MOZ_SANDBOX) +void AddContentSandboxLevelAnnotation() { + if (XRE_GetProcessType() == GeckoProcessType_Content) { + int level = GetEffectiveContentSandboxLevel(); + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::ContentSandboxLevel, level); + } else if (XRE_GetProcessType() == GeckoProcessType_GPU) { + int level = GetEffectiveGpuSandboxLevel(); + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::GpuSandboxLevel, level); + } +} +#endif /* MOZ_SANDBOX */ + +namespace { + +int GetDebugChildPauseTime() { + auto pauseStr = PR_GetEnv("MOZ_DEBUG_CHILD_PAUSE"); + if (pauseStr && *pauseStr) { + int pause = atoi(pauseStr); + if (pause != 1) { // must be !=1 since =1 enables the default pause time +#if defined(OS_WIN) + pause *= 1000; // convert to ms +#endif + return pause; + } + } +#ifdef OS_POSIX + return 30; // seconds +#elif defined(OS_WIN) + return 10000; // milliseconds +#else + return 0; +#endif +} + +static bool IsCrashReporterEnabled(const char* aArg) { + // on windows and mac, |aArg| is the named pipe on which the server is + // listening for requests, or "-" if crash reporting is disabled. +#if defined(XP_MACOSX) || defined(XP_WIN) + return 0 != strcmp("-", aArg); +#else + // on POSIX, |aArg| is "true" if crash reporting is enabled, false otherwise + return 0 != strcmp("false", aArg); +#endif +} + +} // namespace + +nsresult XRE_InitChildProcess(int aArgc, char* aArgv[], + const XREChildData* aChildData) { + NS_ENSURE_ARG_MIN(aArgc, 2); + NS_ENSURE_ARG_POINTER(aArgv); + NS_ENSURE_ARG_POINTER(aArgv[0]); + MOZ_ASSERT(aChildData); + + NS_SetCurrentThreadName("MainThread"); + +#ifdef MOZ_ASAN_REPORTER + // In ASan reporter builds, we need to set ASan's log_path as early as + // possible, so it dumps its errors into files there instead of using + // the default stderr location. Since this is crucial for ASan reporter + // to work at all (and we don't want people to use a non-functional + // ASan reporter build), all failures while setting log_path are fatal. + // + // We receive this log_path via the ASAN_REPORTER_PATH environment variable + // because there is no other way to generically get the necessary profile + // directory in all child types without adding support for that in each + // child process type class (at the risk of missing this in a child). + // + // In certain cases (e.g. child startup through xpcshell or gtests), this + // code needs to remain disabled, as no ASAN_REPORTER_PATH would be available. + if (!PR_GetEnv("MOZ_DISABLE_ASAN_REPORTER") && !PR_GetEnv("MOZ_RUN_GTEST")) { + nsCOMPtr<nsIFile> asanReporterPath = GetFileFromEnv("ASAN_REPORTER_PATH"); + if (!asanReporterPath) { + MOZ_CRASH("Child did not receive ASAN_REPORTER_PATH!"); + } + setASanReporterPath(asanReporterPath); + } +#endif + +#if defined(XP_LINUX) && defined(MOZ_SANDBOX) + // This has to happen before glib thread pools are started. + mozilla::SandboxEarlyInit(); + // This just needs to happen before sandboxing, to initialize the + // cached value, but libmozsandbox can't see this symbol. + mozilla::GetNumberOfProcessors(); +#endif + +#ifdef MOZ_JPROF + // Call the code to install our handler + setupProfilingStuff(); +#endif + +#if defined(XP_WIN) + // From the --attach-console support in nsNativeAppSupportWin.cpp, but + // here we are a content child process, so we always attempt to attach + // to the parent's (ie, the browser's) console. + // Try to attach console to the parent process. + // It will succeed when the parent process is a command line, + // so that stdio will be displayed in it. + UseParentConsole(); + +# if defined(MOZ_SANDBOX) + if (aChildData->sandboxTargetServices) { + SandboxTarget::Instance()->SetTargetServices( + aChildData->sandboxTargetServices); + } +# endif +#endif + + // NB: This must be called before profiler_init + ScopedLogging logger; + + mozilla::LogModule::Init(aArgc, aArgv); + + AUTO_BASE_PROFILER_LABEL("XRE_InitChildProcess (around Gecko Profiler)", + OTHER); + AUTO_PROFILER_INIT; + AUTO_PROFILER_LABEL("XRE_InitChildProcess", OTHER); + +#ifdef XP_MACOSX + gfxPlatformMac::RegisterSupplementalFonts(); +#endif + + // Ensure AbstractThread is minimally setup, so async IPC messages + // work properly. + AbstractThread::InitTLS(); + + // Complete 'task_t' exchange for Mac OS X. This structure has the same size + // regardless of architecture so we don't have any cross-arch issues here. +#ifdef XP_MACOSX + if (aArgc < 1) return NS_ERROR_FAILURE; + +# if defined(MOZ_SANDBOX) + // Save the original number of arguments to pass to the sandbox + // setup routine which also uses the crash server argument. + int allArgc = aArgc; +# endif /* MOZ_SANDBOX */ + + // Acquire the mach bootstrap port name from our command line, and send our + // task_t to the parent process. + const char* const mach_port_name = aArgv[--aArgc]; + + const int kTimeoutMs = 1000; + + UniqueMachSendRight task_sender; + kern_return_t kr = bootstrap_look_up(bootstrap_port, mach_port_name, + getter_Transfers(task_sender)); + if (kr != KERN_SUCCESS) { + NS_WARNING(nsPrintfCString("child bootstrap_look_up failed: %s", + mach_error_string(kr)) + .get()); + return NS_ERROR_FAILURE; + } + + kr = MachSendPortSendRight(task_sender.get(), mach_task_self(), + Some(kTimeoutMs)); + if (kr != KERN_SUCCESS) { + NS_WARNING(nsPrintfCString("child MachSendPortSendRight failed: %s", + mach_error_string(kr)) + .get()); + return NS_ERROR_FAILURE; + } + +# if defined(MOZ_SANDBOX) + std::string sandboxError; + if (!GeckoChildProcessHost::StartMacSandbox(allArgc, aArgv, sandboxError)) { + printf_stderr("Sandbox error: %s\n", sandboxError.c_str()); + MOZ_CRASH("Sandbox initialization failed"); + } +# endif /* MOZ_SANDBOX */ + +#endif /* XP_MACOSX */ + + SetupErrorHandling(aArgv[0]); + + bool exceptionHandlerIsSet = false; + if (!CrashReporter::IsDummy()) { + CrashReporter::FileHandle crashTimeAnnotationFile = + CrashReporter::kInvalidFileHandle; +#if defined(XP_WIN) + if (aArgc < 1) { + return NS_ERROR_FAILURE; + } + // Pop the first argument, this is used by the WER runtime exception module + // which reads it from the command-line so we can just discard it here. + --aArgc; + + const char* const crashTimeAnnotationArg = aArgv[--aArgc]; + crashTimeAnnotationFile = reinterpret_cast<CrashReporter::FileHandle>( + std::stoul(std::string(crashTimeAnnotationArg))); +#endif + + if (aArgc < 1) return NS_ERROR_FAILURE; + const char* const crashReporterArg = aArgv[--aArgc]; + + if (IsCrashReporterEnabled(crashReporterArg)) { + exceptionHandlerIsSet = CrashReporter::SetRemoteExceptionHandler( + crashReporterArg, crashTimeAnnotationFile); + MOZ_ASSERT(exceptionHandlerIsSet, + "Should have been able to set remote exception handler"); + + if (!exceptionHandlerIsSet) { + // Bug 684322 will add better visibility into this condition + NS_WARNING("Could not setup crash reporting\n"); + } + } else { + // We might have registered a runtime exception module very early in + // process startup to catch early crashes. This is before we process the + // crash reporter arg, so unregister here if it turns out the crash + // reporter is disabled. + CrashReporter::UnregisterRuntimeExceptionModule(); + } + } + + gArgv = aArgv; + gArgc = aArgc; + +#ifdef MOZ_X11 + XInitThreads(); +#endif +#ifdef MOZ_WIDGET_GTK + // Setting the name here avoids the need to pass this through to gtk_init(). + g_set_prgname(aArgv[0]); +#endif + +#ifdef OS_POSIX + if (PR_GetEnv("MOZ_DEBUG_CHILD_PROCESS") || + PR_GetEnv("MOZ_DEBUG_CHILD_PAUSE")) { +# if defined(XP_LINUX) && defined(DEBUG) + if (prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0) != 0) { + printf_stderr("Could not allow ptrace from any process.\n"); + } +# endif + printf_stderr( + "\n\nCHILDCHILDCHILDCHILD (process type %s)\n debug me @ %d\n\n", + XRE_GetProcessTypeString(), base::GetCurrentProcId()); + sleep(GetDebugChildPauseTime()); + } +#elif defined(OS_WIN) + if (PR_GetEnv("MOZ_DEBUG_CHILD_PROCESS")) { + NS_DebugBreak(NS_DEBUG_BREAK, + "Invoking NS_DebugBreak() to debug child process", nullptr, + __FILE__, __LINE__); + } else if (PR_GetEnv("MOZ_DEBUG_CHILD_PAUSE")) { + printf_stderr( + "\n\nCHILDCHILDCHILDCHILD (process type %s)\n debug me @ %lu\n\n", + XRE_GetProcessTypeString(), base::GetCurrentProcId()); + ::Sleep(GetDebugChildPauseTime()); + } +#endif + +#ifdef MOZ_WIDGET_ANDROID + // The parent process already did this, but Gecko child processes on + // Android aren't descendants of the parent process, so they don't + // inherit its rlimits. + mozilla::startup::IncreaseDescriptorLimits(); +#endif + + // child processes launched by GeckoChildProcessHost get this magic + // argument appended to their command lines + const char* const parentPIDString = aArgv[aArgc - 1]; + MOZ_ASSERT(parentPIDString, "NULL parent PID"); + --aArgc; + + char* end = 0; + base::ProcessId parentPID = strtol(parentPIDString, &end, 10); + MOZ_ASSERT(!*end, "invalid parent PID"); + + // They also get the initial message channel ID passed in the same manner. + const char* const messageChannelIdString = aArgv[aArgc - 1]; + MOZ_ASSERT(messageChannelIdString, "NULL MessageChannel Id"); + --aArgc; + + nsID messageChannelId{}; + if (!messageChannelId.Parse(messageChannelIdString)) { + return NS_ERROR_FAILURE; + } + +#if defined(XP_WIN) + // On Win7+, when not running as an MSIX package, register the application + // user model id passed in by parent. This ensures windows created by the + // container properly group with the parent app on the Win7 taskbar. + // MSIX packages explicitly do not support setting the appid from within + // the app, as it is set in the package manifest instead. + const char* const appModelUserId = aArgv[--aArgc]; + if (appModelUserId && !mozilla::widget::WinUtils::HasPackageIdentity()) { + // '-' implies no support + if (*appModelUserId != '-') { + nsString appId; + CopyASCIItoUTF16(nsDependentCString(appModelUserId), appId); + // The version string is encased in quotes + appId.Trim("\""); + // Set the id + SetTaskbarGroupId(appId); + } + } +#endif + + base::AtExitManager exitManager; + + nsresult rv = XRE_InitCommandLine(aArgc, aArgv); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + MessageLoop::Type uiLoopType; + switch (XRE_GetProcessType()) { + case GeckoProcessType_Content: + case GeckoProcessType_GPU: + case GeckoProcessType_IPDLUnitTest: + case GeckoProcessType_VR: + case GeckoProcessType_RDD: + case GeckoProcessType_Socket: + case GeckoProcessType_Utility: + // Content processes need the XPCOM/chromium frankenventloop + uiLoopType = MessageLoop::TYPE_MOZILLA_CHILD; + break; + case GeckoProcessType_GMPlugin: + case GeckoProcessType_RemoteSandboxBroker: + uiLoopType = MessageLoop::TYPE_DEFAULT; + break; + default: + uiLoopType = MessageLoop::TYPE_UI; + break; + } + +#if defined(XP_WIN) +# if defined(MOZ_SANDBOX) + if (aChildData->sandboxBrokerServices) { + SandboxBroker::Initialize(aChildData->sandboxBrokerServices); + SandboxBroker::GeckoDependentInitialize(); + } +# endif // defined(MOZ_SANDBOX) + + { + DebugOnly<bool> result = mozilla::WindowsBCryptInitialization(); + MOZ_ASSERT(result); + } +#endif // defined(XP_WIN) + + { + // This is a lexical scope for the MessageLoop below. We want it + // to go out of scope before NS_LogTerm() so that we don't get + // spurious warnings about XPCOM objects being destroyed from a + // static context. + + Maybe<IOInterposerInit> ioInterposerGuard; + + // Associate this thread with a UI MessageLoop + MessageLoop uiMessageLoop(uiLoopType); + { + UniquePtr<ProcessChild> process; + switch (XRE_GetProcessType()) { + case GeckoProcessType_Default: + MOZ_CRASH("This makes no sense"); + break; + + case GeckoProcessType_Content: + ioInterposerGuard.emplace(); + process = MakeUnique<ContentProcess>(parentPID, messageChannelId); + break; + + case GeckoProcessType_IPDLUnitTest: + MOZ_RELEASE_ASSERT(mozilla::_ipdltest::gMakeIPDLUnitTestProcessChild, + "xul-gtest not loaded!"); + process = mozilla::_ipdltest::gMakeIPDLUnitTestProcessChild( + parentPID, messageChannelId); + break; + + case GeckoProcessType_GMPlugin: + process = + MakeUnique<gmp::GMPProcessChild>(parentPID, messageChannelId); + break; + + case GeckoProcessType_GPU: + process = + MakeUnique<gfx::GPUProcessImpl>(parentPID, messageChannelId); + break; + + case GeckoProcessType_VR: + process = + MakeUnique<gfx::VRProcessChild>(parentPID, messageChannelId); + break; + + case GeckoProcessType_RDD: + process = MakeUnique<RDDProcessImpl>(parentPID, messageChannelId); + break; + + case GeckoProcessType_Socket: + ioInterposerGuard.emplace(); + process = + MakeUnique<net::SocketProcessImpl>(parentPID, messageChannelId); + break; + + case GeckoProcessType_Utility: + process = + MakeUnique<ipc::UtilityProcessImpl>(parentPID, messageChannelId); + break; + +#if defined(MOZ_SANDBOX) && defined(XP_WIN) + case GeckoProcessType_RemoteSandboxBroker: + process = MakeUnique<RemoteSandboxBrokerProcessChild>( + parentPID, messageChannelId); + break; +#endif + +#if defined(MOZ_ENABLE_FORKSERVER) + case GeckoProcessType_ForkServer: + MOZ_CRASH("Fork server should not go here"); + break; +#endif + default: + MOZ_CRASH("Unknown main thread class"); + } + + if (!process->Init(aArgc, aArgv)) { + return NS_ERROR_FAILURE; + } + +#if defined(XP_WIN) + // Set child processes up such that they will get killed after the + // chrome process is killed in cases where the user shuts the system + // down or logs off. + ::SetProcessShutdownParameters(0x280 - 1, SHUTDOWN_NORETRY); + + RefPtr<DllServices> dllSvc(DllServices::Get()); + auto dllSvcDisable = + MakeScopeExit([&dllSvc]() { dllSvc->DisableFull(); }); +#endif + +#if defined(MOZ_SANDBOX) && defined(XP_WIN) + // We need to do this after the process has been initialised, as + // InitLoggingIfRequired may need access to prefs. + mozilla::sandboxing::InitLoggingIfRequired( + aChildData->ProvideLogFunction); +#endif + if (XRE_GetProcessType() != GeckoProcessType_RemoteSandboxBroker) { + // Remote sandbox launcher process doesn't have prerequisites for + // these... + mozilla::FilePreferences::InitDirectoriesAllowlist(); + mozilla::FilePreferences::InitPrefs(); + OverrideDefaultLocaleIfNeeded(); + } + +#if defined(MOZ_SANDBOX) + AddContentSandboxLevelAnnotation(); +#endif + + // Run the UI event loop on the main thread. + uiMessageLoop.MessageLoop::Run(); + + // Allow ProcessChild to clean up after itself before going out of + // scope and being deleted + process->CleanUp(); + mozilla::Omnijar::CleanUp(); + } + } + + CrashReporter::UnsetRemoteExceptionHandler(exceptionHandlerIsSet); + + return XRE_DeinitCommandLine(); +} + +MessageLoop* XRE_GetIOMessageLoop() { + if (GetGeckoProcessType() == GeckoProcessType_Default) { + return BrowserProcessSubThread::GetMessageLoop(BrowserProcessSubThread::IO); + } + return IOThreadChild::message_loop(); +} + +nsresult XRE_RunAppShell() { + nsCOMPtr<nsIAppShell> appShell(do_GetService(kAppShellCID)); + NS_ENSURE_TRUE(appShell, NS_ERROR_FAILURE); +#if defined(XP_MACOSX) + if (XRE_UseNativeEventProcessing()) { + // In content processes that want XPCOM (and hence want + // AppShell), we usually run our hybrid event loop through + // MessagePump::Run(), by way of nsBaseAppShell::Run(). The + // Cocoa nsAppShell impl, however, implements its own Run() + // that's unaware of MessagePump. That's all rather suboptimal, + // but oddly enough not a problem... usually. + // + // The problem with this setup comes during startup. + // XPCOM-in-subprocesses depends on IPC, e.g. to init the pref + // service, so we have to init IPC first. But, IPC also + // indirectly kinda-depends on XPCOM, because MessagePump + // schedules work from off-main threads (e.g. IO thread) by + // using NS_DispatchToMainThread(). If the IO thread receives a + // Message from the parent before nsThreadManager is + // initialized, then DispatchToMainThread() will fail, although + // MessagePump will remember the task. This race condition + // isn't a problem when appShell->Run() ends up in + // MessagePump::Run(), because MessagePump will immediate see it + // has work to do. It *is* a problem when we end up in [NSApp + // run], because it's not aware that MessagePump has work that + // needs to be processed; that was supposed to be signaled by + // nsIRunnable(s). + // + // So instead of hacking Cocoa nsAppShell or rewriting the + // event-loop system, we compromise here by processing any tasks + // that might have been enqueued on MessagePump, *before* + // MessagePump::ScheduleWork was able to successfully + // DispatchToMainThread(). + MessageLoop* loop = MessageLoop::current(); + bool couldNest = loop->NestableTasksAllowed(); + + loop->SetNestableTasksAllowed(true); + RefPtr<Runnable> task = new MessageLoop::QuitTask(); + loop->PostTask(task.forget()); + loop->Run(); + + loop->SetNestableTasksAllowed(couldNest); + } +#endif // XP_MACOSX + return appShell->Run(); +} + +void XRE_ShutdownChildProcess() { + MOZ_ASSERT(NS_IsMainThread(), "Wrong thread!"); + + mozilla::DebugOnly<MessageLoop*> ioLoop = XRE_GetIOMessageLoop(); + MOZ_ASSERT(!!ioLoop, "Bad shutdown order"); + + // Quit() sets off the following chain of events + // (1) UI loop starts quitting + // (2) UI loop returns from Run() in XRE_InitChildProcess() + // (3) ProcessChild goes out of scope and terminates the IO thread + // (4) ProcessChild joins the IO thread + // (5) exit() + MessageLoop::current()->Quit(); + +#if defined(XP_MACOSX) + nsCOMPtr<nsIAppShell> appShell(do_GetService(kAppShellCID)); + if (appShell) { + // On Mac, we might be only above nsAppShell::Run(), not + // MessagePump::Run(). See XRE_RunAppShell(). To account for + // that case, we fire off an Exit() here. If we were indeed + // above MessagePump::Run(), this Exit() is just superfluous. + appShell->Exit(); + } +#endif // XP_MACOSX +} + +namespace { +ContentParent* gContentParent; // long-lived, manually refcounted +TestShellParent* GetOrCreateTestShellParent() { + if (!gContentParent) { + // Use a "web" child process by default. File a bug if you don't like + // this and you're sure you wouldn't be better off writing a "browser" + // chrome mochitest where you can have multiple types of content + // processes. + RefPtr<ContentParent> parent = + ContentParent::GetNewOrUsedBrowserProcess(DEFAULT_REMOTE_TYPE); + parent.forget(&gContentParent); + } else if (gContentParent->IsShuttingDown()) { + return nullptr; + } + TestShellParent* tsp = gContentParent->GetTestShellSingleton(); + if (!tsp) { + tsp = gContentParent->CreateTestShell(); + } + return tsp; +} + +} // namespace + +bool XRE_SendTestShellCommand(JSContext* aCx, JSString* aCommand, + JS::Value* aCallback) { + JS::Rooted<JSString*> cmd(aCx, aCommand); + TestShellParent* tsp = GetOrCreateTestShellParent(); + NS_ENSURE_TRUE(tsp, false); + + nsAutoJSString command; + NS_ENSURE_TRUE(command.init(aCx, cmd), false); + + if (!aCallback) { + return tsp->SendExecuteCommand(command); + } + + TestShellCommandParent* callback = static_cast<TestShellCommandParent*>( + tsp->SendPTestShellCommandConstructor(command)); + NS_ENSURE_TRUE(callback, false); + + NS_ENSURE_TRUE(callback->SetCallback(aCx, *aCallback), false); + + return true; +} + +bool XRE_ShutdownTestShell() { + if (!gContentParent) { + return true; + } + bool ret = true; + if (gContentParent->IsAlive()) { + ret = gContentParent->DestroyTestShell( + gContentParent->GetTestShellSingleton()); + } + NS_RELEASE(gContentParent); + return ret; +} + +#ifdef MOZ_X11 +void XRE_InstallX11ErrorHandler() { +# ifdef MOZ_WIDGET_GTK + InstallGdkErrorHandler(); +# endif + + // Ensure our X11 error handler overrides the default GDK error handler such + // that errors are ignored by default. GDK will install its own error handler + // temporarily when pushing error traps internally as needed. This avoids us + // otherwise having to frequently override the error handler merely to trap + // errors in multiple places that would otherwise contend with GDK or other + // libraries that might also override the handler. + InstallX11ErrorHandler(); +} + +void XRE_CleanupX11ErrorHandler() { CleanupX11ErrorHandler(); } +#endif + +#ifdef MOZ_ENABLE_FORKSERVER +int XRE_ForkServer(int* aArgc, char*** aArgv) { + return mozilla::ipc::ForkServer::RunForkServer(aArgc, aArgv) ? 1 : 0; +} +#endif diff --git a/toolkit/xre/nsGDKErrorHandler.cpp b/toolkit/xre/nsGDKErrorHandler.cpp new file mode 100644 index 0000000000..19895cff0a --- /dev/null +++ b/toolkit/xre/nsGDKErrorHandler.cpp @@ -0,0 +1,117 @@ +/* -*- Mode: C++; tab-width: 40; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsGDKErrorHandler.h" + +#include <gtk/gtk.h> +#ifdef MOZ_X11 +# include <gdk/gdkx.h> +#endif +#include <errno.h> +#include <stdlib.h> +#include <string.h> + +#include "nsDebug.h" +#include "nsString.h" +#ifdef MOZ_X11 +# include "nsX11ErrorHandler.h" +#endif + +#include "prenv.h" + +/* See https://bugzilla.gnome.org/show_bug.cgi?id=629608#c8 + * + * GDK implements X11 error traps to ignore X11 errors. + * Unfortunatelly We don't know which X11 events can be ignored + * so we have to utilize the Gdk error handler to avoid + * false alarms in Gtk3. + */ +static void GdkErrorHandler(const gchar* log_domain, GLogLevelFlags log_level, + const gchar* message, gpointer user_data) { +#ifdef MOZ_X11 + if (strstr(message, "X Window System error")) { + XErrorEvent event; + nsDependentCString buffer(message); + char* endptr; + + /* Parse Gdk X Window error message which has this format: + * (Details: serial XXXX error_code XXXX request_code XXXX (XXXX) minor_code + * XXXX) + */ + constexpr auto serialString = "(Details: serial "_ns; + int32_t start = buffer.Find(serialString); + if (start == kNotFound) { + MOZ_CRASH_UNSAFE(message); + } + + start += serialString.Length(); + errno = 0; + event.serial = strtol(buffer.BeginReading() + start, &endptr, 10); + if (errno) { + MOZ_CRASH_UNSAFE(message); + } + + constexpr auto errorCodeString = " error_code "_ns; + if (!StringBeginsWith(Substring(endptr, buffer.EndReading()), + errorCodeString)) { + MOZ_CRASH_UNSAFE(message); + } + + errno = 0; + event.error_code = strtol(endptr + errorCodeString.Length(), &endptr, 10); + if (errno) { + MOZ_CRASH_UNSAFE(message); + } + + constexpr auto requestCodeString = " request_code "_ns; + if (!StringBeginsWith(Substring(endptr, buffer.EndReading()), + requestCodeString)) { + MOZ_CRASH_UNSAFE(message); + } + + errno = 0; + event.request_code = + strtol(endptr + requestCodeString.Length(), &endptr, 10); + if (errno) { + MOZ_CRASH_UNSAFE(message); + } + + constexpr auto minorCodeString = " minor_code "_ns; + start = buffer.Find(minorCodeString, endptr - buffer.BeginReading()); + if (!start) { + MOZ_CRASH_UNSAFE(message); + } + + errno = 0; + event.minor_code = strtol( + buffer.BeginReading() + start + minorCodeString.Length(), nullptr, 10); + if (errno) { + MOZ_CRASH_UNSAFE(message); + } + + event.display = GDK_DISPLAY_XDISPLAY(gdk_display_get_default()); + // Gdk does not provide resource ID + event.resourceid = 0; + + X11Error(event.display, &event); + } else +#endif + { + g_log_default_handler(log_domain, log_level, message, user_data); + MOZ_CRASH_UNSAFE(message); + } +} + +void InstallGdkErrorHandler() { + g_log_set_handler("Gdk", + (GLogLevelFlags)(G_LOG_LEVEL_ERROR | G_LOG_FLAG_FATAL | + G_LOG_FLAG_RECURSION), + GdkErrorHandler, nullptr); +#ifdef MOZ_X11 + if (PR_GetEnv("MOZ_X_SYNC")) { + XSynchronize(GDK_DISPLAY_XDISPLAY(gdk_display_get_default()), X11True); + } +#endif +} diff --git a/toolkit/xre/nsGDKErrorHandler.h b/toolkit/xre/nsGDKErrorHandler.h new file mode 100644 index 0000000000..7c6cdce41f --- /dev/null +++ b/toolkit/xre/nsGDKErrorHandler.h @@ -0,0 +1,8 @@ +/* -*- Mode: C++; tab-width: 40; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifdef MOZ_WIDGET_GTK +void InstallGdkErrorHandler(); +#endif diff --git a/toolkit/xre/nsINativeAppSupport.idl b/toolkit/xre/nsINativeAppSupport.idl new file mode 100644 index 0000000000..7cfce02ab4 --- /dev/null +++ b/toolkit/xre/nsINativeAppSupport.idl @@ -0,0 +1,55 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsISupports.idl" + +/* nsINativeAppSupport + * + * This "pseudo" (in the XPCOM sense) interface provides for + * platform-specific general application support + * + * Due to the nature of the beast, this interface is not a full-blown + * XPCOM component. The primary reason is that objects that implement + * this interface generally must be operational *before* XPCOM (or any + * of the rest of Mozilla) are initialized. As a result, this + * interface is instantiated by somewhat unconventional means. + * + * To create the implementor of this interface, you call the function + * NS_CreateNativeAppSupport. This is done in the startup code + * in nsAppRunner.cpp + * + * The interface provides these functions: + * start - You call this to inform the native app support that the + * application is starting. In addition, it serves as a + * query as to whether the application should continue to + * run. + * + * If the returned boolean result is PR_FALSE, then the + * application should exit without further processing. In + * such cases, the returned nsresult indicates whether the + * reason to exit is due to an error or not. + * + * Win32 Note: In the case of starting a second instance + * of this executable, this function will return + * PR_FALSE and nsresult==NS_OK. This means that + * the command line arguments have been + * successfully passed to the instance of the + * application acting as a remote server. + * quit - Informs the native app support that the application is stopping. The + * app support should disable any functionality enabled by start. + * + * onLastWindowClosing - Called when the last window is closed. Used as a + * "soft" shutdown, passwords are flushed. + */ + +[scriptable, uuid(5fdf8480-1f98-11d4-8077-00600811a9c3)] +interface nsINativeAppSupport : nsISupports { + // Startup/shutdown. + boolean start(); + void enable(); + + void onLastWindowClosing(); + void ReOpen(); +}; diff --git a/toolkit/xre/nsIWinAppHelper.idl b/toolkit/xre/nsIWinAppHelper.idl new file mode 100644 index 0000000000..c8a602977c --- /dev/null +++ b/toolkit/xre/nsIWinAppHelper.idl @@ -0,0 +1,19 @@ +/* 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 "nsISupports.idl" + +/** + * A scriptable interface used on Windows only to do some work from + * a special process that gets created with elevated privileges. + * + * @status UNSTABLE - This interface is not frozen and will probably change in + * future releases. + */ + +[scriptable, uuid(dc263ca8-b257-47eb-b5b7-339d9e0b90f7)] +interface nsIWinAppHelper : nsISupports +{ + readonly attribute boolean userCanElevate; +}; diff --git a/toolkit/xre/nsIXREDirProvider.idl b/toolkit/xre/nsIXREDirProvider.idl new file mode 100644 index 0000000000..ce599a83bc --- /dev/null +++ b/toolkit/xre/nsIXREDirProvider.idl @@ -0,0 +1,23 @@ +/* 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 "nsISupports.idl" + +interface nsIFile; + +[scriptable, uuid(f6ee3c0a-5119-47fc-b1a7-ace9e1111fff)] +interface nsIXREDirProvider : nsISupports +{ + /** + * Only intended to be used from xpcshell tests. Allows setting the local + * and normal profile data directories. Calling this after something using + * them has started up will cause problems. + */ + void setUserDataDirectory(in nsIFile aFile, in boolean aLocal); + + /** + * Gets the hash for the current installation directory. + */ + AString getInstallHash(); +}; diff --git a/toolkit/xre/nsNativeAppSupportBase.cpp b/toolkit/xre/nsNativeAppSupportBase.cpp new file mode 100644 index 0000000000..43c329ec86 --- /dev/null +++ b/toolkit/xre/nsNativeAppSupportBase.cpp @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsNativeAppSupportBase.h" + +nsNativeAppSupportBase::nsNativeAppSupportBase() = default; + +nsNativeAppSupportBase::~nsNativeAppSupportBase() = default; + +NS_IMPL_ISUPPORTS(nsNativeAppSupportBase, nsINativeAppSupport) + +// Start answer defaults to OK. +NS_IMETHODIMP +nsNativeAppSupportBase::Start(bool* result) { + *result = true; + return NS_OK; +} + +NS_IMETHODIMP +nsNativeAppSupportBase::Enable() { return NS_OK; } + +NS_IMETHODIMP +nsNativeAppSupportBase::ReOpen() { return NS_OK; } + +NS_IMETHODIMP +nsNativeAppSupportBase::OnLastWindowClosing() { return NS_OK; } diff --git a/toolkit/xre/nsNativeAppSupportBase.h b/toolkit/xre/nsNativeAppSupportBase.h new file mode 100644 index 0000000000..851ea358fe --- /dev/null +++ b/toolkit/xre/nsNativeAppSupportBase.h @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsNativeAppSupportBase_h__ +#define nsNativeAppSupportBase_h__ + +#include "nsAppRunner.h" +#include "nsINativeAppSupport.h" + +// nsNativeAppSupportBase +// +// This is a default implementation of the nsINativeAppSupport interface +// declared in mozilla/xpfe/appshell/public/nsINativeAppSupport.h. + +class nsNativeAppSupportBase : public nsINativeAppSupport { + public: + nsNativeAppSupportBase(); + + NS_DECL_ISUPPORTS + NS_DECL_NSINATIVEAPPSUPPORT + + protected: + virtual ~nsNativeAppSupportBase(); +}; + +#endif diff --git a/toolkit/xre/nsNativeAppSupportCocoa.mm b/toolkit/xre/nsNativeAppSupportCocoa.mm new file mode 100644 index 0000000000..1fe480b699 --- /dev/null +++ b/toolkit/xre/nsNativeAppSupportCocoa.mm @@ -0,0 +1,178 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsString.h" + +#import <CoreServices/CoreServices.h> +#import <Cocoa/Cocoa.h> + +#include "nsCOMPtr.h" +#include "nsCocoaFeatures.h" +#include "nsNativeAppSupportBase.h" +#include "nsServiceManagerUtils.h" + +#include "nsIBaseWindow.h" +#include "nsCommandLine.h" +#include "mozIDOMWindow.h" +#include "nsIWebNavigation.h" +#include "nsIWidget.h" +#include "nsIWindowMediator.h" +#include "nsPIDOMWindow.h" +#include "WidgetUtils.h" + +// This must be included last: +#include "nsObjCExceptions.h" + +using mozilla::widget::WidgetUtils; + +nsresult GetNativeWindowPointerFromDOMWindow(mozIDOMWindowProxy* a_window, + NSWindow** a_nativeWindow) { + *a_nativeWindow = nil; + if (!a_window) return NS_ERROR_INVALID_ARG; + + nsPIDOMWindowOuter* win = nsPIDOMWindowOuter::From(a_window); + nsCOMPtr<nsIWidget> widget = WidgetUtils::DOMWindowToWidget(win); + if (!widget) { + return NS_ERROR_FAILURE; + } + + *a_nativeWindow = (NSWindow*)widget->GetNativeData(NS_NATIVE_WINDOW); + + return NS_OK; +} + +class nsNativeAppSupportCocoa : public nsNativeAppSupportBase { + public: + nsNativeAppSupportCocoa() : mCanShowUI(false) {} + + NS_IMETHOD Start(bool* aRetVal) override; + NS_IMETHOD ReOpen() override; + NS_IMETHOD Enable() override; + + private: + bool mCanShowUI; +}; + +NS_IMETHODIMP +nsNativeAppSupportCocoa::Enable() { + mCanShowUI = true; + return NS_OK; +} + +NS_IMETHODIMP nsNativeAppSupportCocoa::Start(bool* _retval) { + int major, minor, bugfix; + nsCocoaFeatures::GetSystemVersion(major, minor, bugfix); + + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + // Check that the OS version is supported, if not return false, + // which will make the browser quit. In principle we could display an + // alert here. But the alert's message and buttons would require custom + // localization. So (for now at least) we just log an English message + // to the console before quitting. + if (major < 10 || (major == 10 && minor < 12)) { + NSLog(@"Minimum OS version requirement not met!"); + return NS_OK; + } + + *_retval = true; + + return NS_OK; + + NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); +} + +NS_IMETHODIMP +nsNativeAppSupportCocoa::ReOpen() { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + if (!mCanShowUI) return NS_ERROR_FAILURE; + + bool haveNonMiniaturized = false; + bool haveOpenWindows = false; + bool done = false; + + nsCOMPtr<nsIWindowMediator> wm(do_GetService(NS_WINDOWMEDIATOR_CONTRACTID)); + if (!wm) { + return NS_ERROR_FAILURE; + } else { + nsCOMPtr<nsISimpleEnumerator> windowList; + wm->GetAppWindowEnumerator(nullptr, getter_AddRefs(windowList)); + bool more; + windowList->HasMoreElements(&more); + while (more) { + nsCOMPtr<nsISupports> nextWindow = nullptr; + windowList->GetNext(getter_AddRefs(nextWindow)); + nsCOMPtr<nsIBaseWindow> baseWindow(do_QueryInterface(nextWindow)); + if (!baseWindow) { + windowList->HasMoreElements(&more); + continue; + } else { + haveOpenWindows = true; + } + + nsCOMPtr<nsIWidget> widget = nullptr; + baseWindow->GetMainWidget(getter_AddRefs(widget)); + if (!widget || !widget->IsVisible()) { + windowList->HasMoreElements(&more); + continue; + } + NSWindow* cocoaWindow = (NSWindow*)widget->GetNativeData(NS_NATIVE_WINDOW); + if (![cocoaWindow isMiniaturized]) { + haveNonMiniaturized = true; + break; // have un-minimized windows, nothing to do + } + windowList->HasMoreElements(&more); + } // end while + + if (!haveNonMiniaturized) { + // Prioritize browser windows for deminiaturization + nsCOMPtr<mozIDOMWindowProxy> mru; + wm->GetMostRecentBrowserWindow(getter_AddRefs(mru)); + + // Failing that, deminiaturize the most recently used window + if (!mru) { + wm->GetMostRecentWindow(nullptr, getter_AddRefs(mru)); + } + + if (mru) { + NSWindow* cocoaMru = nil; + GetNativeWindowPointerFromDOMWindow(mru, &cocoaMru); + if (cocoaMru) { + [cocoaMru deminiaturize:nil]; + done = true; + } + } + } // end if have non miniaturized + + if (!haveOpenWindows && !done) { + char* argv[] = {nullptr}; + + // use an empty command line to make the right kind(s) of window open + nsCOMPtr<nsICommandLineRunner> cmdLine(new nsCommandLine()); + + nsresult rv; + rv = cmdLine->Init(0, argv, nullptr, nsICommandLine::STATE_REMOTE_EXPLICIT); + NS_ENSURE_SUCCESS(rv, rv); + + return cmdLine->Run(); + } + + } // got window mediator + return NS_OK; + + NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); +} + +#pragma mark - + +// Create and return an instance of class nsNativeAppSupportCocoa. +nsresult NS_CreateNativeAppSupport(nsINativeAppSupport** aResult) { + *aResult = new nsNativeAppSupportCocoa; + if (!*aResult) return NS_ERROR_OUT_OF_MEMORY; + + NS_ADDREF(*aResult); + return NS_OK; +} diff --git a/toolkit/xre/nsNativeAppSupportDefault.cpp b/toolkit/xre/nsNativeAppSupportDefault.cpp new file mode 100644 index 0000000000..a65ac4e1a5 --- /dev/null +++ b/toolkit/xre/nsNativeAppSupportDefault.cpp @@ -0,0 +1,15 @@ +/* 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 "nsNativeAppSupportBase.h" + +nsresult NS_CreateNativeAppSupport(nsINativeAppSupport** aResult) { + nsNativeAppSupportBase* native = new nsNativeAppSupportBase(); + if (!native) return NS_ERROR_OUT_OF_MEMORY; + + *aResult = native; + NS_ADDREF(*aResult); + + return NS_OK; +} diff --git a/toolkit/xre/nsNativeAppSupportUnix.cpp b/toolkit/xre/nsNativeAppSupportUnix.cpp new file mode 100644 index 0000000000..bed9e546db --- /dev/null +++ b/toolkit/xre/nsNativeAppSupportUnix.cpp @@ -0,0 +1,662 @@ +/* -*- 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 "nsNativeAppSupportBase.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsXPCOM.h" +#include "nsISupportsPrimitives.h" +#include "nsIObserverService.h" +#include "nsIAppStartup.h" +#include "nsServiceManagerUtils.h" +#include "prlink.h" +#include "nsXREDirProvider.h" +#include "nsReadableUtils.h" + +#include "nsIFile.h" +#include "nsDirectoryServiceDefs.h" +#include "nsPIDOMWindow.h" +#include "nsIWidget.h" +#include "mozilla/Logging.h" +#include "mozilla/Services.h" +#include "mozilla/XREAppData.h" + +#include <stdlib.h> +#include <glib.h> +#include <glib-object.h> +#include <gtk/gtk.h> + +#ifdef MOZ_X11 +# include <gdk/gdkx.h> +# include <X11/ICE/ICElib.h> +# include <X11/SM/SMlib.h> +# include <fcntl.h> +# include "nsThreadUtils.h" + +# include <pwd.h> +#endif + +#ifdef MOZ_ENABLE_DBUS +# include <dbus/dbus.h> +#endif + +#define MIN_GTK_MAJOR_VERSION 2 +#define MIN_GTK_MINOR_VERSION 10 +#define UNSUPPORTED_GTK_MSG \ + "We're sorry, this application requires a version of the GTK+ library that is not installed on your computer.\n\n\ +You have GTK+ %d.%d.\nThis application requires GTK+ %d.%d or newer.\n\n\ +Please upgrade your GTK+ library if you wish to use this application." + +#if MOZ_X11 +# undef IceSetIOErrorHandler +# undef IceAddConnectionWatch +# undef IceConnectionNumber +# undef IceProcessMessages +# undef IceGetConnectionContext +# undef SmcInteractDone +# undef SmcSaveYourselfDone +# undef SmcInteractRequest +# undef SmcCloseConnection +# undef SmcOpenConnection +# undef SmcSetProperties + +typedef IceIOErrorHandler (*IceSetIOErrorHandlerFn)(IceIOErrorHandler); +typedef int (*IceAddConnectionWatchFn)(IceWatchProc, IcePointer); +typedef int (*IceConnectionNumberFn)(IceConn); +typedef IceProcessMessagesStatus (*IceProcessMessagesFn)(IceConn, + IceReplyWaitInfo*, + Bool*); +typedef IcePointer (*IceGetConnectionContextFn)(IceConn); + +typedef void (*SmcInteractDoneFn)(SmcConn, Bool); +typedef void (*SmcSaveYourselfDoneFn)(SmcConn, Bool); +typedef int (*SmcInteractRequestFn)(SmcConn, int, SmcInteractProc, SmPointer); +typedef SmcCloseStatus (*SmcCloseConnectionFn)(SmcConn, int, char**); +typedef SmcConn (*SmcOpenConnectionFn)(char*, SmPointer, int, int, + unsigned long, SmcCallbacks*, + const char*, char**, int, char*); +typedef void (*SmcSetPropertiesFn)(SmcConn, int, SmProp**); + +static IceSetIOErrorHandlerFn IceSetIOErrorHandlerPtr; +static IceAddConnectionWatchFn IceAddConnectionWatchPtr; +static IceConnectionNumberFn IceConnectionNumberPtr; +static IceProcessMessagesFn IceProcessMessagesPtr; +static IceGetConnectionContextFn IceGetConnectionContextPtr; +static SmcInteractDoneFn SmcInteractDonePtr; +static SmcSaveYourselfDoneFn SmcSaveYourselfDonePtr; +static SmcInteractRequestFn SmcInteractRequestPtr; +static SmcCloseConnectionFn SmcCloseConnectionPtr; +static SmcOpenConnectionFn SmcOpenConnectionPtr; +static SmcSetPropertiesFn SmcSetPropertiesPtr; + +# define IceSetIOErrorHandler IceSetIOErrorHandlerPtr +# define IceAddConnectionWatch IceAddConnectionWatchPtr +# define IceConnectionNumber IceConnectionNumberPtr +# define IceProcessMessages IceProcessMessagesPtr +# define IceGetConnectionContext IceGetConnectionContextPtr +# define SmcInteractDone SmcInteractDonePtr +# define SmcSaveYourselfDone SmcSaveYourselfDonePtr +# define SmcInteractRequest SmcInteractRequestPtr +# define SmcCloseConnection SmcCloseConnectionPtr +# define SmcOpenConnection SmcOpenConnectionPtr +# define SmcSetProperties SmcSetPropertiesPtr + +enum ClientState { + STATE_DISCONNECTED, + STATE_REGISTERING, + STATE_IDLE, + STATE_INTERACTING, + STATE_SHUTDOWN_CANCELLED +}; + +using namespace mozilla; + +static const char* gClientStateTable[] = {"DISCONNECTED", "REGISTERING", "IDLE", + "INTERACTING", "SHUTDOWN_CANCELLED"}; + +static LazyLogModule sMozSMLog("MozSM"); +#endif /* MOZ_X11 */ + +class nsNativeAppSupportUnix : public nsNativeAppSupportBase { + public: +#if MOZ_X11 + nsNativeAppSupportUnix() + : mSessionConnection(nullptr), mClientState(STATE_DISCONNECTED){}; + ~nsNativeAppSupportUnix() { + // this goes out of scope after "web-workers-shutdown" async shutdown phase + // so it's safe to disconnect here (i.e. the application won't lose data) + DisconnectFromSM(); + }; + + void DisconnectFromSM(); +#endif + NS_IMETHOD Start(bool* aRetVal) override; + NS_IMETHOD Enable() override; + + private: +#if MOZ_X11 + static void SaveYourselfCB(SmcConn smc_conn, SmPointer client_data, + int save_style, Bool shutdown, int interact_style, + Bool fast); + static void DieCB(SmcConn smc_conn, SmPointer client_data); + static void InteractCB(SmcConn smc_conn, SmPointer client_data); + static void SaveCompleteCB(SmcConn smc_conn, SmPointer client_data){}; + static void ShutdownCancelledCB(SmcConn smc_conn, SmPointer client_data); + void DoInteract(); + void SetClientState(ClientState aState) { + mClientState = aState; + MOZ_LOG(sMozSMLog, LogLevel::Debug, + ("New state = %s\n", gClientStateTable[aState])); + } + + SmcConn mSessionConnection; + ClientState mClientState; +#endif +}; + +#if MOZ_X11 +static gboolean process_ice_messages(IceConn connection) { + IceProcessMessagesStatus status; + + status = IceProcessMessages(connection, nullptr, nullptr); + + switch (status) { + case IceProcessMessagesSuccess: + return TRUE; + + case IceProcessMessagesIOError: { + nsNativeAppSupportUnix* native = static_cast<nsNativeAppSupportUnix*>( + IceGetConnectionContext(connection)); + native->DisconnectFromSM(); + } + return FALSE; + + case IceProcessMessagesConnectionClosed: + return FALSE; + + default: + g_assert_not_reached(); + } +} + +static gboolean ice_iochannel_watch(GIOChannel* channel, GIOCondition condition, + gpointer client_data) { + return process_ice_messages(static_cast<IceConn>(client_data)); +} + +static void ice_connection_watch(IceConn connection, IcePointer client_data, + Bool opening, IcePointer* watch_data) { + guint watch_id; + + if (opening) { + GIOChannel* channel; + int fd = IceConnectionNumber(connection); + + fcntl(fd, F_SETFD, fcntl(fd, F_GETFD, 0) | FD_CLOEXEC); + channel = g_io_channel_unix_new(fd); + watch_id = + g_io_add_watch(channel, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR), + ice_iochannel_watch, connection); + g_io_channel_unref(channel); + + *watch_data = GUINT_TO_POINTER(watch_id); + } else { + watch_id = GPOINTER_TO_UINT(*watch_data); + g_source_remove(watch_id); + } +} + +static void ice_io_error_handler(IceConn connection) { + // override the default handler which would exit the application; + // do nothing and let ICELib handle the failure of the connection gracefully. +} + +static void ice_init(void) { + static bool initted = false; + + if (!initted) { + IceSetIOErrorHandler(ice_io_error_handler); + IceAddConnectionWatch(ice_connection_watch, nullptr); + initted = true; + } +} + +void nsNativeAppSupportUnix::InteractCB(SmcConn smc_conn, + SmPointer client_data) { + nsNativeAppSupportUnix* self = + static_cast<nsNativeAppSupportUnix*>(client_data); + + self->SetClientState(STATE_INTERACTING); + + // We do this asynchronously, as we spin the event loop recursively if + // a dialog is displayed. If we do this synchronously, we don't finish + // processing the current ICE event whilst the dialog is displayed, which + // means we won't process any more. libsm hates us if we do the InteractDone + // with a pending ShutdownCancelled, and we would certainly like to handle Die + // whilst a dialog is displayed + NS_DispatchToCurrentThread( + NewRunnableMethod("nsNativeAppSupportUnix::DoInteract", self, + &nsNativeAppSupportUnix::DoInteract)); +} + +void nsNativeAppSupportUnix::DoInteract() { + nsCOMPtr<nsIObserverService> obsServ = + mozilla::services::GetObserverService(); + if (!obsServ) { + SmcInteractDone(mSessionConnection, False); + SmcSaveYourselfDone(mSessionConnection, True); + SetClientState(STATE_IDLE); + return; + } + + nsCOMPtr<nsISupportsPRBool> cancelQuit = + do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID); + + bool abortQuit = false; + if (cancelQuit) { + cancelQuit->SetData(false); + obsServ->NotifyObservers(cancelQuit, "quit-application-requested", nullptr); + + cancelQuit->GetData(&abortQuit); + } + + if (!abortQuit && mClientState == STATE_DISCONNECTED) { + // The session manager disappeared, whilst we were interacting, so + // quit now + nsCOMPtr<nsIAppStartup> appService = + do_GetService("@mozilla.org/toolkit/app-startup;1"); + + if (appService) { + bool userAllowedQuit = true; + appService->Quit(nsIAppStartup::eForceQuit, 0, &userAllowedQuit); + } + } else { + if (mClientState != STATE_SHUTDOWN_CANCELLED) { + // Only do this if the shutdown wasn't cancelled + SmcInteractDone(mSessionConnection, !!abortQuit); + SmcSaveYourselfDone(mSessionConnection, !abortQuit); + } + + SetClientState(STATE_IDLE); + } +} + +void nsNativeAppSupportUnix::SaveYourselfCB(SmcConn smc_conn, + SmPointer client_data, + int save_style, Bool shutdown, + int interact_style, Bool fast) { + nsNativeAppSupportUnix* self = + static_cast<nsNativeAppSupportUnix*>(client_data); + + // Expect a SaveYourselfCB if we're registering a new client. + // All properties are already set in Start() so just reply with + // SmcSaveYourselfDone if the callback matches the expected signature. + // + // Ancient versions (?) of xsm do not follow such an early SaveYourself with + // SaveComplete. This is a problem if the application freezes interaction + // while waiting for a response to SmcSaveYourselfDone. So never freeze + // interaction when in STATE_REGISTERING. + // + // That aside, we could treat each combination of flags appropriately and not + // special-case this. + if (self->mClientState == STATE_REGISTERING) { + self->SetClientState(STATE_IDLE); + + if (save_style == SmSaveLocal && interact_style == SmInteractStyleNone && + !shutdown && !fast) { + SmcSaveYourselfDone(self->mSessionConnection, True); + return; + } + } + + if (self->mClientState == STATE_SHUTDOWN_CANCELLED) { + // The last shutdown request was cancelled whilst we were interacting, + // and we haven't finished interacting yet. Switch the state back again + self->SetClientState(STATE_INTERACTING); + } + + nsCOMPtr<nsIObserverService> obsServ = + mozilla::services::GetObserverService(); + if (!obsServ) { + SmcSaveYourselfDone(smc_conn, True); + return; + } + + bool status = false; + nsCOMPtr<nsISupportsPRBool> didSaveSession = + do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID); + + if (!didSaveSession) { + SmcSaveYourselfDone(smc_conn, True); + return; + } + + // Notify observers to save the session state + didSaveSession->SetData(false); + obsServ->NotifyObservers(didSaveSession, "session-save", nullptr); + + didSaveSession->GetData(&status); + + // If the interact style permits us to, we are shutting down and we didn't + // manage to (or weren't asked to) save the local state, then notify the user + // in advance that we are doing to quit (assuming that we aren't already + // doing so) + if (!status && shutdown && interact_style != SmInteractStyleNone) { + if (self->mClientState != STATE_INTERACTING) { + SmcInteractRequest(smc_conn, SmDialogNormal, + nsNativeAppSupportUnix::InteractCB, client_data); + } + } else { + SmcSaveYourselfDone(smc_conn, True); + } +} + +void nsNativeAppSupportUnix::DieCB(SmcConn smc_conn, SmPointer client_data) { + nsCOMPtr<nsIAppStartup> appService = + do_GetService("@mozilla.org/toolkit/app-startup;1"); + + if (appService) { + bool userAllowedQuit = false; + appService->Quit(nsIAppStartup::eForceQuit, 0, &userAllowedQuit); + } + // Quit causes the shutdown to begin but the shutdown process is asynchronous + // so we can't DisconnectFromSM() yet +} + +void nsNativeAppSupportUnix::ShutdownCancelledCB(SmcConn smc_conn, + SmPointer client_data) { + nsNativeAppSupportUnix* self = + static_cast<nsNativeAppSupportUnix*>(client_data); + + // Interacting is the only time when we wouldn't already have called + // SmcSaveYourselfDone. Do that now, then set the state to make sure we + // don't send it again after finishing interacting + if (self->mClientState == STATE_INTERACTING) { + SmcSaveYourselfDone(smc_conn, False); + self->SetClientState(STATE_SHUTDOWN_CANCELLED); + } +} + +void nsNativeAppSupportUnix::DisconnectFromSM() { + // the SM is free to exit any time after we disconnect, so callers must be + // sure to have reached a sufficiently advanced phase of shutdown that there + // is no risk of data loss: + // e.g. all async writes are complete by the end of "profile-before-change" + if (mSessionConnection) { + SetClientState(STATE_DISCONNECTED); + SmcCloseConnection(mSessionConnection, 0, nullptr); + mSessionConnection = nullptr; + gdk_x11_set_sm_client_id(nullptr); // follow gnome-client behaviour + } +} + +static void SetSMValue(SmPropValue& val, const nsCString& data) { + val.value = static_cast<SmPointer>(const_cast<char*>(data.get())); + val.length = data.Length(); +} + +static void SetSMProperty(SmProp& prop, const char* name, const char* type, + int numVals, SmPropValue vals[]) { + prop.name = const_cast<char*>(name); + prop.type = const_cast<char*>(type); + prop.num_vals = numVals; + prop.vals = vals; +} +#endif /* MOZ_X11 */ + +static void RemoveArg(char** argv) { + do { + *argv = *(argv + 1); + ++argv; + } while (*argv); + + --gArgc; +} + +NS_IMETHODIMP +nsNativeAppSupportUnix::Start(bool* aRetVal) { + NS_ASSERTION(gAppData, "gAppData must not be null."); + +// The dbus library is used by both nsWifiScannerDBus and BluetoothDBusService, +// from diffrent threads. This could lead to race conditions if the dbus is not +// initialized before making any other library calls. +#ifdef MOZ_ENABLE_DBUS + dbus_threads_init_default(); +#endif + + *aRetVal = true; + +#ifdef MOZ_X11 + gboolean sm_disable = FALSE; + if (!getenv("SESSION_MANAGER")) { + sm_disable = TRUE; + } + + nsAutoCString prev_client_id; + + char** curarg = gArgv + 1; + while (*curarg) { + char* arg = *curarg; + if (arg[0] == '-' && arg[1] == '-') { + arg += 2; + if (!strcmp(arg, "sm-disable")) { + RemoveArg(curarg); + sm_disable = TRUE; + continue; + } else if (!strcmp(arg, "sm-client-id")) { + RemoveArg(curarg); + if (*curarg[0] != '-') { + prev_client_id = *curarg; + RemoveArg(curarg); + } + continue; + } + } + + ++curarg; + } + + if (prev_client_id.IsEmpty()) { + prev_client_id = getenv("DESKTOP_AUTOSTART_ID"); + } + + // We don't want child processes to use the same ID + unsetenv("DESKTOP_AUTOSTART_ID"); + + char* client_id = nullptr; + if (!sm_disable) { + PRLibrary* iceLib = PR_LoadLibrary("libICE.so.6"); + if (!iceLib) { + return NS_OK; + } + + PRLibrary* smLib = PR_LoadLibrary("libSM.so.6"); + if (!smLib) { + PR_UnloadLibrary(iceLib); + return NS_OK; + } + + IceSetIOErrorHandler = (IceSetIOErrorHandlerFn)PR_FindFunctionSymbol( + iceLib, "IceSetIOErrorHandler"); + IceAddConnectionWatch = (IceAddConnectionWatchFn)PR_FindFunctionSymbol( + iceLib, "IceAddConnectionWatch"); + IceConnectionNumber = (IceConnectionNumberFn)PR_FindFunctionSymbol( + iceLib, "IceConnectionNumber"); + IceProcessMessages = (IceProcessMessagesFn)PR_FindFunctionSymbol( + iceLib, "IceProcessMessages"); + IceGetConnectionContext = (IceGetConnectionContextFn)PR_FindFunctionSymbol( + iceLib, "IceGetConnectionContext"); + if (!IceSetIOErrorHandler || !IceAddConnectionWatch || + !IceConnectionNumber || !IceProcessMessages || + !IceGetConnectionContext) { + PR_UnloadLibrary(iceLib); + PR_UnloadLibrary(smLib); + return NS_OK; + } + + SmcInteractDone = + (SmcInteractDoneFn)PR_FindFunctionSymbol(smLib, "SmcInteractDone"); + SmcSaveYourselfDone = (SmcSaveYourselfDoneFn)PR_FindFunctionSymbol( + smLib, "SmcSaveYourselfDone"); + SmcInteractRequest = (SmcInteractRequestFn)PR_FindFunctionSymbol( + smLib, "SmcInteractRequest"); + SmcCloseConnection = (SmcCloseConnectionFn)PR_FindFunctionSymbol( + smLib, "SmcCloseConnection"); + SmcOpenConnection = + (SmcOpenConnectionFn)PR_FindFunctionSymbol(smLib, "SmcOpenConnection"); + SmcSetProperties = + (SmcSetPropertiesFn)PR_FindFunctionSymbol(smLib, "SmcSetProperties"); + if (!SmcInteractDone || !SmcSaveYourselfDone || !SmcInteractRequest || + !SmcCloseConnection || !SmcOpenConnection || !SmcSetProperties) { + PR_UnloadLibrary(iceLib); + PR_UnloadLibrary(smLib); + return NS_OK; + } + + ice_init(); + + // all callbacks are mandatory in libSM 1.0, so listen even if we don't + // care. + unsigned long mask = SmcSaveYourselfProcMask | SmcDieProcMask | + SmcSaveCompleteProcMask | SmcShutdownCancelledProcMask; + + SmcCallbacks callbacks; + callbacks.save_yourself.callback = nsNativeAppSupportUnix::SaveYourselfCB; + callbacks.save_yourself.client_data = static_cast<SmPointer>(this); + + callbacks.die.callback = nsNativeAppSupportUnix::DieCB; + callbacks.die.client_data = static_cast<SmPointer>(this); + + callbacks.save_complete.callback = nsNativeAppSupportUnix::SaveCompleteCB; + callbacks.save_complete.client_data = nullptr; + + callbacks.shutdown_cancelled.callback = + nsNativeAppSupportUnix::ShutdownCancelledCB; + callbacks.shutdown_cancelled.client_data = static_cast<SmPointer>(this); + + char errbuf[256]; + mSessionConnection = SmcOpenConnection( + nullptr, this, SmProtoMajor, SmProtoMinor, mask, &callbacks, + prev_client_id.get(), &client_id, sizeof(errbuf), errbuf); + } + + if (!mSessionConnection) { + return NS_OK; + } + + LogModule::Init( + gArgc, gArgv); // need to make sure initialized before SetClientState + if (prev_client_id.IsEmpty() || + (client_id && !prev_client_id.Equals(client_id))) { + SetClientState(STATE_REGISTERING); + } else { + SetClientState(STATE_IDLE); + } + + gdk_x11_set_sm_client_id(client_id); + + // Set SM Properties + // SmCloneCommand, SmProgram, SmRestartCommand, SmUserID are required + // properties so must be set, and must have a sensible fallback value. + + // Determine executable path to use for XSMP session restore + + // Is there a request to suppress default binary launcher? + nsAutoCString path(getenv("MOZ_APP_LAUNCHER")); + + if (path.IsEmpty()) { + NS_ASSERTION(gDirServiceProvider, + "gDirServiceProvider is NULL! This shouldn't happen!"); + nsCOMPtr<nsIFile> executablePath; + nsresult rv; + + bool dummy; + rv = gDirServiceProvider->GetFile(XRE_EXECUTABLE_FILE, &dummy, + getter_AddRefs(executablePath)); + + if (NS_SUCCEEDED(rv)) { + // Strip off the -bin suffix to get the shell script we should run; this + // is what Breakpad does + nsAutoCString leafName; + rv = executablePath->GetNativeLeafName(leafName); + if (NS_SUCCEEDED(rv) && StringEndsWith(leafName, "-bin"_ns)) { + leafName.SetLength(leafName.Length() - strlen("-bin")); + executablePath->SetNativeLeafName(leafName); + } + + executablePath->GetNativePath(path); + } + } + + if (path.IsEmpty()) { + // can't determine executable path. Best fallback is name from + // application.ini but it might not resolve to the same executable at + // launch time. + path = gAppData->name; // will always be set + ToLowerCase(path); + MOZ_LOG(sMozSMLog, LogLevel::Warning, + ("Could not determine executable path. Falling back to %s.", + path.get())); + } + + SmProp propRestart, propClone, propProgram, propUser, *props[4]; + SmPropValue valsRestart[3], valsClone[1], valsProgram[1], valsUser[1]; + int n = 0; + + constexpr auto kClientIDParam = "--sm-client-id"_ns; + + SetSMValue(valsRestart[0], path); + SetSMValue(valsRestart[1], kClientIDParam); + SetSMValue(valsRestart[2], nsDependentCString(client_id)); + SetSMProperty(propRestart, SmRestartCommand, SmLISTofARRAY8, 3, valsRestart); + props[n++] = &propRestart; + + SetSMValue(valsClone[0], path); + SetSMProperty(propClone, SmCloneCommand, SmLISTofARRAY8, 1, valsClone); + props[n++] = &propClone; + + nsAutoCString appName(gAppData->name); // will always be set + ToLowerCase(appName); + + SetSMValue(valsProgram[0], appName); + SetSMProperty(propProgram, SmProgram, SmARRAY8, 1, valsProgram); + props[n++] = &propProgram; + + nsAutoCString userName; // username that started the program + struct passwd* pw = getpwuid(getuid()); + if (pw && pw->pw_name) { + userName = pw->pw_name; + } else { + userName = "nobody"_ns; + MOZ_LOG( + sMozSMLog, LogLevel::Warning, + ("Could not determine user-name. Falling back to %s.", userName.get())); + } + + SetSMValue(valsUser[0], userName); + SetSMProperty(propUser, SmUserID, SmARRAY8, 1, valsUser); + props[n++] = &propUser; + + SmcSetProperties(mSessionConnection, n, props); + + g_free(client_id); +#endif /* MOZ_X11 */ + + return NS_OK; +} + +NS_IMETHODIMP +nsNativeAppSupportUnix::Enable() { return NS_OK; } + +nsresult NS_CreateNativeAppSupport(nsINativeAppSupport** aResult) { + nsNativeAppSupportBase* native = new nsNativeAppSupportUnix(); + if (!native) return NS_ERROR_OUT_OF_MEMORY; + + *aResult = native; + NS_ADDREF(*aResult); + + return NS_OK; +} diff --git a/toolkit/xre/nsNativeAppSupportWin.cpp b/toolkit/xre/nsNativeAppSupportWin.cpp new file mode 100644 index 0000000000..8c4b43b3f6 --- /dev/null +++ b/toolkit/xre/nsNativeAppSupportWin.cpp @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsNativeAppSupportBase.h" +#include "nsNativeAppSupportWin.h" + +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/WindowsConsole.h" + +using namespace mozilla; + +/* + * This code attaches the process to the appropriate console. + */ + +class nsNativeAppSupportWin : public nsNativeAppSupportBase { + public: + // Utility function to handle a Win32-specific command line + // option: "--console", which dynamically creates a Windows + // console. + void CheckConsole(); + + private: + ~nsNativeAppSupportWin() {} +}; // nsNativeAppSupportWin + +void nsNativeAppSupportWin::CheckConsole() { + if (CheckArg(gArgc, gArgv, "attach-console") == ARG_FOUND) { + UseParentConsole(); + } + + if (CheckArg(gArgc, gArgv, "console") == ARG_FOUND) { + if (AllocConsole()) { + // Redirect the standard streams to the new console, but + // only if they haven't been redirected to a valid file. + // Visual Studio's _fileno() returns -2 for the standard + // streams if they aren't associated with an output stream. + if (_fileno(stdout) == -2) { + freopen("CONOUT$", "w", stdout); + } + // There is no CONERR$, so use CONOUT$ for stderr as well. + if (_fileno(stderr) == -2) { + freopen("CONOUT$", "w", stderr); + } + if (_fileno(stdin) == -2) { + freopen("CONIN$", "r", stdin); + } + } + } +} + +// Create and return an instance of class nsNativeAppSupportWin. +nsresult NS_CreateNativeAppSupport(nsINativeAppSupport** aResult) { + nsNativeAppSupportWin* pNative = new nsNativeAppSupportWin; + if (!pNative) return NS_ERROR_OUT_OF_MEMORY; + + // Check for dynamic console creation request. + pNative->CheckConsole(); + + *aResult = pNative; + NS_ADDREF(*aResult); + + return NS_OK; +} diff --git a/toolkit/xre/nsNativeAppSupportWin.h b/toolkit/xre/nsNativeAppSupportWin.h new file mode 100644 index 0000000000..4bf01e1bb5 --- /dev/null +++ b/toolkit/xre/nsNativeAppSupportWin.h @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* This file has *public* stuff needed for the Win32 implementation of + * the nsINativeAppSupport interface. It has to be broken out into a + * separate file in order to ensure that the generated .h file can be + * used in a Win32 .rc file. See /mozilla/xpfe/bootstrap/splash.rc. + * + * This file, and the generated .h, are only needed on Win32 platforms. + */ + +// Constants identifying Win32 "native" resources. + +#define IDI_APPICON 1 +#define IDI_DOCUMENT 2 +#define IDI_NEWWINDOW 3 +#define IDI_NEWTAB 4 +// If IDI_PBMODE's index changes, PRIVATE_BROWSING_ICON_INDEX +// in BrowserContentHandler.sys.mjs must also be updated. +#define IDI_PBMODE 5 +#define IDI_DOCUMENT_PDF 6 +#ifndef IDI_APPLICATION +# define IDI_APPLICATION 32512 +#endif + +// String that goes in the WinXP Start Menu. +#define IDS_STARTMENU_APPNAME 103 diff --git a/toolkit/xre/nsSigHandlers.cpp b/toolkit/xre/nsSigHandlers.cpp new file mode 100644 index 0000000000..f28a21fce4 --- /dev/null +++ b/toolkit/xre/nsSigHandlers.cpp @@ -0,0 +1,402 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* + * This module is supposed to abstract signal handling away from the other + * platforms that do not support it. + */ + +#include "nsSigHandlers.h" + +#ifdef XP_UNIX + +# include <signal.h> +# include <stdio.h> +# include <string.h> +# include "prthread.h" +# include "prenv.h" +# include "nsDebug.h" +# include "nsString.h" +# include "nsXULAppAPI.h" + +# if defined(LINUX) +# include <sys/time.h> +# include <sys/resource.h> +# include <unistd.h> +# include <stdlib.h> // atoi +# include <sys/prctl.h> +# ifndef ANDROID // no Android impl +# include <ucontext.h> +# endif +# endif + +# if defined(SOLARIS) +# include <sys/resource.h> +# include <ucontext.h> +# endif + +// Note: some tests manipulate this value. +unsigned int _gdb_sleep_duration = 300; + +# if defined(LINUX) && !defined(ANDROID) && defined(DEBUG) && \ + (defined(__i386) || defined(__x86_64) || defined(PPC)) +# define CRAWL_STACK_ON_SIGSEGV +# endif + +# ifndef PR_SET_PTRACER +# define PR_SET_PTRACER 0x59616d61 +# endif +# ifndef PR_SET_PTRACER_ANY +# define PR_SET_PTRACER_ANY ((unsigned long)-1) +# endif + +# if defined(CRAWL_STACK_ON_SIGSEGV) + +# include <unistd.h> +# include "nsISupportsUtils.h" +# include "mozilla/Attributes.h" +# include "mozilla/StackWalk.h" + +static const char* gProgname = "huh?"; + +// NB: keep me up to date with the same variable in +// ipc/chromium/chrome/common/ipc_channel_posix.cc +static const int kClientChannelFd = 3; + +extern "C" { + +static void PrintStackFrame(uint32_t aFrameNumber, void* aPC, void* aSP, + void* aClosure) { + char buf[1024]; + MozCodeAddressDetails details; + + MozDescribeCodeAddress(aPC, &details); + MozFormatCodeAddressDetails(buf, sizeof(buf), aFrameNumber, aPC, &details); + fprintf(stdout, "%s\n", buf); + fflush(stdout); +} +} + +void common_crap_handler(int signum, const void* aFirstFramePC) { + printf("\nProgram %s (pid = %d) received signal %d.\n", gProgname, getpid(), + signum); + + printf("Stack:\n"); + MozStackWalk(PrintStackFrame, aFirstFramePC, /* maxFrames */ 0, nullptr); + + printf("Sleeping for %d seconds.\n", _gdb_sleep_duration); + printf("Type 'gdb %s %d' to attach your debugger to this thread.\n", + gProgname, getpid()); + + // Allow us to be ptraced by gdb on Linux with Yama restrictions enabled. + prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY); + + sleep(_gdb_sleep_duration); + + printf("Done sleeping...\n"); + + _exit(signum); +} + +MOZ_NEVER_INLINE void ah_crap_handler(int signum) { + common_crap_handler(signum, CallerPC()); +} + +MOZ_NEVER_INLINE void child_ah_crap_handler(int signum) { + if (!getenv("MOZ_DONT_UNBLOCK_PARENT_ON_CHILD_CRASH")) + close(kClientChannelFd); + common_crap_handler(signum, CallerPC()); +} + +# endif // CRAWL_STACK_ON_SIGSEGV + +# ifdef MOZ_WIDGET_GTK +// Need this include for version test below. +# include <glib.h> +# endif + +# if defined(MOZ_WIDGET_GTK) && \ + (GLIB_MAJOR_VERSION > 2 || \ + (GLIB_MAJOR_VERSION == 2 && GLIB_MINOR_VERSION >= 6)) + +static GLogFunc orig_log_func = nullptr; + +extern "C" { +static void glib_log_func(const gchar* log_domain, GLogLevelFlags log_level, + const gchar* message, gpointer user_data); +} + +// GDK sometimes avoids calling exit handlers, but we still want to know when we +// crash, see https://gitlab.gnome.org/GNOME/gtk/-/issues/4514 and bug 1743144. +static bool IsCrashyGtkMessage(const nsACString& aMessage) { + if (aMessage.EqualsLiteral("Lost connection to Wayland compositor.")) { + // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gdk/wayland/gdkeventsource.c#L210 + return true; + } + if (StringBeginsWith(aMessage, "Error flushing display: "_ns)) { + // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gdk/wayland/gdkeventsource.c#L68 + return true; + } + if (StringBeginsWith(aMessage, "Error reading events from display: "_ns)) { + // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gdk/wayland/gdkeventsource.c#L97 + return true; + } + if (StringBeginsWith(aMessage, "Error "_ns) && + StringEndsWith(aMessage, " dispatching to Wayland display."_ns)) { + // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gdk/wayland/gdkeventsource.c#L205 + return true; + } + return false; +} + +/* static */ void glib_log_func(const gchar* log_domain, + GLogLevelFlags log_level, const gchar* message, + gpointer user_data) { + if (MOZ_UNLIKELY(IsCrashyGtkMessage(nsDependentCString(message)))) { + MOZ_CRASH_UNSAFE(strdup(message)); + } + + if (log_level & + (G_LOG_LEVEL_ERROR | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION)) { + NS_DebugBreak(NS_DEBUG_ASSERTION, message, "glib assertion", __FILE__, + __LINE__); + } else if (log_level & (G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING)) { + NS_DebugBreak(NS_DEBUG_WARNING, message, "glib warning", __FILE__, + __LINE__); + } + + orig_log_func(log_domain, log_level, message, nullptr); +} + +# endif + +# ifdef SA_SIGINFO +static void fpehandler(int signum, siginfo_t* si, void* context) { + /* Integer divide by zero or integer overflow. */ + /* Note: FPE_INTOVF is ignored on Intel, PowerPC and SPARC systems. */ + if (si->si_code == FPE_INTDIV || si->si_code == FPE_INTOVF) { + NS_DebugBreak(NS_DEBUG_ABORT, "Divide by zero", nullptr, __FILE__, + __LINE__); + } + +# ifdef XP_MACOSX +# if defined(__i386__) || defined(__amd64__) + ucontext_t* uc = (ucontext_t*)context; + + _STRUCT_FP_CONTROL* ctrl = &uc->uc_mcontext->__fs.__fpu_fcw; + ctrl->__invalid = ctrl->__denorm = ctrl->__zdiv = ctrl->__ovrfl = + ctrl->__undfl = ctrl->__precis = 1; + + _STRUCT_FP_STATUS* status = &uc->uc_mcontext->__fs.__fpu_fsw; + status->__invalid = status->__denorm = status->__zdiv = status->__ovrfl = + status->__undfl = status->__precis = status->__stkflt = + status->__errsumm = 0; + + uint32_t* mxcsr = &uc->uc_mcontext->__fs.__fpu_mxcsr; + *mxcsr |= SSE_EXCEPTION_MASK; /* disable all SSE exceptions */ + *mxcsr &= ~SSE_STATUS_FLAGS; /* clear all pending SSE exceptions */ +# endif +# endif +# if defined(LINUX) && !defined(ANDROID) + +# if defined(__i386__) + ucontext_t* uc = (ucontext_t*)context; + /* + * It seems that we have no access to mxcsr on Linux. libc + * seems to be translating cw/sw to mxcsr. + */ + unsigned long int* cw = &uc->uc_mcontext.fpregs->cw; + *cw |= FPU_EXCEPTION_MASK; + + unsigned long int* sw = &uc->uc_mcontext.fpregs->sw; + *sw &= ~FPU_STATUS_FLAGS; +# endif +# if defined(__amd64__) + ucontext_t* uc = (ucontext_t*)context; + + uint16_t* cw = &uc->uc_mcontext.fpregs->cwd; + *cw |= FPU_EXCEPTION_MASK; + + uint16_t* sw = &uc->uc_mcontext.fpregs->swd; + *sw &= ~FPU_STATUS_FLAGS; + + uint32_t* mxcsr = &uc->uc_mcontext.fpregs->mxcsr; + *mxcsr |= SSE_EXCEPTION_MASK; /* disable all SSE exceptions */ + *mxcsr &= ~SSE_STATUS_FLAGS; /* clear all pending SSE exceptions */ +# endif +# endif +# ifdef SOLARIS + ucontext_t* uc = (ucontext_t*)context; + +# if defined(__i386) + uint32_t* cw = &uc->uc_mcontext.fpregs.fp_reg_set.fpchip_state.state[0]; + *cw |= FPU_EXCEPTION_MASK; + + uint32_t* sw = &uc->uc_mcontext.fpregs.fp_reg_set.fpchip_state.state[1]; + *sw &= ~FPU_STATUS_FLAGS; + + /* address of the instruction that caused the exception */ + uint32_t* ip = &uc->uc_mcontext.fpregs.fp_reg_set.fpchip_state.state[3]; + uc->uc_mcontext.gregs[REG_PC] = *ip; +# endif +# if defined(__amd64__) + uint16_t* cw = &uc->uc_mcontext.fpregs.fp_reg_set.fpchip_state.cw; + *cw |= FPU_EXCEPTION_MASK; + + uint16_t* sw = &uc->uc_mcontext.fpregs.fp_reg_set.fpchip_state.sw; + *sw &= ~FPU_STATUS_FLAGS; + + uint32_t* mxcsr = &uc->uc_mcontext.fpregs.fp_reg_set.fpchip_state.mxcsr; + *mxcsr |= SSE_EXCEPTION_MASK; /* disable all SSE exceptions */ + *mxcsr &= ~SSE_STATUS_FLAGS; /* clear all pending SSE exceptions */ +# endif +# endif +} +# endif + +void InstallSignalHandlers(const char* aProgname) { +# if defined(CRAWL_STACK_ON_SIGSEGV) + if (aProgname) { + const char* tmp = strdup(aProgname); + if (tmp) { + gProgname = tmp; + } + } +# endif // CRAWL_STACK_ON_SIGSEGV + + const char* gdbSleep = PR_GetEnv("MOZ_GDB_SLEEP"); + if (gdbSleep && *gdbSleep) { + unsigned int s; + if (1 == sscanf(gdbSleep, "%u", &s)) { + _gdb_sleep_duration = s; + } + } + +# if defined(CRAWL_STACK_ON_SIGSEGV) + if (!getenv("XRE_NO_WINDOWS_CRASH_DIALOG")) { + void (*crap_handler)(int) = GeckoProcessType_Default != XRE_GetProcessType() + ? child_ah_crap_handler + : ah_crap_handler; + signal(SIGSEGV, crap_handler); + signal(SIGILL, crap_handler); + signal(SIGABRT, crap_handler); + } +# endif // CRAWL_STACK_ON_SIGSEGV + +# ifdef SA_SIGINFO + /* Install a handler for floating point exceptions and disable them if they + * occur. */ + struct sigaction sa, osa; + sa.sa_flags = SA_ONSTACK | SA_RESTART | SA_SIGINFO; + sa.sa_sigaction = fpehandler; + sigemptyset(&sa.sa_mask); + sigaction(SIGFPE, &sa, &osa); +# endif + + if (!XRE_IsParentProcess()) { + /* + * If the user is debugging a Gecko parent process in gdb and hits ^C to + * suspend, a SIGINT signal will be sent to the child. We ignore this signal + * so the child isn't killed. + */ + signal(SIGINT, SIG_IGN); + } + +# if defined(DEBUG) && defined(LINUX) + const char* memLimit = PR_GetEnv("MOZ_MEM_LIMIT"); + if (memLimit && *memLimit) { + long m = atoi(memLimit); + m *= (1024 * 1024); + struct rlimit r; + r.rlim_cur = m; + r.rlim_max = m; + setrlimit(RLIMIT_AS, &r); + } +# endif + +# if defined(MOZ_WIDGET_GTK) && \ + (GLIB_MAJOR_VERSION > 2 || \ + (GLIB_MAJOR_VERSION == 2 && GLIB_MINOR_VERSION >= 6)) + // Override the default glib logging function to intercept some crashes that + // are uninterceptable otherwise. + // Also, when XPCOM_DEBUG_BREAK is set, we can also get stacks for them. + // so we get stacks for it too. + orig_log_func = g_log_set_default_handler(glib_log_func, nullptr); +# endif +} + +#elif XP_WIN + +# include <windows.h> + +# ifdef _M_IX86 +/* + * WinNT.h prior to SDK7 does not expose the structure of the ExtendedRegisters + * for ia86. We known that MxCsr is at offset 0x18 and is a DWORD. + */ +# define MXCSR(ctx) (*(DWORD*)(((BYTE*)(ctx)->ExtendedRegisters) + 0x18)) +# endif + +# ifdef _M_X64 +# define MXCSR(ctx) (ctx)->MxCsr +# endif + +# if defined(_M_IX86) || defined(_M_X64) + +# ifdef _M_X64 +# define X87CW(ctx) (ctx)->FltSave.ControlWord +# define X87SW(ctx) (ctx)->FltSave.StatusWord +# else +# define X87CW(ctx) (ctx)->FloatSave.ControlWord +# define X87SW(ctx) (ctx)->FloatSave.StatusWord +# endif + +static LPTOP_LEVEL_EXCEPTION_FILTER gFPEPreviousFilter; + +LONG __stdcall FpeHandler(PEXCEPTION_POINTERS pe) { + PEXCEPTION_RECORD e = (PEXCEPTION_RECORD)pe->ExceptionRecord; + CONTEXT* c = (CONTEXT*)pe->ContextRecord; + + switch (e->ExceptionCode) { + case STATUS_FLOAT_DENORMAL_OPERAND: + case STATUS_FLOAT_DIVIDE_BY_ZERO: + case STATUS_FLOAT_INEXACT_RESULT: + case STATUS_FLOAT_INVALID_OPERATION: + case STATUS_FLOAT_OVERFLOW: + case STATUS_FLOAT_STACK_CHECK: + case STATUS_FLOAT_UNDERFLOW: + case STATUS_FLOAT_MULTIPLE_FAULTS: + case STATUS_FLOAT_MULTIPLE_TRAPS: + X87CW(c) |= FPU_EXCEPTION_MASK; /* disable all FPU exceptions */ + X87SW(c) &= ~FPU_STATUS_FLAGS; /* clear all pending FPU exceptions */ +# ifdef _M_IX86 + if (c->ContextFlags & CONTEXT_EXTENDED_REGISTERS) { +# endif + MXCSR(c) |= SSE_EXCEPTION_MASK; /* disable all SSE exceptions */ + MXCSR(c) &= ~SSE_STATUS_FLAGS; /* clear all pending SSE exceptions */ +# ifdef _M_IX86 + } +# endif + return EXCEPTION_CONTINUE_EXECUTION; + } + LONG action = EXCEPTION_CONTINUE_SEARCH; + if (gFPEPreviousFilter) action = gFPEPreviousFilter(pe); + + return action; +} + +void InstallSignalHandlers(const char* aProgname) { + gFPEPreviousFilter = SetUnhandledExceptionFilter(FpeHandler); +} + +# else + +void InstallSignalHandlers(const char* aProgname) {} + +# endif + +#else +# error No signal handling implementation for this platform. +#endif diff --git a/toolkit/xre/nsSigHandlers.h b/toolkit/xre/nsSigHandlers.h new file mode 100644 index 0000000000..f7424c026a --- /dev/null +++ b/toolkit/xre/nsSigHandlers.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 40; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#if defined(_M_IX86) || defined(_M_X64) || defined(__i386__) || \ + defined(__i386) || defined(__amd64__) + +/* + * x87 FPU Control Word: + * + * 0 -> IM Invalid Operation + * 1 -> DM Denormalized Operand + * 2 -> ZM Zero Divide + * 3 -> OM Overflow + * 4 -> UM Underflow + * 5 -> PM Precision + */ +# define FPU_EXCEPTION_MASK 0x3f + +/* + * x86 FPU Status Word: + * + * 0..5 -> Exception flags (see x86 FPU Control Word) + * 6 -> SF Stack Fault + * 7 -> ES Error Summary Status + */ +# define FPU_STATUS_FLAGS 0xff + +/* + * MXCSR Control and Status Register: + * + * 0..5 -> Exception flags (see x86 FPU Control Word) + * 6 -> DAZ Denormals Are Zero + * 7..12 -> Exception mask (see x86 FPU Control Word) + */ +# define SSE_STATUS_FLAGS FPU_EXCEPTION_MASK +# define SSE_EXCEPTION_MASK (FPU_EXCEPTION_MASK << 7) + +#endif diff --git a/toolkit/xre/nsUpdateDriver.cpp b/toolkit/xre/nsUpdateDriver.cpp new file mode 100644 index 0000000000..c355741c76 --- /dev/null +++ b/toolkit/xre/nsUpdateDriver.cpp @@ -0,0 +1,955 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 <stdlib.h> +#include <stdio.h> +#include "nsUpdateDriver.h" + +#include "nsDebug.h" +#include "nsXULAppAPI.h" +#include "nsAppRunner.h" +#include "nsIFile.h" +#include "nsVariant.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "prproces.h" +#include "mozilla/Logging.h" +#include "prenv.h" +#include "nsVersionComparator.h" +#include "nsDirectoryServiceDefs.h" +#include "nsThreadUtils.h" +#include "nsIXULAppInfo.h" +#include "mozilla/Preferences.h" +#include "nsPrintfCString.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/Printf.h" +#include "mozilla/UniquePtr.h" +#include "nsIObserverService.h" +#include "nsNetCID.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Services.h" +#include "mozilla/dom/Promise.h" + +#ifdef XP_MACOSX +# include "nsILocalFileMac.h" +# include "nsCommandLineServiceMac.h" +# include "MacLaunchHelper.h" +# include "updaterfileutils_osx.h" +# include "mozilla/Monitor.h" +# include "gfxPlatformMac.h" +#endif + +#if defined(XP_WIN) +# include <direct.h> +# include <process.h> +# include <windows.h> +# include <shlwapi.h> +# include <strsafe.h> +# include "commonupdatedir.h" +# include "nsWindowsHelpers.h" +# include "pathhash.h" +# include "WinUtils.h" +# define getcwd(path, size) _getcwd(path, size) +# define getpid() GetCurrentProcessId() +#elif defined(XP_UNIX) +# include <unistd.h> +# include <sys/wait.h> +#endif + +using namespace mozilla; + +static LazyLogModule sUpdateLog("updatedriver"); +// Some other file in our unified batch might have defined LOG already. +#ifdef LOG +# undef LOG +#endif +#define LOG(args) MOZ_LOG(sUpdateLog, mozilla::LogLevel::Debug, args) + +#ifdef XP_MACOSX +static void UpdateDriverSetupMacCommandLine(int& argc, char**& argv, + bool restart) { + if (NS_IsMainThread()) { + CommandLineServiceMac::SetupMacCommandLine(argc, argv, restart); + return; + } + // Bug 1335916: SetupMacCommandLine calls a CoreFoundation function that + // asserts that it was called from the main thread, so if we are not the main + // thread, we have to dispatch that call to there. But we also have to get the + // result from it, so we can't just dispatch and return, we have to wait + // until the dispatched operation actually completes. So we also set up a + // monitor to signal us when that happens, and block until then. + Monitor monitor MOZ_UNANNOTATED("nsUpdateDriver SetupMacCommandLine"); + + nsresult rv = NS_DispatchToMainThread(NS_NewRunnableFunction( + "UpdateDriverSetupMacCommandLine", + [&argc, &argv, restart, &monitor]() -> void { + CommandLineServiceMac::SetupMacCommandLine(argc, argv, restart); + MonitorAutoLock(monitor).Notify(); + })); + + if (NS_FAILED(rv)) { + LOG( + ("Update driver error dispatching SetupMacCommandLine to main thread: " + "%d\n", + rv)); + return; + } + + // The length of this wait is arbitrary, but should be long enough that having + // it expire means something is seriously wrong. + CVStatus status = + MonitorAutoLock(monitor).Wait(TimeDuration::FromSeconds(60)); + if (status == CVStatus::Timeout) { + LOG(("Update driver timed out waiting for SetupMacCommandLine\n")); + } +} +#endif + +static nsresult GetCurrentWorkingDir(nsACString& aOutPath) { + // Cannot use NS_GetSpecialDirectory because XPCOM is not yet initialized. + // This code is duplicated from xpcom/io/SpecialSystemDirectory.cpp: + + aOutPath.Truncate(); + +#if defined(XP_WIN) + wchar_t wpath[MAX_PATH]; + if (!_wgetcwd(wpath, ArrayLength(wpath))) { + return NS_ERROR_FAILURE; + } + CopyUTF16toUTF8(nsDependentString(wpath), aOutPath); +#else + char path[MAXPATHLEN]; + if (!getcwd(path, ArrayLength(path))) { + return NS_ERROR_FAILURE; + } + aOutPath = path; +#endif + + return NS_OK; +} + +/** + * Get the path to the installation directory. For Mac OS X this will be the + * bundle directory. + * + * @param appDir the application directory file object + * @param installDirPath the path to the installation directory + */ +static nsresult GetInstallDirPath(nsIFile* appDir, nsACString& installDirPath) { + nsresult rv; +#ifdef XP_MACOSX + nsCOMPtr<nsIFile> parentDir1, parentDir2; + rv = appDir->GetParent(getter_AddRefs(parentDir1)); + NS_ENSURE_SUCCESS(rv, rv); + rv = parentDir1->GetParent(getter_AddRefs(parentDir2)); + NS_ENSURE_SUCCESS(rv, rv); + rv = parentDir2->GetNativePath(installDirPath); + NS_ENSURE_SUCCESS(rv, rv); +#elif XP_WIN + nsAutoString installDirPathW; + rv = appDir->GetPath(installDirPathW); + NS_ENSURE_SUCCESS(rv, rv); + CopyUTF16toUTF8(installDirPathW, installDirPath); +#else + rv = appDir->GetNativePath(installDirPath); + NS_ENSURE_SUCCESS(rv, rv); +#endif + return NS_OK; +} + +static bool GetFile(nsIFile* dir, const nsACString& name, + nsCOMPtr<nsIFile>& result) { + nsresult rv; + + nsCOMPtr<nsIFile> file; + rv = dir->Clone(getter_AddRefs(file)); + if (NS_FAILED(rv)) { + return false; + } + + rv = file->AppendNative(name); + if (NS_FAILED(rv)) { + return false; + } + + result = file; + return true; +} + +static bool GetStatusFile(nsIFile* dir, nsCOMPtr<nsIFile>& result) { + return GetFile(dir, "update.status"_ns, result); +} + +/** + * Get the contents of the update.status file when the update.status file can + * be opened with read and write access. The reason it is opened for both read + * and write is to prevent trying to update when the user doesn't have write + * access to the update directory. + * + * @param statusFile the status file object. + * @param buf the buffer holding the file contents + * + * @return true if successful, false otherwise. + */ +template <size_t Size> +static bool GetStatusFileContents(nsIFile* statusFile, char (&buf)[Size]) { + static_assert( + Size > 16, + "Buffer needs to be large enough to hold the known status codes"); + + PRFileDesc* fd = nullptr; + nsresult rv = statusFile->OpenNSPRFileDesc(PR_RDWR, 0660, &fd); + if (NS_FAILED(rv)) { + return false; + } + + const int32_t n = PR_Read(fd, buf, Size); + PR_Close(fd); + + return (n >= 0); +} + +enum UpdateStatus { + eNoUpdateAction, + ePendingUpdate, + ePendingService, + ePendingElevate, + eAppliedUpdate, + eAppliedService, +}; + +/** + * Returns a value indicating what needs to be done in order to handle an + * update. + * + * @param dir the directory in which we should look for an update.status file. + * @param statusFile the update.status file found in the directory. + * + * @return the update action to be performed. + */ +static UpdateStatus GetUpdateStatus(nsIFile* dir, + nsCOMPtr<nsIFile>& statusFile) { + if (GetStatusFile(dir, statusFile)) { + char buf[32]; + if (GetStatusFileContents(statusFile, buf)) { + const char kPending[] = "pending"; + const char kPendingService[] = "pending-service"; + const char kPendingElevate[] = "pending-elevate"; + const char kApplied[] = "applied"; + const char kAppliedService[] = "applied-service"; + if (!strncmp(buf, kPendingElevate, sizeof(kPendingElevate) - 1)) { + return ePendingElevate; + } + if (!strncmp(buf, kPendingService, sizeof(kPendingService) - 1)) { + return ePendingService; + } + if (!strncmp(buf, kPending, sizeof(kPending) - 1)) { + return ePendingUpdate; + } + if (!strncmp(buf, kAppliedService, sizeof(kAppliedService) - 1)) { + return eAppliedService; + } + if (!strncmp(buf, kApplied, sizeof(kApplied) - 1)) { + return eAppliedUpdate; + } + } + } + return eNoUpdateAction; +} + +static bool GetVersionFile(nsIFile* dir, nsCOMPtr<nsIFile>& result) { + return GetFile(dir, "update.version"_ns, result); +} + +// Compares the current application version with the update's application +// version. +static bool IsOlderVersion(nsIFile* versionFile, const char* appVersion) { + PRFileDesc* fd = nullptr; + nsresult rv = versionFile->OpenNSPRFileDesc(PR_RDONLY, 0660, &fd); + if (NS_FAILED(rv)) { + return true; + } + + char buf[32]; + const int32_t n = PR_Read(fd, buf, sizeof(buf)); + PR_Close(fd); + + if (n < 0) { + return false; + } + + // Trim off the trailing newline + if (buf[n - 1] == '\n') { + buf[n - 1] = '\0'; + } + + // If the update xml doesn't provide the application version the file will + // contain the string "null" and it is assumed that the update is not older. + const char kNull[] = "null"; + if (strncmp(buf, kNull, sizeof(kNull) - 1) == 0) { + return false; + } + + return mozilla::Version(appVersion) > buf; +} + +/** + * Applies, switches, or stages an update. + * + * @param greDir the GRE directory + * @param updateDir the update root directory + * @param appDir the application directory + * @param appArgc the number of args passed to the application + * @param appArgv the args passed to the application + * (used for restarting the application when necessary) + * @param restart true when a restart is necessary. + * @param isStaged true when the update has already been staged + * @param outpid (out) parameter holding the handle to the updater application + * when staging updates + */ +static void ApplyUpdate(nsIFile* greDir, nsIFile* updateDir, nsIFile* appDir, + int appArgc, char** appArgv, bool restart, + bool isStaged, ProcessType* outpid) { + // The following determines the update operation to perform. + // 1. When restart is false the update will be staged. + // 2. When restart is true and isStaged is false the update will apply the mar + // file to the installation directory. + // 3. When restart is true and isStaged is true the update will switch the + // staged update with the installation directory. + + nsresult rv; + + nsCOMPtr<nsIFile> updater; + nsAutoCString updaterPath; + nsAutoCString updateDirPath; +#if defined(XP_WIN) + // Get an nsIFile reference for the updater in the installation dir. + if (!GetFile(greDir, nsLiteralCString(UPDATER_BIN), updater)) { + return; + } + + // Get the path to the updater. + nsAutoString updaterPathW; + rv = updater->GetPath(updaterPathW); + if (NS_FAILED(rv)) { + return; + } + CopyUTF16toUTF8(updaterPathW, updaterPath); + + // Get the path to the update dir. + nsAutoString updateDirPathW; + rv = updateDir->GetPath(updateDirPathW); + if (NS_FAILED(rv)) { + return; + } + CopyUTF16toUTF8(updateDirPathW, updateDirPath); +#elif defined(XP_MACOSX) + // Get an nsIFile reference for the updater in the installation dir. + if (!GetFile(appDir, nsLiteralCString(UPDATER_APP), updater)) { + return; + } + rv = updater->AppendNative("Contents"_ns); + if (NS_FAILED(rv)) { + return; + } + rv = updater->AppendNative("MacOS"_ns); + if (NS_FAILED(rv)) { + return; + } + rv = updater->AppendNative(nsLiteralCString(UPDATER_BIN)); + if (NS_FAILED(rv)) { + return; + } + + // Get the path to the updater. + rv = updater->GetNativePath(updaterPath); + if (NS_FAILED(rv)) { + return; + } + + // Get the path to the update dir. + rv = updateDir->GetNativePath(updateDirPath); + if (NS_FAILED(rv)) { + return; + } +#else + // Get an nsIFile reference for the updater in the installation dir. + if (!GetFile(greDir, nsLiteralCString(UPDATER_BIN), updater)) { + return; + } + + // Get the path to the updater. + rv = updater->GetNativePath(updaterPath); + if (NS_FAILED(rv)) { + return; + } + + // Get the path to the update dir. + rv = updateDir->GetNativePath(updateDirPath); + if (NS_FAILED(rv)) { + return; + } +#endif + + // appFilePath and workingDirPath are only used when the application will be + // restarted. + nsAutoCString appFilePath; + nsAutoCString workingDirPath; + if (restart) { + // Get the path to the current working directory. + rv = GetCurrentWorkingDir(workingDirPath); + if (NS_FAILED(rv)) { + return; + } + + // Get the application file path used by the updater to restart the + // application after the update has finished. + nsCOMPtr<nsIFile> appFile; + XRE_GetBinaryPath(getter_AddRefs(appFile)); + if (!appFile) { + return; + } + +#if defined(XP_WIN) + nsAutoString appFilePathW; + rv = appFile->GetPath(appFilePathW); + if (NS_FAILED(rv)) { + return; + } + CopyUTF16toUTF8(appFilePathW, appFilePath); +#else + rv = appFile->GetNativePath(appFilePath); + if (NS_FAILED(rv)) { + return; + } +#endif + } + + // Get the installation directory path. + nsAutoCString installDirPath; + rv = GetInstallDirPath(appDir, installDirPath); + if (NS_FAILED(rv)) { + return; + } + +#if defined(XP_MACOSX) + // If we're going to do a restart, we need to make sure the font registration + // thread has finished before this process exits (bug 1777332). + if (restart) { + gfxPlatformMac::WaitForFontRegistration(); + } + + // We need to detect whether elevation is required for this update. This can + // occur when an admin user installs the application, but another admin + // user attempts to update (see bug 394984). + // We only check if we need elevation if we are restarting. We don't attempt + // to stage if elevation is required. Staging happens without the user knowing + // about it, and we don't want to ask for elevation for seemingly no reason. + bool needElevation = false; + if (restart) { + needElevation = !IsRecursivelyWritable(installDirPath.get()); + if (needElevation) { + // Normally we would check this via nsIAppStartup::wasSilentlyStarted, + // but nsIAppStartup isn't available yet. + char* mozAppSilentStart = PR_GetEnv("MOZ_APP_SILENT_START"); + bool wasSilentlyStarted = + mozAppSilentStart && (strcmp(mozAppSilentStart, "") != 0); + if (wasSilentlyStarted) { + // Elevation always requires prompting for credentials on macOS. If we + // are trying to restart silently, we must not display UI such as this + // prompt. + // We make this check here rather than in the updater, because it is + // actually Firefox that shows the elevation prompt (via + // InstallPrivilegedHelper), not the updater. + return; + } + } + } +#endif + + nsAutoCString applyToDirPath; + nsCOMPtr<nsIFile> updatedDir; + if (restart && !isStaged) { + // The install directory is the same as the apply to directory. + applyToDirPath.Assign(installDirPath); + } else { + // Get the directory where the update is staged or will be staged. This is + // `updateDir` for macOS and `appDir` for all other platforms. macOS cannot + // stage updates inside the .app bundle (`appDir`) without breaking the code + // signature on the bundle, so we use `updateDir` instead. +#if defined(XP_MACOSX) + if (!GetFile(updateDir, "Updated.app"_ns, updatedDir)) { +#else + if (!GetFile(appDir, "updated"_ns, updatedDir)) { +#endif + return; + } +#if defined(XP_WIN) + nsAutoString applyToDirPathW; + rv = updatedDir->GetPath(applyToDirPathW); + if (NS_FAILED(rv)) { + return; + } + CopyUTF16toUTF8(applyToDirPathW, applyToDirPath); +#else + rv = updatedDir->GetNativePath(applyToDirPath); +#endif + } + if (NS_FAILED(rv)) { + return; + } + + if (restart && isStaged) { + // When the update should already be staged make sure that the updated + // directory exists. + bool updatedDirExists = false; + if (NS_FAILED(updatedDir->Exists(&updatedDirExists)) || !updatedDirExists) { + return; + } + } + + // On platforms where we are not calling execv, we may need to make the + // updater executable wait for the calling process to exit. Otherwise, the + // updater may have trouble modifying our executable image (because it might + // still be in use). This is accomplished by passing our PID to the updater + // so that it can wait for us to exit. This is not perfect as there is a race + // condition that could bite us. It's possible that the calling process could + // exit before the updater waits on the specified PID, and in the meantime a + // new process with the same PID could be created. This situation is + // unlikely, however, given the way most operating systems recycle PIDs. We'll + // take our chances ;-) Construct the PID argument for this process to pass to + // the updater. + nsAutoCString pid; + if (restart) { +#if defined(XP_UNIX) & !defined(XP_MACOSX) + // When execv is used for an update that requires a restart 0 is passed + // which is ignored by the updater. + pid.AssignLiteral("0"); +#else + pid.AppendInt((int32_t)getpid()); +#endif + if (isStaged) { + // Append a special token to the PID in order to inform the updater that + // it should replace install with the updated directory. + pid.AppendLiteral("/replace"); + } + } else { + // Signal the updater application that it should stage the update. + pid.AssignLiteral("-1"); + } + + int argc = 5; + if (restart) { + argc = appArgc + 6; + if (gRestartedByOS) { + argc += 1; + } + } + char** argv = static_cast<char**>(malloc((argc + 1) * sizeof(char*))); + if (!argv) { + return; + } + argv[0] = (char*)updaterPath.get(); + argv[1] = (char*)updateDirPath.get(); + argv[2] = (char*)installDirPath.get(); + argv[3] = (char*)applyToDirPath.get(); + argv[4] = (char*)pid.get(); + if (restart && appArgc) { + argv[5] = (char*)workingDirPath.get(); + argv[6] = (char*)appFilePath.get(); + for (int i = 1; i < appArgc; ++i) { + argv[6 + i] = appArgv[i]; + } + if (gRestartedByOS) { + // We haven't truly started up, restore this argument so that we will have + // it upon restart. + argv[6 + appArgc] = const_cast<char*>("-os-restarted"); + } + } + argv[argc] = nullptr; + + if (restart && gSafeMode) { + PR_SetEnv("MOZ_SAFE_MODE_RESTART=1"); + } + + LOG(("spawning updater process [%s]\n", updaterPath.get())); + +#if defined(XP_UNIX) && !defined(XP_MACOSX) + // We use execv to spawn the updater process on all UNIX systems except Mac + // OSX since it is known to cause problems on the Mac. Windows has execv, but + // it is a faked implementation that doesn't really replace the current + // process. Instead it spawns a new process, so we gain nothing from using + // execv on Windows. + if (restart) { + int execResult = execv(updaterPath.get(), argv); + free(argv); + exit(execResult); + } + *outpid = fork(); + if (*outpid == -1) { + free(argv); + return; + } + if (*outpid == 0) { + int execResult = execv(updaterPath.get(), argv); + free(argv); + exit(execResult); + } +#elif defined(XP_WIN) + if (isStaged) { + // Launch the updater to replace the installation with the staged updated. + if (!WinLaunchChild(updaterPathW.get(), argc, argv)) { + free(argv); + return; + } + } else { + // Launch the updater to either stage or apply an update. + if (!WinLaunchChild(updaterPathW.get(), argc, argv, nullptr, outpid)) { + free(argv); + return; + } + } +#elif defined(XP_MACOSX) +UpdateDriverSetupMacCommandLine(argc, argv, restart); +if (restart && needElevation) { + bool hasLaunched = LaunchElevatedUpdate(argc, argv, outpid); + free(argv); + if (!hasLaunched) { + LOG(("Failed to launch elevated update!")); + exit(1); + } + exit(0); +} + +if (isStaged) { + // Launch the updater to replace the installation with the staged updated. + LaunchChildMac(argc, argv); +} else { + // Launch the updater to either stage or apply an update. + LaunchChildMac(argc, argv, outpid); +} +#else +if (isStaged) { + // Launch the updater to replace the installation with the staged updated. + PR_CreateProcessDetached(updaterPath.get(), argv, nullptr, nullptr); +} else { + // Launch the updater to either stage or apply an update. + *outpid = PR_CreateProcess(updaterPath.get(), argv, nullptr, nullptr); +} +#endif + free(argv); + if (restart) { + exit(0); + } +} + +#if !defined(XP_WIN) +/** + * Wait briefly to see if a process terminates, then return true if it has. + * + * (Not implemented on Windows, where HandleWatcher is used instead.) + */ +static bool ProcessHasTerminated(ProcessType pt) { +# if defined(XP_MACOSX) + // We're waiting for the process to terminate in LaunchChildMac. + return true; +# elif defined(XP_UNIX) + int exitStatus; + pid_t exited = waitpid(pt, &exitStatus, WNOHANG); + if (exited == 0) { + // Process is still running. + sleep(1); + return false; + } + if (exited == -1) { + LOG(("Error while checking if the updater process is finished")); + // This shouldn't happen, but if it does, the updater process is lost to us, + // so the best we can do is pretend that it's exited. + return true; + } + // If we get here, the process has exited; make sure it exited normally. + if (WIFEXITED(exitStatus) && (WEXITSTATUS(exitStatus) != 0)) { + LOG(("Error while running the updater process, check update.log")); + } + return true; +# else + // No way to have a non-blocking implementation on these platforms, + // because we're using NSPR and it only provides a blocking wait. + int32_t exitCode; + PR_WaitProcess(pt, &exitCode); + if (exitCode != 0) { + LOG(("Error while running the updater process, check update.log")); + } + return true; +# endif +} +#endif + +nsresult ProcessUpdates(nsIFile* greDir, nsIFile* appDir, nsIFile* updRootDir, + int argc, char** argv, const char* appVersion, + bool restart, ProcessType* pid) { + nsresult rv; + +#ifdef XP_WIN + // If we're in a package, we know any updates that we find are not for us. + if (mozilla::widget::WinUtils::HasPackageIdentity()) { + return NS_OK; + } +#endif + + nsCOMPtr<nsIFile> updatesDir; + rv = updRootDir->Clone(getter_AddRefs(updatesDir)); + NS_ENSURE_SUCCESS(rv, rv); + rv = updatesDir->AppendNative("updates"_ns); + NS_ENSURE_SUCCESS(rv, rv); + rv = updatesDir->AppendNative("0"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + // Return early since there isn't a valid update when the update application + // version file doesn't exist or if the update's application version is less + // than the current application version. The cleanup of the update will happen + // during post update processing in nsUpdateService.js. + nsCOMPtr<nsIFile> versionFile; + if (!GetVersionFile(updatesDir, versionFile) || + IsOlderVersion(versionFile, appVersion)) { + return NS_OK; + } + + nsCOMPtr<nsIFile> statusFile; + UpdateStatus status = GetUpdateStatus(updatesDir, statusFile); + switch (status) { + case ePendingUpdate: + case ePendingService: { + ApplyUpdate(greDir, updatesDir, appDir, argc, argv, restart, false, pid); + break; + } + case eAppliedUpdate: + case eAppliedService: + // An update was staged and needs to be switched so the updated + // application is used. + ApplyUpdate(greDir, updatesDir, appDir, argc, argv, restart, true, pid); + break; + case ePendingElevate: + // No action should be performed since the user hasn't opted into + // elevating for the update so continue application startup. + case eNoUpdateAction: + // We don't need to do any special processing here, we'll just continue to + // startup the application. + break; + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsUpdateProcessor, nsIUpdateProcessor) + +nsUpdateProcessor::nsUpdateProcessor() : mUpdaterPID(0) {} + +#ifdef XP_WIN +nsUpdateProcessor::~nsUpdateProcessor() { mProcessWatcher.Stop(); } +#else +nsUpdateProcessor::~nsUpdateProcessor() = default; +#endif + +NS_IMETHODIMP +nsUpdateProcessor::ProcessUpdate() { + nsresult rv; + + nsCOMPtr<nsIProperties> ds = + do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> exeFile; + rv = ds->Get(XRE_EXECUTABLE_FILE, NS_GET_IID(nsIFile), + getter_AddRefs(exeFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> appDir; + rv = exeFile->GetParent(getter_AddRefs(appDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> greDir; + rv = ds->Get(NS_GRE_DIR, NS_GET_IID(nsIFile), getter_AddRefs(greDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> updRoot; + rv = ds->Get(XRE_UPDATE_ROOT_DIR, NS_GET_IID(nsIFile), + getter_AddRefs(updRoot)); + NS_ASSERTION(NS_SUCCEEDED(rv), "Can't get the UpdRootD dir"); + + // XRE_UPDATE_ROOT_DIR should not fail but if it does fallback to the + // application directory just to be safe. + if (NS_FAILED(rv)) { + rv = appDir->Clone(getter_AddRefs(updRoot)); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIXULAppInfo> appInfo = + do_GetService("@mozilla.org/xre/app-info;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString appVersion; + rv = appInfo->GetVersion(appVersion); + NS_ENSURE_SUCCESS(rv, rv); + + // Copy the parameters to the StagedUpdateInfo structure shared with the + // worker thread. + mInfo.mGREDir = greDir; + mInfo.mAppDir = appDir; + mInfo.mUpdateRoot = updRoot; + mInfo.mArgc = 0; + mInfo.mArgv = nullptr; + mInfo.mAppVersion = appVersion; + + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + nsCOMPtr<nsIRunnable> r = + NewRunnableMethod("nsUpdateProcessor::StartStagedUpdate", this, + &nsUpdateProcessor::StartStagedUpdate); + return NS_NewNamedThread("UpdateProcessor", getter_AddRefs(mWorkerThread), r); +} + +void nsUpdateProcessor::StartStagedUpdate() { + MOZ_ASSERT(!NS_IsMainThread(), "main thread"); + + // If we fail to launch the updater process or its monitor for some reason, we + // need to shut down the worker thread, as there isn't anything more for us to + // do. + auto onExitStopThread = mozilla::MakeScopeExit([&] { + nsresult rv = NS_DispatchToMainThread( + NewRunnableMethod("nsUpdateProcessor::ShutdownWorkerThread", this, + &nsUpdateProcessor::ShutdownWorkerThread)); + NS_ENSURE_SUCCESS_VOID(rv); + }); + + // Launch updater. (We do this on a worker thread to avoid blocking the main + // thread with file I/O.) + nsresult rv = ProcessUpdates(mInfo.mGREDir, mInfo.mAppDir, mInfo.mUpdateRoot, + mInfo.mArgc, mInfo.mArgv, + mInfo.mAppVersion.get(), false, &mUpdaterPID); + if (NS_FAILED(rv)) { + MOZ_LOG(sUpdateLog, mozilla::LogLevel::Error, + ("could not start updater process: %s", GetStaticErrorName(rv))); + return; + } + + if (!mUpdaterPID) { + // not an error + MOZ_LOG(sUpdateLog, mozilla::LogLevel::Verbose, + ("ProcessUpdates() indicated nothing to do")); + return; + } + +#ifdef WIN32 + // Set up a HandleWatcher to report to the main thread when we're done. + RefPtr<nsIThread> mainThread; + NS_GetMainThread(getter_AddRefs(mainThread)); + mProcessWatcher.Watch(mUpdaterPID, mainThread, + NewRunnableMethod("nsUpdateProcessor::UpdateDone", this, + &nsUpdateProcessor::UpdateDone)); + +// On Windows, that's all we need the worker thread for. Let +// `onExitStopThread` shut us down. +#else + // Monitor the state of the updater process while it is staging an update. + rv = NS_DispatchToCurrentThread( + NewRunnableMethod("nsUpdateProcessor::WaitForProcess", this, + &nsUpdateProcessor::WaitForProcess)); + if (NS_FAILED(rv)) { + MOZ_LOG(sUpdateLog, mozilla::LogLevel::Error, + ("could not start updater process poll: error %s", + GetStaticErrorName(rv))); + return; + } + + // Leave the worker thread alive to run WaitForProcess. Either it or its + // successors will be responsible for shutting down the worker thread. + onExitStopThread.release(); +#endif +} + +void nsUpdateProcessor::ShutdownWorkerThread() { + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + mWorkerThread->Shutdown(); + mWorkerThread = nullptr; +} + +#ifndef WIN32 +void nsUpdateProcessor::WaitForProcess() { + MOZ_ASSERT(!NS_IsMainThread(), "main thread"); + if (ProcessHasTerminated(mUpdaterPID)) { + NS_DispatchToMainThread(NewRunnableMethod( + "nsUpdateProcessor::UpdateDone", this, &nsUpdateProcessor::UpdateDone)); + } else { + NS_DispatchToCurrentThread( + NewRunnableMethod("nsUpdateProcessor::WaitForProcess", this, + &nsUpdateProcessor::WaitForProcess)); + } +} +#endif + +void nsUpdateProcessor::UpdateDone() { + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + nsCOMPtr<nsIUpdateManager> um = + do_GetService("@mozilla.org/updates/update-manager;1"); + if (um) { + // This completes asynchronously, but nothing else that we are doing in this + // function requires waiting for this to complete. + RefPtr<mozilla::dom::Promise> outPromise; + um->RefreshUpdateStatus(getter_AddRefs(outPromise)); + } + +// On Windows, shutting down the worker thread is taken care of by another task. +// (Which may not have run yet, so we can't assert.) +#ifndef XP_WIN + ShutdownWorkerThread(); +#endif +} + +NS_IMETHODIMP +nsUpdateProcessor::GetServiceRegKeyExists(bool* aResult) { +#ifndef XP_WIN + return NS_ERROR_NOT_IMPLEMENTED; +#else // #ifdef XP_WIN + nsCOMPtr<nsIProperties> dirSvc( + do_GetService("@mozilla.org/file/directory_service;1")); + NS_ENSURE_TRUE(dirSvc, NS_ERROR_SERVICE_NOT_AVAILABLE); + + nsCOMPtr<nsIFile> installBin; + nsresult rv = dirSvc->Get(XRE_EXECUTABLE_FILE, NS_GET_IID(nsIFile), + getter_AddRefs(installBin)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> installDir; + rv = installBin->GetParent(getter_AddRefs(installDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString installPath; + rv = installDir->GetPath(installPath); + NS_ENSURE_SUCCESS(rv, rv); + + wchar_t maintenanceServiceKey[MAX_PATH + 1]; + BOOL success = CalculateRegistryPathFromFilePath( + PromiseFlatString(installPath).get(), maintenanceServiceKey); + NS_ENSURE_TRUE(success, NS_ERROR_FAILURE); + + HKEY regHandle; + LSTATUS ls = RegOpenKeyExW(HKEY_LOCAL_MACHINE, maintenanceServiceKey, 0, + KEY_QUERY_VALUE | KEY_WOW64_64KEY, ®Handle); + if (ls == ERROR_SUCCESS) { + RegCloseKey(regHandle); + *aResult = true; + return NS_OK; + } + if (ls == ERROR_FILE_NOT_FOUND) { + *aResult = false; + return NS_OK; + } + // We got an error we weren't expecting reading the registry. + return NS_ERROR_NOT_AVAILABLE; +#endif // #ifdef XP_WIN +} diff --git a/toolkit/xre/nsUpdateDriver.h b/toolkit/xre/nsUpdateDriver.h new file mode 100644 index 0000000000..24a20f3f76 --- /dev/null +++ b/toolkit/xre/nsUpdateDriver.h @@ -0,0 +1,112 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#ifndef nsUpdateDriver_h__ +#define nsUpdateDriver_h__ + +#include "nscore.h" +#include "nsIUpdateService.h" +#include "nsIThread.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "mozilla/Attributes.h" + +class nsIFile; + +#if defined(XP_WIN) +# include <windows.h> +# include "mozilla/WinHandleWatcher.h" +typedef HANDLE ProcessType; +#elif defined(XP_UNIX) +typedef pid_t ProcessType; +#else +# include "prproces.h" +typedef PRProcess* ProcessType; +#endif + +#ifdef XP_WIN +# define UPDATER_BIN "updater.exe" +# define MAINTENANCE_SVC_NAME L"MozillaMaintenance" +#elif XP_MACOSX +# define UPDATER_APP "updater.app" +# define UPDATER_BIN "org.mozilla.updater" +#else +# define UPDATER_BIN "updater" +#endif + +/** + * This function processes any available updates. As part of that process, it + * may exit the current process and relaunch it at a later time. + * + * Two directories are passed to this function: greDir (where the actual + * binary resides) and appDir (which contains application.ini for XULRunner + * apps). If this is not a XULRunner app then appDir is identical to greDir. + * + * The argc and argv passed to this function should be what is needed to + * relaunch the current process. + * + * The appVersion param passed to this function is the current application's + * version and is used to determine if an update's version is older than the + * current application version. + * + * If you want the update to be processed without restarting, set the restart + * parameter to false. + * + * This function does not modify appDir. + */ +nsresult ProcessUpdates(nsIFile* greDir, nsIFile* appDir, nsIFile* updRootDir, + int argc, char** argv, const char* appVersion, + bool restart = true, ProcessType* pid = nullptr); + +// The implementation of the update processor handles the task of loading the +// updater application for staging an update. +// XXX ehsan this is living in this file in order to make use of the existing +// stuff here, we might want to move it elsewhere in the future. +class nsUpdateProcessor final : public nsIUpdateProcessor { + public: + nsUpdateProcessor(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIUPDATEPROCESSOR + + private: + ~nsUpdateProcessor(); + + struct StagedUpdateInfo { + StagedUpdateInfo() : mArgc(0), mArgv(nullptr) {} + ~StagedUpdateInfo() { + for (int i = 0; i < mArgc; ++i) { + delete[] mArgv[i]; + } + delete[] mArgv; + } + + nsCOMPtr<nsIFile> mGREDir; + nsCOMPtr<nsIFile> mAppDir; + nsCOMPtr<nsIFile> mUpdateRoot; + int mArgc; + char** mArgv; + nsCString mAppVersion; + }; + + private: + void StartStagedUpdate(); + void UpdateDone(); + void ShutdownWorkerThread(); + +#ifndef XP_WIN + void WaitForProcess(); +#endif + + private: + ProcessType mUpdaterPID; + nsCOMPtr<nsIThread> mWorkerThread; +#ifdef XP_WIN + mozilla::HandleWatcher mProcessWatcher; +#endif + StagedUpdateInfo mInfo; +}; +#endif // nsUpdateDriver_h__ diff --git a/toolkit/xre/nsUpdateSyncManager.cpp b/toolkit/xre/nsUpdateSyncManager.cpp new file mode 100644 index 0000000000..b2f6d4247b --- /dev/null +++ b/toolkit/xre/nsUpdateSyncManager.cpp @@ -0,0 +1,129 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "nsUpdateSyncManager.h" + +#include "mozilla/Unused.h" +#include "mozilla/Services.h" +#include "nsComponentManagerUtils.h" +#include "nsCRT.h" +#include "nsIFile.h" +#include "nsIObserverService.h" +#include "nsIProperties.h" +#include "nsString.h" +#include "nsXULAppAPI.h" + +// The lock code generates a path that already includes the vendor name, +// so this only needs to name the specific lock. +#define UPDATE_LOCK_NAME_TOKEN "UpdateLock" + +nsUpdateSyncManager* gUpdateSyncManager = nullptr; + +NS_IMPL_ISUPPORTS(nsUpdateSyncManager, nsIUpdateSyncManager, nsIObserver) + +nsUpdateSyncManager::nsUpdateSyncManager(nsIFile* anAppFile /* = nullptr */) { + gUpdateSyncManager = this; + OpenLock(anAppFile); +} + +nsUpdateSyncManager::~nsUpdateSyncManager() { + ReleaseLock(); + gUpdateSyncManager = nullptr; +} + +already_AddRefed<nsUpdateSyncManager> nsUpdateSyncManager::GetSingleton() { + if (!gUpdateSyncManager) { + new nsUpdateSyncManager(); // This sets gUpdateSyncManager. + } + return do_AddRef(gUpdateSyncManager); +} + +NS_IMETHODIMP nsUpdateSyncManager::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + mozilla::Unused << aSubject; + mozilla::Unused << aData; + + // We want to hold the lock for as much of the lifetime of the app as we can, + // so we observe xpcom-startup so we get constructed as early as possible, + // which triggers constructing the singleton. + if (!nsCRT::strcmp(aTopic, NS_XPCOM_STARTUP_OBSERVER_ID)) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + return observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, + false); + } + return NS_ERROR_SERVICE_NOT_AVAILABLE; + } + if (!nsCRT::strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + ReleaseLock(); + } + + return NS_OK; +} + +nsresult nsUpdateSyncManager::OpenLock(nsIFile* anAppFile) { + nsresult rv; + if (mLock != MULTI_INSTANCE_LOCK_HANDLE_ERROR) { + // Lock is already open. + return NS_OK; + } + + // Our component registration should already have made sure of this. + if (NS_WARN_IF(XRE_GetProcessType() != GeckoProcessType_Default)) { + return NS_OK; + } + + nsCOMPtr<nsIFile> appFile = mozilla::GetNormalizedAppFile(anAppFile); + if (!appFile) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr<nsIFile> appDirFile; + rv = appFile->GetParent(getter_AddRefs(appDirFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString appDirPath; + rv = appDirFile->GetPath(appDirPath); + NS_ENSURE_SUCCESS(rv, rv); + + mLock = + mozilla::OpenMultiInstanceLock(UPDATE_LOCK_NAME_TOKEN, appDirPath.get()); + NS_ENSURE_TRUE(mLock, NS_ERROR_FAILURE); + + return NS_OK; +} + +void nsUpdateSyncManager::ReleaseLock() { + if (mLock == MULTI_INSTANCE_LOCK_HANDLE_ERROR) { + // Lock is already released. + return; + } + + mozilla::ReleaseMultiInstanceLock(mLock); + mLock = MULTI_INSTANCE_LOCK_HANDLE_ERROR; +} + +NS_IMETHODIMP nsUpdateSyncManager::IsOtherInstanceRunning(bool* aResult) { + if (NS_WARN_IF(XRE_GetProcessType() != GeckoProcessType_Default)) { + return NS_ERROR_SERVICE_NOT_AVAILABLE; + } + + if (mLock == MULTI_INSTANCE_LOCK_HANDLE_ERROR) { + return NS_ERROR_NOT_INITIALIZED; + } + + bool rv = mozilla::IsOtherInstanceRunning(mLock, aResult); + NS_ENSURE_TRUE(rv, NS_ERROR_FAILURE); + + return NS_OK; +} + +NS_IMETHODIMP nsUpdateSyncManager::ResetLock(nsIFile* anAppFile = nullptr) { + ReleaseLock(); + return OpenLock(anAppFile); +} diff --git a/toolkit/xre/nsUpdateSyncManager.h b/toolkit/xre/nsUpdateSyncManager.h new file mode 100644 index 0000000000..4c24cde8d4 --- /dev/null +++ b/toolkit/xre/nsUpdateSyncManager.h @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#ifndef nsUpdateSyncManager_h__ +#define nsUpdateSyncManager_h__ + +#include "mozilla/AlreadyAddRefed.h" +#include "nsIObserver.h" +#include "nsIUpdateService.h" +#include "MultiInstanceLock.h" + +// The update sync manager is responsible for making sure that only one +// instance of the application is running at the time we want to start updating +// it. It does this by taking a multi-instance lock very early during the +// application's startup process. Then, when app update tasks are ready to run, +// the update service asks us whether anything else has also taken that lock, +// which, if true, would mean another instance of the application is currently +// running and performing update tasks should be avoided (the update service +// also runs a timeout and eventually goes ahead with the update in order to +// prevent an external program from effectively disabling updates). +class nsUpdateSyncManager final : public nsIUpdateSyncManager, + public nsIObserver { + public: + explicit nsUpdateSyncManager(nsIFile* anAppFile = nullptr); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIUPDATESYNCMANAGER + NS_DECL_NSIOBSERVER + + static already_AddRefed<nsUpdateSyncManager> GetSingleton(); + + private: + ~nsUpdateSyncManager(); + + nsUpdateSyncManager(nsUpdateSyncManager&) = delete; + nsUpdateSyncManager(nsUpdateSyncManager&&) = delete; + nsUpdateSyncManager& operator=(nsUpdateSyncManager&) = delete; + nsUpdateSyncManager& operator=(nsUpdateSyncManager&&) = delete; + + nsresult OpenLock(nsIFile* anAppFile = nullptr); + void ReleaseLock(); + + mozilla::MultiInstLockHandle mLock = MULTI_INSTANCE_LOCK_HANDLE_ERROR; +}; + +#endif // nsUpdateSyncManager_h__ diff --git a/toolkit/xre/nsWindowsRestart.cpp b/toolkit/xre/nsWindowsRestart.cpp new file mode 100644 index 0000000000..869ca7c92c --- /dev/null +++ b/toolkit/xre/nsWindowsRestart.cpp @@ -0,0 +1,197 @@ +/* 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/. */ + +// This file is not build directly. Instead, it is included in multiple +// shared objects. + +#ifdef nsWindowsRestart_cpp +# error \ + "nsWindowsRestart.cpp is not a header file, and must only be included once." +#else +# define nsWindowsRestart_cpp +#endif + +#include "mozilla/CmdLineAndEnvUtils.h" +#include "nsUTF8Utils.h" + +#include <shellapi.h> + +// Needed for CreateEnvironmentBlock +#include <userenv.h> +#ifndef __MINGW32__ +# pragma comment(lib, "userenv.lib") +#endif + +/** + * Convert UTF8 to UTF16 without using the normal XPCOM goop, which we + * can't link to updater.exe. + */ +static char16_t* AllocConvertUTF8toUTF16(const char* arg) { + // UTF16 can't be longer in units than UTF8 + size_t len = strlen(arg); + char16_t* s = new char16_t[(len + 1) * sizeof(char16_t)]; + if (!s) return nullptr; + + size_t dstLen = ::MultiByteToWideChar(CP_UTF8, 0, arg, len, + reinterpret_cast<wchar_t*>(s), len); + s[dstLen] = 0; + + return s; +} + +static void FreeAllocStrings(int argc, wchar_t** argv) { + while (argc) { + --argc; + delete[] argv[argc]; + } + + delete[] argv; +} + +static wchar_t** AllocConvertUTF8toUTF16Strings(int argc, char** argv) { + wchar_t** argvConverted = new wchar_t*[argc]; + if (!argvConverted) return nullptr; + + for (int i = 0; i < argc; ++i) { + argvConverted[i] = + reinterpret_cast<wchar_t*>(AllocConvertUTF8toUTF16(argv[i])); + if (!argvConverted[i]) { + FreeAllocStrings(i, argvConverted); + return nullptr; + } + } + return argvConverted; +} + +/** + * Return true if we are in a job with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE and + * we can break away from it. + * CreateProcess fails if we try to break away from a job but it's not allowed. + * So if we cannot determine the result due to a failure, we assume we don't + * need to break away and this returns false. + */ +static bool NeedToBreakAwayFromJob() { + // If we can't determine if we are in a job, we assume we're not in a job. + BOOL inJob = FALSE; + if (!::IsProcessInJob(::GetCurrentProcess(), nullptr, &inJob)) { + return false; + } + + // If there is no job, there is nothing to worry about. + if (!inJob) { + return false; + } + + // If we can't get the job object flags, we assume no need to break away from + // it. + JOBOBJECT_EXTENDED_LIMIT_INFORMATION job_info = {}; + if (!::QueryInformationJobObject(nullptr, JobObjectExtendedLimitInformation, + &job_info, sizeof(job_info), nullptr)) { + return false; + } + + // If JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE is not set, no need to worry about + // the job. + if (!(job_info.BasicLimitInformation.LimitFlags & + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)) { + return false; + } + + // If we can't break away from the current job, there is nothing we can do. + if (!(job_info.BasicLimitInformation.LimitFlags & + JOB_OBJECT_LIMIT_BREAKAWAY_OK)) { + return false; + } + + // We can and need to break away from the job. + return true; +} + +/** + * Launch a child process with the specified arguments. + * @note argv[0] is ignored + * @note The form of this function that takes char **argv expects UTF-8 + */ + +BOOL WinLaunchChild(const wchar_t* exePath, int argc, wchar_t** argv, + HANDLE userToken = nullptr, HANDLE* hProcess = nullptr); + +BOOL WinLaunchChild(const wchar_t* exePath, int argc, char** argv, + HANDLE userToken, HANDLE* hProcess) { + wchar_t** argvConverted = AllocConvertUTF8toUTF16Strings(argc, argv); + if (!argvConverted) return FALSE; + + BOOL ok = WinLaunchChild(exePath, argc, argvConverted, userToken, hProcess); + FreeAllocStrings(argc, argvConverted); + return ok; +} + +BOOL WinLaunchChild(const wchar_t* exePath, int argc, wchar_t** argv, + HANDLE userToken, HANDLE* hProcess) { + BOOL ok; + + mozilla::UniquePtr<wchar_t[]> cl(mozilla::MakeCommandLine(argc, argv)); + if (!cl) { + return FALSE; + } + + DWORD creationFlags = + NeedToBreakAwayFromJob() ? CREATE_BREAKAWAY_FROM_JOB : 0; + + STARTUPINFOW si = {0}; + si.cb = sizeof(STARTUPINFOW); + si.lpDesktop = const_cast<LPWSTR>(L"winsta0\\Default"); + PROCESS_INFORMATION pi = {0}; + + if (userToken == nullptr) { + ok = CreateProcessW(exePath, cl.get(), + nullptr, // no special security attributes + nullptr, // no special thread attributes + FALSE, // don't inherit filehandles + creationFlags, + nullptr, // inherit my environment + nullptr, // use my current directory + &si, &pi); + } else { + // Create an environment block for the process we're about to start using + // the user's token. + LPVOID environmentBlock = nullptr; + if (!CreateEnvironmentBlock(&environmentBlock, userToken, TRUE)) { + environmentBlock = nullptr; + } + + ok = CreateProcessAsUserW(userToken, exePath, cl.get(), + nullptr, // no special security attributes + nullptr, // no special thread attributes + FALSE, // don't inherit filehandles + creationFlags, environmentBlock, + nullptr, // use my current directory + &si, &pi); + + if (environmentBlock) { + DestroyEnvironmentBlock(environmentBlock); + } + } + + if (ok) { + if (hProcess) { + *hProcess = pi.hProcess; // the caller now owns the HANDLE + } else { + CloseHandle(pi.hProcess); + } + CloseHandle(pi.hThread); + } else { + LPVOID lpMsgBuf = nullptr; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, GetLastError(), + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpMsgBuf, + 0, nullptr); + wprintf(L"Error restarting: %s\n", + lpMsgBuf ? static_cast<const wchar_t*>(lpMsgBuf) : L"(null)"); + if (lpMsgBuf) LocalFree(lpMsgBuf); + } + + return ok; +} diff --git a/toolkit/xre/nsWindowsWMain.cpp b/toolkit/xre/nsWindowsWMain.cpp new file mode 100644 index 0000000000..ea14a59b80 --- /dev/null +++ b/toolkit/xre/nsWindowsWMain.cpp @@ -0,0 +1,176 @@ +/* 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/. */ + +#ifndef nsWindowsWMain_cpp +#define nsWindowsWMain_cpp + +// This file is a .cpp file meant to be included in nsBrowserApp.cpp and other +// similar bootstrap code. It converts wide-character windows wmain into UTF-8 +// narrow-character strings. + +#ifndef XP_WIN +# error This file only makes sense on Windows. +#endif + +#include "mozilla/Char16.h" +#include "nsUTF8Utils.h" +#include "nsWindowsHelpers.h" + +#include <windows.h> +#include <versionhelpers.h> + +#ifdef __MINGW32__ + +/* MingW currently does not implement a wide version of the + startup routines. Workaround is to implement something like + it ourselves. See bug 411826 */ + +# include <shellapi.h> + +int wmain(int argc, WCHAR** argv); + +int main(int argc, char** argv) { + LPWSTR commandLine = GetCommandLineW(); + int argcw = 0; + LPWSTR* argvw = CommandLineToArgvW(commandLine, &argcw); + if (!argvw) return 127; + + int result = wmain(argcw, argvw); + LocalFree(argvw); + return result; +} +#endif /* __MINGW32__ */ + +#define main NS_internal_main + +#ifndef XRE_WANT_ENVIRON +int main(int argc, char** argv); +#else +int main(int argc, char** argv, char** envp); +#endif + +// SanitizeEnvironmentVariables() +// +// Mitigate CVE-2007-6753 (binary planting via unexpanded environment variable +// references) by forcibly expanding all environment variable references in +// %PATH%. +// +// CVE-2007-6753 is documented to have affected all active mainline Windows +// versions at the time of its announcement (i.e., up to Windows 7). Microsoft +// has never formally stated that later versions of Windows are free of this +// issue; out of an abundance of caution, we continue to mitigate it on Windows +// 8 and beyond. +static void SanitizeEnvironmentVariables() { + DWORD bufferSize = GetEnvironmentVariableW(L"PATH", nullptr, 0); + if (bufferSize) { + wchar_t* originalPath = new wchar_t[bufferSize]; + if (bufferSize - 1 == + GetEnvironmentVariableW(L"PATH", originalPath, bufferSize)) { + bufferSize = ExpandEnvironmentStringsW(originalPath, nullptr, 0); + // The maximum length of a Windows environment variable and the maximum + // length of a string returned by ExpandEnvironmentStringsW are both + // documented to be 32,767 characters. Under some versions of Windows, + // however, both may be longer, with delayed but deleterious consequences. + // + // We therefore cap the size manually, in case the user has a sufficiently + // problematic %PATH%. (See bug 1753910.) This is unlikely to occur unless + // there is some form of recursive referencing involved, in which case + // expansion is no mitigation anyway. + if (bufferSize && bufferSize < 32768) { + wchar_t* newPath = new wchar_t[bufferSize]; + if (ExpandEnvironmentStringsW(originalPath, newPath, bufferSize)) { + SetEnvironmentVariableW(L"PATH", newPath); + } + delete[] newPath; + } + } + delete[] originalPath; + } +} + +static char* AllocConvertUTF16toUTF8(char16ptr_t arg) { + // be generous... UTF16 units can expand up to 3 UTF8 units + size_t len = wcslen(arg); + // ConvertUTF16toUTF8 requires +1. Let's do that here, too, lacking + // knowledge of Windows internals. + size_t dstLen = len * 3 + 1; + char* s = new char[dstLen + 1]; // Another +1 for zero terminator + if (!s) return nullptr; + + int written = + ::WideCharToMultiByte(CP_UTF8, 0, arg, len, s, dstLen, nullptr, nullptr); + s[written] = 0; + return s; +} + +static void FreeAllocStrings(int argc, char** argv) { + while (argc) { + --argc; + delete[] argv[argc]; + } + + delete[] argv; +} + +int wmain(int argc, WCHAR** argv) { + // In Windows 7 32-bit, user32.dll must be mapped to the same virtual + // address in all processes. Otherwise, win32k's user-mode callback causes + // crash. Since we delayload user32.dll, if our code or injected code + // reserves the user32 address before user32.dll is loaded, it is loaded + // to a new address and we crash. To mitigate this problem, we explicitly + // load user32.dll as early as possible. See bug 1730033 for details. + if (!IsWindows8OrGreater()) { + SYSTEM_INFO sysInfo; + ::GetNativeSystemInfo(&sysInfo); + if (sysInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_INTEL) { + LoadLibrarySystem32(L"user32.dll"); + } + } + + SanitizeEnvironmentVariables(); + SetDllDirectoryW(L""); + + // Only run this code if LauncherProcessWin.h was included beforehand, thus + // signalling that the hosting process should support launcher mode. +#if defined(mozilla_LauncherProcessWin_h) + mozilla::Maybe<int> launcherResult = + mozilla::LauncherMain(argc, argv, sAppData); + if (launcherResult) { + return launcherResult.value(); + } +#endif // defined(mozilla_LauncherProcessWin_h) + + char** argvConverted = new char*[argc + 1]; + if (!argvConverted) return 127; + + for (int i = 0; i < argc; ++i) { + argvConverted[i] = AllocConvertUTF16toUTF8(argv[i]); + if (!argvConverted[i]) { + return 127; + } + } + argvConverted[argc] = nullptr; + + // need to save argvConverted copy for later deletion. + char** deleteUs = new char*[argc + 1]; + if (!deleteUs) { + FreeAllocStrings(argc, argvConverted); + return 127; + } + for (int i = 0; i < argc; i++) deleteUs[i] = argvConverted[i]; +#ifndef XRE_WANT_ENVIRON + int result = main(argc, argvConverted); +#else + // Force creation of the multibyte _environ variable. + getenv("PATH"); + int result = main(argc, argvConverted, _environ); +#endif + + delete[] argvConverted; + FreeAllocStrings(argc, deleteUs); + + return result; +} + +#endif // nsWindowsWMain_cpp diff --git a/toolkit/xre/nsX11ErrorHandler.cpp b/toolkit/xre/nsX11ErrorHandler.cpp new file mode 100644 index 0000000000..77f71db8f9 --- /dev/null +++ b/toolkit/xre/nsX11ErrorHandler.cpp @@ -0,0 +1,156 @@ +/* -*- Mode: C++; tab-width: 40; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsX11ErrorHandler.h" + +#include "prenv.h" +#include "nsXULAppAPI.h" +#include "nsExceptionHandler.h" +#include "nsDebug.h" +#include "nsString.h" +#include "nsTArray.h" + +#include "mozilla/X11Util.h" +#include <X11/Xlib.h> + +#define BUFSIZE 2048 // What Xlib uses with XGetErrorDatabaseText + +struct XExtension { + nsCString name; + int major_opcode; + + XExtension(const char* aName, int aCode) : name(aName), major_opcode(aCode) {} +}; + +static nsTArray<XExtension> sXExtensions; + +// man XSetErrorHandler says "the error handler should not call any +// functions (directly or indirectly) on the display that will generate +// protocol requests or that will look for input events" so we query the +// extension list early to avoid problems. +static void QueryXExtensions(Display* aDisplay) { + if (!sXExtensions.IsEmpty() || !aDisplay) { + return; + } + int nExts = 0; + char** extNames = XListExtensions(aDisplay, &nExts); + if (!extNames) { + return; + } + for (int i = 0; i < nExts; ++i) { + int major_opcode, first_event, first_error; + if (XQueryExtension(aDisplay, extNames[i], &major_opcode, &first_event, + &first_error)) { + sXExtensions.EmplaceBack(extNames[i], major_opcode); + } + } + XFreeExtensionList(extNames); +} + +extern "C" { +int X11Error(Display* display, XErrorEvent* event) { +#ifdef DEBUG + // Get an indication of how long ago the request that caused the error was + // made. + unsigned long age = NextRequest(display) - event->serial; +#endif + + // Get a string to represent the request that caused the error. + nsAutoCString message; + if (event->request_code < 128) { + // Core protocol request + message.AppendInt(event->request_code); + } else { + // Extension request + for (XExtension& ext : sXExtensions) { + if (ext.major_opcode == event->request_code) { + message.Append(ext.name); + message.Append('.'); + message.AppendInt(event->minor_code); + break; + } + } + } + + char buffer[BUFSIZE]; + if (message.IsEmpty()) { + buffer[0] = '\0'; + } else { + XGetErrorDatabaseText(display, "XRequest", message.get(), "", buffer, + sizeof(buffer)); + } + + nsAutoCString notes; + if (buffer[0]) { + notes.Append(buffer); + } else { + notes.AppendLiteral("Request "); + notes.AppendInt(event->request_code); + notes.Append('.'); + notes.AppendInt(event->minor_code); + } + + notes.AppendLiteral(": "); + + // Get a string to describe the error. + XGetErrorText(display, event->error_code, buffer, sizeof(buffer)); + notes.Append(buffer); + +#ifdef DEBUG + // For requests where Xlib gets the reply synchronously, |age| will be 1 + // and the stack will include the function making the request. For + // asynchronous requests, the current stack will often be unrelated to the + // point of making the request, even if |age| is 1, but sometimes this may + // help us count back to the point of the request. With XSynchronize on, + // the stack will include the function making the request, even though + // |age| will be 2 for asynchronous requests because XSynchronize is + // implemented by an empty request from an XSync, which has not yet been + // processed. + if (age > 1) { + // XSynchronize returns the previous "after function". If a second + // XSynchronize call returns the same function after an enable call then + // synchronization must have already been enabled. + if (XSynchronize(display, X11True) == XSynchronize(display, X11False)) { + notes.AppendLiteral("; sync"); + } else { + notes.AppendLiteral("; "); + notes.AppendInt(uint32_t(age)); + notes.AppendLiteral(" requests ago"); + } + } + + // The resource id is unlikely to be useful in a crash report without + // context of other ids, but add it to the debug console output. + notes.AppendLiteral("; id=0x"); + notes.AppendInt(uint32_t(event->resourceid), 16); +# ifdef MOZ_X11 + // Actually, for requests where Xlib gets the reply synchronously, + // MOZ_X_SYNC=1 will not be necessary, but we don't have a table to tell us + // which requests get a synchronous reply. + if (!PR_GetEnv("MOZ_X_SYNC")) { + notes.AppendLiteral( + "\nRe-running with MOZ_X_SYNC=1 in the environment may give a more " + "helpful backtrace."); + } +# endif +#endif + + NS_WARNING(notes.get()); + return 0; +} +} + +void InstallX11ErrorHandler() { + XSetErrorHandler(X11Error); + + if (Display* display = mozilla::DefaultXDisplay()) { + QueryXExtensions(display); + if (PR_GetEnv("MOZ_X_SYNC")) { + XSynchronize(display, X11True); + } + } +} + +void CleanupX11ErrorHandler() { sXExtensions.Clear(); } diff --git a/toolkit/xre/nsX11ErrorHandler.h b/toolkit/xre/nsX11ErrorHandler.h new file mode 100644 index 0000000000..bd7ac59154 --- /dev/null +++ b/toolkit/xre/nsX11ErrorHandler.h @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 40; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifdef MOZ_X11 +# include <X11/Xlib.h> +# include "X11UndefineNone.h" // Unset some macros defined by X.h included by Xlib.h + +/** + * InstallX11ErrorHandler is not suitable for processes running with GTK3 as + * GDK3 will replace the handler. This is still used for the plugin process, + * which runs with GTK2. + **/ +void InstallX11ErrorHandler(); + +void CleanupX11ErrorHandler(); + +extern "C" int X11Error(Display* display, XErrorEvent* event); +#endif diff --git a/toolkit/xre/nsXREDirProvider.cpp b/toolkit/xre/nsXREDirProvider.cpp new file mode 100644 index 0000000000..41d2353fe8 --- /dev/null +++ b/toolkit/xre/nsXREDirProvider.cpp @@ -0,0 +1,1512 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsAppRunner.h" +#include "nsXREDirProvider.h" +#ifndef ANDROID +# include "commonupdatedir.h" +#endif + +#include "jsapi.h" +#include "xpcpublic.h" +#include "prprf.h" + +#include "nsIAppStartup.h" +#include "nsIFile.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsISimpleEnumerator.h" +#include "nsIToolkitProfileService.h" +#include "nsIXULRuntime.h" +#include "commonupdatedir.h" + +#include "nsAppDirectoryServiceDefs.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsXULAppAPI.h" +#include "nsCategoryManagerUtils.h" + +#include "nsDependentString.h" +#include "nsCOMArray.h" +#include "nsArrayEnumerator.h" +#include "nsEnumeratorUtils.h" +#include "nsReadableUtils.h" + +#include "SpecialSystemDirectory.h" + +#include "mozilla/dom/ScriptSettings.h" + +#include "mozilla/AppShutdown.h" +#include "mozilla/AutoRestore.h" +#ifdef MOZ_BACKGROUNDTASKS +# include "mozilla/BackgroundTasks.h" +#endif +#include "mozilla/Components.h" +#include "mozilla/Services.h" +#include "mozilla/Omnijar.h" +#include "mozilla/Preferences.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/Telemetry.h" +#include "mozilla/XREAppData.h" +#include "nsPrintfCString.h" + +#ifdef MOZ_THUNDERBIRD +# include "nsIPK11TokenDB.h" +# include "nsIPK11Token.h" +#endif + +#include <stdlib.h> + +#ifdef XP_WIN +# include <windows.h> +# include <shlobj.h> +# include "WinUtils.h" +#endif +#ifdef XP_MACOSX +# include "nsILocalFileMac.h" +// for chflags() +# include <sys/stat.h> +# include <unistd.h> +#endif +#ifdef XP_UNIX +# include <ctype.h> +#endif +#ifdef XP_IOS +# include "UIKitDirProvider.h" +#endif + +#if defined(MOZ_CONTENT_TEMP_DIR) +# include "mozilla/SandboxSettings.h" +# include "nsID.h" +# include "mozilla/Unused.h" +#endif + +#if defined(XP_MACOSX) +# define APP_REGISTRY_NAME "Application Registry" +#elif defined(XP_WIN) +# define APP_REGISTRY_NAME "registry.dat" +#else +# define APP_REGISTRY_NAME "appreg" +#endif + +#define PREF_OVERRIDE_DIRNAME "preferences" + +#if defined(MOZ_CONTENT_TEMP_DIR) +static already_AddRefed<nsIFile> GetProcessSandboxTempDir( + GeckoProcessType type); +static nsresult DeleteDirIfExists(nsIFile* dir); +static bool IsContentSandboxDisabled(); +static const char* GetProcessTempBaseDirKey(); +static already_AddRefed<nsIFile> CreateProcessSandboxTempDir( + GeckoProcessType procType); +#endif + +nsXREDirProvider* gDirServiceProvider = nullptr; +nsIFile* gDataDirHomeLocal = nullptr; +nsIFile* gDataDirHome = nullptr; +nsCOMPtr<nsIFile> gDataDirProfileLocal = nullptr; +nsCOMPtr<nsIFile> gDataDirProfile = nullptr; + +// These are required to allow nsXREDirProvider to be usable in xpcshell tests. +// where gAppData is null. +#if defined(XP_MACOSX) || defined(XP_UNIX) +static const char* GetAppName() { + if (gAppData) { + return gAppData->name; + } + return nullptr; +} +#endif + +#ifdef XP_MACOSX +static const char* GetAppVendor() { + if (gAppData) { + return gAppData->vendor; + } + return nullptr; +} +#endif + +nsXREDirProvider::nsXREDirProvider() { gDirServiceProvider = this; } + +nsXREDirProvider::~nsXREDirProvider() { + gDirServiceProvider = nullptr; + gDataDirHomeLocal = nullptr; + gDataDirHome = nullptr; +} + +already_AddRefed<nsXREDirProvider> nsXREDirProvider::GetSingleton() { + if (!gDirServiceProvider) { + new nsXREDirProvider(); // This sets gDirServiceProvider + } + return do_AddRef(gDirServiceProvider); +} + +nsresult nsXREDirProvider::Initialize(nsIFile* aXULAppDir, nsIFile* aGREDir) { + NS_ENSURE_ARG(aXULAppDir); + NS_ENSURE_ARG(aGREDir); + + mXULAppDir = aXULAppDir; + mGREDir = aGREDir; + nsCOMPtr<nsIFile> binaryPath; + nsresult rv = XRE_GetBinaryPath(getter_AddRefs(binaryPath)); + NS_ENSURE_SUCCESS(rv, rv); + return binaryPath->GetParent(getter_AddRefs(mGREBinDir)); +} + +nsresult nsXREDirProvider::SetProfile(nsIFile* aDir, nsIFile* aLocalDir) { + MOZ_ASSERT(aDir && aLocalDir, "We don't support no-profile apps!"); + MOZ_ASSERT(!mProfileDir && !mProfileLocalDir, + "You may only set the profile directories once"); + + nsresult rv = EnsureDirectoryExists(aDir); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnsureDirectoryExists(aLocalDir); + NS_ENSURE_SUCCESS(rv, rv); + +#ifndef XP_WIN + nsAutoCString profilePath; + rv = aDir->GetNativePath(profilePath); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString localProfilePath; + rv = aLocalDir->GetNativePath(localProfilePath); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mozilla::IsUtf8(profilePath) || !mozilla::IsUtf8(localProfilePath)) { + PR_fprintf( + PR_STDERR, + "Error: The profile path is not valid UTF-8. Unable to continue.\n"); + return NS_ERROR_FAILURE; + } +#endif + +#ifdef XP_MACOSX + bool same; + if (NS_SUCCEEDED(aDir->Equals(aLocalDir, &same)) && !same) { + // Ensure that the cache directory is not indexed by Spotlight + // (bug 718910). At least on OS X, the cache directory (under + // ~/Library/Caches/) is always the "local" user profile + // directory. This is confusing, since *both* user profile + // directories are "local" (they both exist under the user's + // home directory). But this usage dates back at least as far + // as the patch for bug 291033, where "local" seems to mean + // "suitable for temporary storage". Don't hide the cache + // directory if by some chance it and the "non-local" profile + // directory are the same -- there are bad side effects from + // hiding a profile directory under /Library/Application Support/ + // (see bug 801883). + nsAutoCString cacheDir; + if (NS_SUCCEEDED(aLocalDir->GetNativePath(cacheDir))) { + if (chflags(cacheDir.get(), UF_HIDDEN)) { + NS_WARNING("Failed to set Cache directory to HIDDEN."); + } + } + } +#endif + + mProfileDir = aDir; + mProfileLocalDir = aLocalDir; + return NS_OK; +} + +NS_IMPL_QUERY_INTERFACE(nsXREDirProvider, nsIDirectoryServiceProvider, + nsIDirectoryServiceProvider2, nsIXREDirProvider, + nsIProfileStartup) + +NS_IMETHODIMP_(MozExternalRefCountType) +nsXREDirProvider::AddRef() { return 1; } + +NS_IMETHODIMP_(MozExternalRefCountType) +nsXREDirProvider::Release() { return 0; } + +nsresult nsXREDirProvider::GetUserProfilesRootDir(nsIFile** aResult) { + nsCOMPtr<nsIFile> file; + nsresult rv = GetUserDataDirectory(getter_AddRefs(file), false); + + if (NS_SUCCEEDED(rv)) { +#if !defined(XP_UNIX) || defined(XP_MACOSX) + rv = file->AppendNative("Profiles"_ns); +#endif + // We must create the profile directory here if it does not exist. + nsresult tmp = EnsureDirectoryExists(file); + if (NS_FAILED(tmp)) { + rv = tmp; + } + } + file.swap(*aResult); + return rv; +} + +nsresult nsXREDirProvider::GetUserProfilesLocalDir(nsIFile** aResult) { + nsCOMPtr<nsIFile> file; + nsresult rv = GetUserDataDirectory(getter_AddRefs(file), true); + + if (NS_SUCCEEDED(rv)) { +#if !defined(XP_UNIX) || defined(XP_MACOSX) + rv = file->AppendNative("Profiles"_ns); +#endif + // We must create the profile directory here if it does not exist. + nsresult tmp = EnsureDirectoryExists(file); + if (NS_FAILED(tmp)) { + rv = tmp; + } + } + file.swap(*aResult); + return NS_OK; +} + +#ifdef MOZ_BACKGROUNDTASKS +nsresult nsXREDirProvider::GetBackgroundTasksProfilesRootDir( + nsIFile** aResult) { + nsCOMPtr<nsIFile> file; + nsresult rv = GetUserDataDirectory(getter_AddRefs(file), false); + + if (NS_SUCCEEDED(rv)) { +# if !defined(XP_UNIX) || defined(XP_MACOSX) + // Sibling to regular user "Profiles" directory. + rv = file->AppendNative("Background Tasks Profiles"_ns); +# endif + // We must create the directory here if it does not exist. + nsresult tmp = EnsureDirectoryExists(file); + if (NS_FAILED(tmp)) { + rv = tmp; + } + } + file.swap(*aResult); + return rv; +} +#endif + +#if defined(XP_UNIX) || defined(XP_MACOSX) +/** + * Get the directory that is the parent of the system-wide directories + * for extensions and native manifests. + * + * On OSX this is /Library/Application Support/Mozilla + * On Linux this is /usr/{lib,lib64}/mozilla + * (for 32- and 64-bit systems respsectively) + */ +static nsresult GetSystemParentDirectory(nsIFile** aFile) { + nsresult rv; + nsCOMPtr<nsIFile> localDir; +# if defined(XP_MACOSX) + rv = GetOSXFolderType(kOnSystemDisk, kApplicationSupportFolderType, + getter_AddRefs(localDir)); + if (NS_SUCCEEDED(rv)) { + rv = localDir->AppendNative("Mozilla"_ns); + } +# else + constexpr auto dirname = +# ifdef HAVE_USR_LIB64_DIR + "/usr/lib64/mozilla"_ns +# elif defined(__OpenBSD__) || defined(__FreeBSD__) + "/usr/local/lib/mozilla"_ns +# else + "/usr/lib/mozilla"_ns +# endif + ; + rv = NS_NewNativeLocalFile(dirname, false, getter_AddRefs(localDir)); +# endif + + if (NS_SUCCEEDED(rv)) { + localDir.forget(aFile); + } + return rv; +} +#endif + +NS_IMETHODIMP +nsXREDirProvider::GetFile(const char* aProperty, bool* aPersistent, + nsIFile** aFile) { + *aPersistent = true; + nsresult rv = NS_ERROR_FAILURE; + + nsCOMPtr<nsIFile> file; + + if (!strcmp(aProperty, NS_APP_USER_PROFILE_LOCAL_50_DIR) || + !strcmp(aProperty, NS_APP_PROFILE_LOCAL_DIR_STARTUP)) { + if (mProfileLocalDir) { + rv = mProfileLocalDir->Clone(getter_AddRefs(file)); + } else { + // Profile directories are only set up in the parent process. + // We don't expect every caller to check if they are in the right process, + // so fail immediately to avoid warning spam. + NS_WARNING_ASSERTION(!XRE_IsParentProcess(), + "tried to get profile in parent too early"); + return NS_ERROR_FAILURE; + } + } else if (!strcmp(aProperty, NS_APP_USER_PROFILE_50_DIR) || + !strcmp(aProperty, NS_APP_PROFILE_DIR_STARTUP)) { + if (mProfileDir) { + rv = mProfileDir->Clone(getter_AddRefs(file)); + } else { + // Profile directories are only set up in the parent process. + // We don't expect every caller to check if they are in the right process, + // so fail immediately to avoid warning spam. + NS_WARNING_ASSERTION(!XRE_IsParentProcess(), + "tried to get profile in parent too early"); + return NS_ERROR_FAILURE; + } + } else if (!strcmp(aProperty, NS_GRE_DIR)) { + // On Android, internal files are inside the APK, a zip file, so this + // folder doesn't really make sense. +#if !defined(MOZ_WIDGET_ANDROID) + rv = mGREDir->Clone(getter_AddRefs(file)); +#endif // !defined(MOZ_WIDGET_ANDROID) + } else if (!strcmp(aProperty, NS_GRE_BIN_DIR)) { + rv = mGREBinDir->Clone(getter_AddRefs(file)); + } else if (!strcmp(aProperty, NS_OS_CURRENT_PROCESS_DIR) || + !strcmp(aProperty, NS_APP_INSTALL_CLEANUP_DIR)) { + rv = GetAppDir()->Clone(getter_AddRefs(file)); + } else if (!strcmp(aProperty, NS_APP_PREF_DEFAULTS_50_DIR)) { + // Same as NS_GRE_DIR +#if !defined(MOZ_WIDGET_ANDROID) + // return the GRE default prefs directory here, and the app default prefs + // directory (if applicable) in NS_APP_PREFS_DEFAULTS_DIR_LIST. + rv = mGREDir->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + rv = file->AppendNative("defaults"_ns); + NS_ENSURE_SUCCESS(rv, rv); + rv = file->AppendNative("pref"_ns); +#endif // !defined(MOZ_WIDGET_ANDROID) + } else if (!strcmp(aProperty, NS_APP_APPLICATION_REGISTRY_DIR) || + !strcmp(aProperty, XRE_USER_APP_DATA_DIR)) { + rv = GetUserAppDataDirectory(getter_AddRefs(file)); + } +#if defined(XP_UNIX) || defined(XP_MACOSX) + else if (!strcmp(aProperty, XRE_SYS_NATIVE_MANIFESTS)) { + rv = ::GetSystemParentDirectory(getter_AddRefs(file)); + } else if (!strcmp(aProperty, XRE_USER_NATIVE_MANIFESTS)) { + rv = GetUserDataDirectoryHome(getter_AddRefs(file), false); + NS_ENSURE_SUCCESS(rv, rv); +# if defined(XP_MACOSX) + rv = file->AppendNative("Mozilla"_ns); +# else // defined(XP_MACOSX) + rv = file->AppendNative(".mozilla"_ns); +# endif // defined(XP_MACOSX) + } +#endif // defined(XP_UNIX) || defined(XP_MACOSX) + else if (!strcmp(aProperty, XRE_UPDATE_ROOT_DIR)) { + rv = GetUpdateRootDir(getter_AddRefs(file)); + } else if (!strcmp(aProperty, XRE_OLD_UPDATE_ROOT_DIR)) { + rv = GetUpdateRootDir(getter_AddRefs(file), true); + } else if (!strcmp(aProperty, NS_APP_APPLICATION_REGISTRY_FILE)) { + rv = GetUserAppDataDirectory(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + rv = file->AppendNative(nsLiteralCString(APP_REGISTRY_NAME)); + } else if (!strcmp(aProperty, NS_APP_USER_PROFILES_ROOT_DIR)) { + rv = GetUserProfilesRootDir(getter_AddRefs(file)); + } else if (!strcmp(aProperty, NS_APP_USER_PROFILES_LOCAL_ROOT_DIR)) { + rv = GetUserProfilesLocalDir(getter_AddRefs(file)); + } else if (!strcmp(aProperty, XRE_EXECUTABLE_FILE)) { + rv = XRE_GetBinaryPath(getter_AddRefs(file)); + } +#if defined(XP_UNIX) || defined(XP_MACOSX) + else if (!strcmp(aProperty, XRE_SYS_LOCAL_EXTENSION_PARENT_DIR)) { +# ifdef ENABLE_SYSTEM_EXTENSION_DIRS + rv = GetSystemExtensionsDirectory(getter_AddRefs(file)); +# endif + } +#endif // defined(XP_UNIX) || defined(XP_MACOSX) +#if defined(XP_UNIX) && !defined(XP_MACOSX) + else if (!strcmp(aProperty, XRE_SYS_SHARE_EXTENSION_PARENT_DIR)) { +# ifdef ENABLE_SYSTEM_EXTENSION_DIRS +# if defined(__OpenBSD__) || defined(__FreeBSD__) + static const char* const sysLExtDir = "/usr/local/share/mozilla/extensions"; +# else + static const char* const sysLExtDir = "/usr/share/mozilla/extensions"; +# endif + rv = NS_NewNativeLocalFile(nsDependentCString(sysLExtDir), false, + getter_AddRefs(file)); +# endif + } +#endif // defined(XP_UNIX) && !defined(XP_MACOSX) + else if (!strcmp(aProperty, XRE_USER_SYS_EXTENSION_DIR)) { +#ifdef ENABLE_SYSTEM_EXTENSION_DIRS + rv = GetSysUserExtensionsDirectory(getter_AddRefs(file)); +#endif + } else if (!strcmp(aProperty, XRE_USER_RUNTIME_DIR)) { +#if defined(XP_UNIX) + nsPrintfCString path("/run/user/%d/%s/", getuid(), GetAppName()); + ToLowerCase(path); + rv = NS_NewNativeLocalFile(path, false, getter_AddRefs(file)); +#endif + } else if (!strcmp(aProperty, XRE_APP_DISTRIBUTION_DIR)) { + bool persistent = false; + rv = GetFile(NS_GRE_DIR, &persistent, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + rv = file->AppendNative("distribution"_ns); + } else if (!strcmp(aProperty, XRE_APP_FEATURES_DIR)) { + rv = GetAppDir()->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + rv = file->AppendNative("features"_ns); + } else if (!strcmp(aProperty, XRE_ADDON_APP_DIR)) { + nsCOMPtr<nsIDirectoryServiceProvider> dirsvc( + do_GetService("@mozilla.org/file/directory_service;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + bool unused; + rv = dirsvc->GetFile("XCurProcD", &unused, getter_AddRefs(file)); + } +#if defined(MOZ_CONTENT_TEMP_DIR) + else if (!strcmp(aProperty, NS_APP_CONTENT_PROCESS_TEMP_DIR)) { + if (!mContentTempDir) { + rv = LoadContentProcessTempDir(); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = mContentTempDir->Clone(getter_AddRefs(file)); + } +#endif // defined(MOZ_CONTENT_TEMP_DIR) + else if (!strcmp(aProperty, NS_APP_USER_CHROME_DIR)) { + // We need to allow component, xpt, and chrome registration to + // occur prior to the profile-after-change notification. + rv = GetProfileStartupDir(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + rv = file->AppendNative("chrome"_ns); + } else if (!strcmp(aProperty, NS_APP_PREFS_50_DIR)) { + rv = GetProfileDir(getter_AddRefs(file)); + } else if (!strcmp(aProperty, NS_APP_PREFS_50_FILE)) { + rv = GetProfileDir(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + rv = file->AppendNative("prefs.js"_ns); + } else if (!strcmp(aProperty, NS_APP_PREFS_OVERRIDE_DIR)) { + rv = GetProfileDir(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + rv = file->AppendNative(nsLiteralCString(PREF_OVERRIDE_DIRNAME)); + NS_ENSURE_SUCCESS(rv, rv); + rv = EnsureDirectoryExists(file); + } else { + // We don't know anything about this property. Fail without warning, because + // otherwise we'll get too much warning spam due to + // nsDirectoryService::Get() trying everything it gets with every provider. + return NS_ERROR_FAILURE; + } + + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(file, NS_ERROR_FAILURE); + + file.forget(aFile); + return NS_OK; +} + +static void LoadDirIntoArray(nsIFile* dir, const char* const* aAppendList, + nsCOMArray<nsIFile>& aDirectories) { + if (!dir) return; + + nsCOMPtr<nsIFile> subdir; + dir->Clone(getter_AddRefs(subdir)); + if (!subdir) return; + + for (const char* const* a = aAppendList; *a; ++a) { + subdir->AppendNative(nsDependentCString(*a)); + } + + bool exists; + if (NS_SUCCEEDED(subdir->Exists(&exists)) && exists) { + aDirectories.AppendObject(subdir); + } +} + +#if defined(MOZ_CONTENT_TEMP_DIR) + +static const char* GetProcessTempBaseDirKey() { return NS_OS_TEMP_DIR; } + +// +// Sets mContentTempDir so that it refers to the appropriate temp dir. +// If the sandbox is enabled, NS_APP_CONTENT_PROCESS_TEMP_DIR, otherwise +// NS_OS_TEMP_DIR is used. +// +nsresult nsXREDirProvider::LoadContentProcessTempDir() { + // The parent is responsible for creating the sandbox temp dir. + if (XRE_IsParentProcess()) { + mContentProcessSandboxTempDir = + CreateProcessSandboxTempDir(GeckoProcessType_Content); + mContentTempDir = mContentProcessSandboxTempDir; + } else { + mContentTempDir = !IsContentSandboxDisabled() + ? GetProcessSandboxTempDir(GeckoProcessType_Content) + : nullptr; + } + + if (!mContentTempDir) { + nsresult rv = + NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(mContentTempDir)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + +static bool IsContentSandboxDisabled() { + return !mozilla::BrowserTabsRemoteAutostart() || + (!mozilla::IsContentSandboxEnabled()); +} + +// +// If a process sandbox temp dir is to be used, returns an nsIFile +// for the directory. Returns null if an error occurs. +// +static already_AddRefed<nsIFile> GetProcessSandboxTempDir( + GeckoProcessType type) { + nsCOMPtr<nsIFile> localFile; + + nsresult rv = NS_GetSpecialDirectory(GetProcessTempBaseDirKey(), + getter_AddRefs(localFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + MOZ_ASSERT(type == GeckoProcessType_Content); + + const char* prefKey = "security.sandbox.content.tempDirSuffix"; + nsAutoString tempDirSuffix; + rv = mozilla::Preferences::GetString(prefKey, tempDirSuffix); + if (NS_WARN_IF(NS_FAILED(rv)) || tempDirSuffix.IsEmpty()) { + return nullptr; + } + + rv = localFile->Append(u"Temp-"_ns + tempDirSuffix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + return localFile.forget(); +} + +// +// Create a temporary directory for use from sandboxed processes. +// Only called in the parent. The path is derived from a UUID stored in a +// pref which is available to content processes. Returns null +// if the content sandbox is disabled or if an error occurs. +// +static already_AddRefed<nsIFile> CreateProcessSandboxTempDir( + GeckoProcessType procType) { + if ((procType == GeckoProcessType_Content) && IsContentSandboxDisabled()) { + return nullptr; + } + + MOZ_ASSERT(procType == GeckoProcessType_Content); + + // Get (and create if blank) temp directory suffix pref. + const char* pref = "security.sandbox.content.tempDirSuffix"; + + nsresult rv; + nsAutoString tempDirSuffix; + mozilla::Preferences::GetString(pref, tempDirSuffix); + + if (tempDirSuffix.IsEmpty()) { + nsID uuid; + rv = nsID::GenerateUUIDInPlace(uuid); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + char uuidChars[NSID_LENGTH]; + uuid.ToProvidedString(uuidChars); + tempDirSuffix.AssignASCII(uuidChars, NSID_LENGTH); +# ifdef XP_UNIX + // Braces in a path are somewhat annoying to deal with + // and pretty alien on Unix + tempDirSuffix.StripChars(u"{}"); +# endif + + // Save the pref + rv = mozilla::Preferences::SetString(pref, tempDirSuffix); + if (NS_WARN_IF(NS_FAILED(rv))) { + // If we fail to save the pref we don't want to create the temp dir, + // because we won't be able to clean it up later. + return nullptr; + } + + nsCOMPtr<nsIPrefService> prefsvc = mozilla::Preferences::GetService(); + if (!prefsvc || NS_FAILED((rv = prefsvc->SavePrefFile(nullptr)))) { + // Again, if we fail to save the pref file we might not be able to clean + // up the temp directory, so don't create one. Note that in the case + // the preference values allows an off main thread save, the successful + // return from the call doesn't mean we actually saved the file. See + // bug 1364496 for details. + NS_WARNING("Failed to save pref file, cannot create temp dir."); + return nullptr; + } + } + + nsCOMPtr<nsIFile> sandboxTempDir = GetProcessSandboxTempDir(procType); + if (!sandboxTempDir) { + NS_WARNING("Failed to determine sandbox temp dir path."); + return nullptr; + } + + // Remove the directory. It may exist due to a previous crash. + if (NS_FAILED(DeleteDirIfExists(sandboxTempDir))) { + NS_WARNING("Failed to reset sandbox temp dir."); + return nullptr; + } + + // Create the directory + rv = sandboxTempDir->Create(nsIFile::DIRECTORY_TYPE, 0700); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to create sandbox temp dir."); + return nullptr; + } + + return sandboxTempDir.forget(); +} + +static nsresult DeleteDirIfExists(nsIFile* dir) { + if (dir) { + // Don't return an error if the directory doesn't exist. + nsresult rv = dir->Remove(/* aRecursive */ true); + if (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND) { + return rv; + } + } + return NS_OK; +} + +#endif // defined(MOZ_CONTENT_TEMP_DIR) + +static const char* const kAppendPrefDir[] = {"defaults", "preferences", + nullptr}; +#ifdef MOZ_BACKGROUNDTASKS +static const char* const kAppendBackgroundTasksPrefDir[] = { + "defaults", "backgroundtasks", nullptr}; +#endif + +NS_IMETHODIMP +nsXREDirProvider::GetFiles(const char* aProperty, + nsISimpleEnumerator** aResult) { + nsresult rv = NS_ERROR_FAILURE; + *aResult = nullptr; + + if (!strcmp(aProperty, NS_APP_PREFS_DEFAULTS_DIR_LIST)) { + nsCOMArray<nsIFile> directories; + + LoadDirIntoArray(mXULAppDir, kAppendPrefDir, directories); +#ifdef MOZ_BACKGROUNDTASKS + if (mozilla::BackgroundTasks::IsBackgroundTaskMode()) { + LoadDirIntoArray(mGREDir, kAppendBackgroundTasksPrefDir, directories); + LoadDirIntoArray(mXULAppDir, kAppendBackgroundTasksPrefDir, directories); + } +#endif + + rv = NS_NewArrayEnumerator(aResult, directories, NS_GET_IID(nsIFile)); + } else if (!strcmp(aProperty, NS_APP_CHROME_DIR_LIST)) { + // NS_APP_CHROME_DIR_LIST is only used to get default (native) icons + // for OS window decoration. + + static const char* const kAppendChromeDir[] = {"chrome", nullptr}; + nsCOMArray<nsIFile> directories; + LoadDirIntoArray(mXULAppDir, kAppendChromeDir, directories); + + rv = NS_NewArrayEnumerator(aResult, directories, NS_GET_IID(nsIFile)); + } + NS_ENSURE_SUCCESS(rv, rv); + + return NS_SUCCESS_AGGREGATE_RESULT; +} + +NS_IMETHODIMP +nsXREDirProvider::GetDirectory(nsIFile** aResult) { + NS_ENSURE_TRUE(mProfileDir, NS_ERROR_NOT_INITIALIZED); + return mProfileDir->Clone(aResult); +} + +void nsXREDirProvider::InitializeUserPrefs() { + if (!mPrefsInitialized) { + mozilla::Preferences::InitializeUserPrefs(); + } +} + +void nsXREDirProvider::FinishInitializingUserPrefs() { + if (!mPrefsInitialized) { + mozilla::Preferences::FinishInitializingUserPrefs(); + mPrefsInitialized = true; + } +} + +NS_IMETHODIMP +nsXREDirProvider::DoStartup() { + nsresult rv; + + if (!mAppStarted) { + nsCOMPtr<nsIObserverService> obsSvc = + mozilla::services::GetObserverService(); + if (!obsSvc) return NS_ERROR_FAILURE; + + mAppStarted = true; + + /* + Make sure we've setup prefs before profile-do-change to be able to use + them to track crashes and because we want to begin crash tracking before + other code run from this notification since they may cause crashes. + */ + MOZ_ASSERT(mPrefsInitialized); + + bool safeModeNecessary = false; + nsCOMPtr<nsIAppStartup> appStartup( + mozilla::components::AppStartup::Service()); + if (appStartup) { + rv = appStartup->TrackStartupCrashBegin(&safeModeNecessary); + if (NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE) + NS_WARNING("Error while beginning startup crash tracking"); + + if (!gSafeMode && safeModeNecessary) { + appStartup->RestartInSafeMode(nsIAppStartup::eForceQuit); + return NS_OK; + } + } + + static const char16_t kStartup[] = {'s', 't', 'a', 'r', + 't', 'u', 'p', '\0'}; + obsSvc->NotifyObservers(nullptr, "profile-do-change", kStartup); + + // Initialize the Enterprise Policies service in the parent process + // In the content process it's loaded on demand when needed + if (XRE_IsParentProcess()) { + nsCOMPtr<nsIObserver> policies( + do_GetService("@mozilla.org/enterprisepolicies;1")); + if (policies) { + policies->Observe(nullptr, "policies-startup", nullptr); + } + } + +#ifdef MOZ_THUNDERBIRD + bool bgtaskMode = false; +# ifdef MOZ_BACKGROUNDTASKS + bgtaskMode = mozilla::BackgroundTasks::IsBackgroundTaskMode(); +# endif + if (!bgtaskMode && + mozilla::Preferences::GetBool( + "security.prompt_for_master_password_on_startup", false)) { + // Prompt for the master password prior to opening application windows, + // to avoid the race that triggers multiple prompts (see bug 177175). + // We use this code until we have a better solution, possibly as + // described in bug 177175 comment 384. + nsCOMPtr<nsIPK11TokenDB> db = + do_GetService("@mozilla.org/security/pk11tokendb;1"); + if (db) { + nsCOMPtr<nsIPK11Token> token; + if (NS_SUCCEEDED(db->GetInternalKeyToken(getter_AddRefs(token)))) { + mozilla::Unused << token->Login(false); + } + } else { + NS_WARNING("Failed to get nsIPK11TokenDB service."); + } + } +#endif + + bool initExtensionManager = +#ifdef MOZ_BACKGROUNDTASKS + !mozilla::BackgroundTasks::IsBackgroundTaskMode(); +#else + true; +#endif + if (initExtensionManager) { + // Init the Extension Manager + nsCOMPtr<nsIObserver> em = + do_GetService("@mozilla.org/addons/integration;1"); + if (em) { + em->Observe(nullptr, "addons-startup", nullptr); + } else { + NS_WARNING("Failed to create Addons Manager."); + } + } + + obsSvc->NotifyObservers(nullptr, "profile-after-change", kStartup); + + // Any component that has registered for the profile-after-change category + // should also be created at this time. + (void)NS_CreateServicesFromCategory("profile-after-change", nullptr, + "profile-after-change"); + + if (gSafeMode && safeModeNecessary) { + static const char16_t kCrashed[] = {'c', 'r', 'a', 's', + 'h', 'e', 'd', '\0'}; + obsSvc->NotifyObservers(nullptr, "safemode-forced", kCrashed); + } + + // 1 = Regular mode, 2 = Safe mode, 3 = Safe mode forced + int mode = 1; + if (gSafeMode) { + if (safeModeNecessary) + mode = 3; + else + mode = 2; + } + mozilla::Telemetry::Accumulate(mozilla::Telemetry::SAFE_MODE_USAGE, mode); + + obsSvc->NotifyObservers(nullptr, "profile-initial-state", nullptr); + +#if defined(MOZ_CONTENT_TEMP_DIR) + // Makes sure the content temp dir has been loaded if it hasn't been + // already. In the parent this ensures it has been created before we attempt + // to start any content processes. + if (!mContentTempDir) { + mozilla::Unused << NS_WARN_IF(NS_FAILED(LoadContentProcessTempDir())); + } +#endif + } + return NS_OK; +} + +void nsXREDirProvider::DoShutdown() { + AUTO_PROFILER_LABEL("nsXREDirProvider::DoShutdown", OTHER); + + if (mAppStarted) { + mozilla::AppShutdown::AdvanceShutdownPhase( + mozilla::ShutdownPhase::AppShutdownNetTeardown, nullptr); + mozilla::AppShutdown::AdvanceShutdownPhase( + mozilla::ShutdownPhase::AppShutdownTeardown, nullptr); + +#ifdef DEBUG + // Not having this causes large intermittent leaks. See bug 1340425. + if (JSContext* cx = mozilla::dom::danger::GetJSContext()) { + JS_GC(cx); + } +#endif + + mozilla::AppShutdown::AdvanceShutdownPhase( + mozilla::ShutdownPhase::AppShutdown, nullptr); + mozilla::AppShutdown::AdvanceShutdownPhase( + mozilla::ShutdownPhase::AppShutdownQM, nullptr); + mozilla::AppShutdown::AdvanceShutdownPhase( + mozilla::ShutdownPhase::AppShutdownTelemetry, nullptr); + mAppStarted = false; + } + + gDataDirProfileLocal = nullptr; + gDataDirProfile = nullptr; + +#if defined(MOZ_CONTENT_TEMP_DIR) + if (XRE_IsParentProcess()) { + mozilla::Unused << DeleteDirIfExists(mContentProcessSandboxTempDir); + } +#endif +} + +#ifdef XP_WIN +static nsresult GetShellFolderPath(KNOWNFOLDERID folder, nsAString& _retval) { + DWORD flags = KF_FLAG_SIMPLE_IDLIST | KF_FLAG_DONT_VERIFY | KF_FLAG_NO_ALIAS; + PWSTR path = nullptr; + + if (!SUCCEEDED(SHGetKnownFolderPath(folder, flags, NULL, &path))) { + return NS_ERROR_NOT_AVAILABLE; + } + + _retval = nsDependentString(path); + CoTaskMemFree(path); + return NS_OK; +} + +/** + * Provides a fallback for getting the path to APPDATA or LOCALAPPDATA by + * querying the registry when the call to SHGetSpecialFolderLocation or + * SHGetPathFromIDListW is unable to provide these paths (Bug 513958). + */ +static nsresult GetRegWindowsAppDataFolder(bool aLocal, nsAString& _retval) { + HKEY key; + LPCWSTR keyName = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders"; + DWORD res = ::RegOpenKeyExW(HKEY_CURRENT_USER, keyName, 0, KEY_READ, &key); + if (res != ERROR_SUCCESS) { + _retval.SetLength(0); + return NS_ERROR_NOT_AVAILABLE; + } + + DWORD type, size; + res = RegQueryValueExW(key, (aLocal ? L"Local AppData" : L"AppData"), nullptr, + &type, nullptr, &size); + // The call to RegQueryValueExW must succeed, the type must be REG_SZ, the + // buffer size must not equal 0, and the buffer size be a multiple of 2. + if (res != ERROR_SUCCESS || type != REG_SZ || size == 0 || size % 2 != 0) { + ::RegCloseKey(key); + _retval.SetLength(0); + return NS_ERROR_NOT_AVAILABLE; + } + + // |size| may or may not include room for the terminating null character + DWORD resultLen = size / 2; + + if (!_retval.SetLength(resultLen, mozilla::fallible)) { + ::RegCloseKey(key); + _retval.SetLength(0); + return NS_ERROR_NOT_AVAILABLE; + } + + auto begin = _retval.BeginWriting(); + + res = RegQueryValueExW(key, (aLocal ? L"Local AppData" : L"AppData"), nullptr, + nullptr, (LPBYTE)begin, &size); + ::RegCloseKey(key); + if (res != ERROR_SUCCESS) { + _retval.SetLength(0); + return NS_ERROR_NOT_AVAILABLE; + } + + if (!_retval.CharAt(resultLen - 1)) { + // It was already null terminated. + _retval.Truncate(resultLen - 1); + } + + return NS_OK; +} +#endif + +static nsresult HashInstallPath(nsAString& aInstallPath, nsAString& aPathHash) { + mozilla::UniquePtr<NS_tchar[]> hash; + bool success = ::GetInstallHash(PromiseFlatString(aInstallPath).get(), hash); + if (!success) { + return NS_ERROR_FAILURE; + } + + // The hash string is a NS_tchar*, which is wchar* in Windows and char* + // elsewhere. +#ifdef XP_WIN + aPathHash.Assign(hash.get()); +#else + aPathHash.AssignASCII(hash.get()); +#endif + return NS_OK; +} + +/** + * Gets a hash of the installation directory. + */ +nsresult nsXREDirProvider::GetInstallHash(nsAString& aPathHash) { + nsAutoString stringToHash; + +#ifdef XP_WIN + if (mozilla::widget::WinUtils::HasPackageIdentity()) { + // For packages, the install path includes the version number, so it isn't + // a stable or consistent identifier for the installation. The package + // family name is though, so use that instead of the path. + stringToHash = mozilla::widget::WinUtils::GetPackageFamilyName(); + } else +#endif + { + nsCOMPtr<nsIFile> installDir; + nsCOMPtr<nsIFile> appFile; + bool per = false; + nsresult rv = GetFile(XRE_EXECUTABLE_FILE, &per, getter_AddRefs(appFile)); + NS_ENSURE_SUCCESS(rv, rv); + rv = appFile->GetParent(getter_AddRefs(installDir)); + NS_ENSURE_SUCCESS(rv, rv); + + // It is possible that the path we have is on a case insensitive + // filesystem in which case the path may vary depending on how the + // application is called. We want to normalize the case somehow. +#ifdef XP_WIN + // Windows provides a way to get the correct case. + if (!mozilla::widget::WinUtils::ResolveJunctionPointsAndSymLinks( + installDir)) { + NS_WARNING("Failed to resolve install directory."); + } +#elif defined(MOZ_WIDGET_COCOA) + // On OSX roundtripping through an FSRef fixes the case. + FSRef ref; + nsCOMPtr<nsILocalFileMac> macFile = do_QueryInterface(installDir); + rv = macFile->GetFSRef(&ref); + NS_ENSURE_SUCCESS(rv, rv); + rv = NS_NewLocalFileWithFSRef(&ref, true, getter_AddRefs(macFile)); + NS_ENSURE_SUCCESS(rv, rv); + installDir = static_cast<nsIFile*>(macFile); +#endif + // On linux XRE_EXECUTABLE_FILE already seems to be set to the correct path. + + rv = installDir->GetPath(stringToHash); + NS_ENSURE_SUCCESS(rv, rv); + } + + // If we somehow failed to get an actual value, hashing an empty string could + // potentially cause some serious problems given all the things this hash is + // used for. So we don't allow that. + if (stringToHash.IsEmpty()) { + return NS_ERROR_FAILURE; + } + + return HashInstallPath(stringToHash, aPathHash); +} + +/** + * Before bug 1555319 the directory hashed can have had an incorrect case. + * Access to that hash is still available through this function. It is needed so + * we can migrate users who may have an incorrect hash in profiles.ini. This + * support can probably be removed in a few releases time. + */ +nsresult nsXREDirProvider::GetLegacyInstallHash(nsAString& aPathHash) { + nsCOMPtr<nsIFile> installDir; + nsCOMPtr<nsIFile> appFile; + bool per = false; + nsresult rv = GetFile(XRE_EXECUTABLE_FILE, &per, getter_AddRefs(appFile)); + NS_ENSURE_SUCCESS(rv, rv); + rv = appFile->GetParent(getter_AddRefs(installDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString installPath; + rv = installDir->GetPath(installPath); + NS_ENSURE_SUCCESS(rv, rv); + +#ifdef XP_WIN +# if defined(MOZ_THUNDERBIRD) || defined(MOZ_SUITE) + // Convert a 64-bit install path to what would have been the 32-bit install + // path to allow users to migrate their profiles from one to the other. + PWSTR pathX86 = nullptr; + HRESULT hres = + SHGetKnownFolderPath(FOLDERID_ProgramFilesX86, 0, nullptr, &pathX86); + if (SUCCEEDED(hres)) { + nsDependentString strPathX86(pathX86); + if (!StringBeginsWith(installPath, strPathX86, + nsCaseInsensitiveStringComparator)) { + PWSTR path = nullptr; + hres = SHGetKnownFolderPath(FOLDERID_ProgramFiles, 0, nullptr, &path); + if (SUCCEEDED(hres)) { + if (StringBeginsWith(installPath, nsDependentString(path), + nsCaseInsensitiveStringComparator)) { + installPath.Replace(0, wcslen(path), strPathX86); + } + } + CoTaskMemFree(path); + } + } + CoTaskMemFree(pathX86); +# endif +#endif + return HashInstallPath(installPath, aPathHash); +} + +nsresult nsXREDirProvider::GetUpdateRootDir(nsIFile** aResult, + bool aGetOldLocation) { +#ifndef XP_WIN + // There is no old update location on platforms other than Windows. Windows is + // the only platform for which we migrated the update directory. + if (aGetOldLocation) { + return NS_ERROR_NOT_IMPLEMENTED; + } +#endif + nsCOMPtr<nsIFile> updRoot; + nsCOMPtr<nsIFile> appFile; + bool per = false; + nsresult rv = GetFile(XRE_EXECUTABLE_FILE, &per, getter_AddRefs(appFile)); + NS_ENSURE_SUCCESS(rv, rv); + rv = appFile->GetParent(getter_AddRefs(updRoot)); + NS_ENSURE_SUCCESS(rv, rv); + +#ifdef XP_MACOSX + nsCOMPtr<nsIFile> appRootDirFile; + nsCOMPtr<nsIFile> localDir; + nsAutoString appDirPath; + if (NS_FAILED(appFile->GetParent(getter_AddRefs(appRootDirFile))) || + NS_FAILED(appRootDirFile->GetPath(appDirPath)) || + NS_FAILED(GetUserDataDirectoryHome(getter_AddRefs(localDir), true))) { + return NS_ERROR_FAILURE; + } + + int32_t dotIndex = appDirPath.RFind(u".app"); + if (dotIndex == kNotFound) { + dotIndex = appDirPath.Length(); + } + appDirPath = Substring(appDirPath, 1, dotIndex - 1); + + bool hasVendor = GetAppVendor() && strlen(GetAppVendor()) != 0; + if (hasVendor || GetAppName()) { + if (NS_FAILED(localDir->AppendNative( + nsDependentCString(hasVendor ? GetAppVendor() : GetAppName())))) { + return NS_ERROR_FAILURE; + } + } else if (NS_FAILED(localDir->AppendNative("Mozilla"_ns))) { + return NS_ERROR_FAILURE; + } + + if (NS_FAILED(localDir->Append(u"updates"_ns)) || + NS_FAILED(localDir->AppendRelativePath(appDirPath))) { + return NS_ERROR_FAILURE; + } + + localDir.forget(aResult); + return NS_OK; + +#elif XP_WIN + nsAutoString installPath; + rv = updRoot->GetPath(installPath); + NS_ENSURE_SUCCESS(rv, rv); + + mozilla::UniquePtr<wchar_t[]> updatePath; + HRESULT hrv; + if (aGetOldLocation) { + hrv = + GetOldUpdateDirectory(PromiseFlatString(installPath).get(), updatePath); + } else { + hrv = GetCommonUpdateDirectory(PromiseFlatString(installPath).get(), + updatePath); + } + if (FAILED(hrv)) { + return NS_ERROR_FAILURE; + } + nsAutoString updatePathStr; + updatePathStr.Assign(updatePath.get()); + updRoot->InitWithPath(updatePathStr); + updRoot.forget(aResult); + return NS_OK; +#else + updRoot.forget(aResult); + return NS_OK; +#endif // XP_WIN +} + +nsresult nsXREDirProvider::GetProfileStartupDir(nsIFile** aResult) { + NS_ENSURE_TRUE(mProfileDir, NS_ERROR_FAILURE); + return mProfileDir->Clone(aResult); +} + +nsresult nsXREDirProvider::GetProfileDir(nsIFile** aResult) { + if (!mProfileDir) { + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mProfileDir)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(mProfileDir, NS_ERROR_FAILURE); + } + return mProfileDir->Clone(aResult); +} + +NS_IMETHODIMP +nsXREDirProvider::SetUserDataDirectory(nsIFile* aFile, bool aLocal) { + if (aLocal) { + NS_IF_RELEASE(gDataDirHomeLocal); + NS_IF_ADDREF(gDataDirHomeLocal = aFile); + } else { + NS_IF_RELEASE(gDataDirHome); + NS_IF_ADDREF(gDataDirHome = aFile); + } + + return NS_OK; +} + +/* static */ +nsresult nsXREDirProvider::SetUserDataProfileDirectory(nsCOMPtr<nsIFile>& aFile, + bool aLocal) { + if (aLocal) { + gDataDirProfileLocal = aFile; + } else { + gDataDirProfile = aFile; + } + + return NS_OK; +} + +nsresult nsXREDirProvider::GetUserDataDirectoryHome(nsIFile** aFile, + bool aLocal) { + // Copied from nsAppFileLocationProvider (more or less) + nsresult rv; + nsCOMPtr<nsIFile> localDir; + + if (aLocal && gDataDirHomeLocal) { + return gDataDirHomeLocal->Clone(aFile); + } + if (!aLocal && gDataDirHome) { + return gDataDirHome->Clone(aFile); + } + +#if defined(XP_MACOSX) + FSRef fsRef; + OSType folderType; + if (aLocal) { + folderType = kCachedDataFolderType; + } else { +# ifdef MOZ_THUNDERBIRD + folderType = kDomainLibraryFolderType; +# else + folderType = kApplicationSupportFolderType; +# endif + } + OSErr err = ::FSFindFolder(kUserDomain, folderType, kCreateFolder, &fsRef); + NS_ENSURE_FALSE(err, NS_ERROR_FAILURE); + + rv = NS_NewNativeLocalFile(""_ns, true, getter_AddRefs(localDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsILocalFileMac> dirFileMac = do_QueryInterface(localDir); + NS_ENSURE_TRUE(dirFileMac, NS_ERROR_UNEXPECTED); + + rv = dirFileMac->InitWithFSRef(&fsRef); + NS_ENSURE_SUCCESS(rv, rv); + + localDir = dirFileMac; +#elif defined(XP_IOS) + nsAutoCString userDir; + if (GetUIKitDirectory(aLocal, userDir)) { + rv = NS_NewNativeLocalFile(userDir, true, getter_AddRefs(localDir)); + } else { + rv = NS_ERROR_FAILURE; + } + NS_ENSURE_SUCCESS(rv, rv); +#elif defined(XP_WIN) + nsString path; + if (aLocal) { + rv = GetShellFolderPath(FOLDERID_LocalAppData, path); + if (NS_FAILED(rv)) rv = GetRegWindowsAppDataFolder(aLocal, path); + } + if (!aLocal || NS_FAILED(rv)) { + rv = GetShellFolderPath(FOLDERID_RoamingAppData, path); + if (NS_FAILED(rv)) { + if (!aLocal) rv = GetRegWindowsAppDataFolder(aLocal, path); + } + } + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_NewLocalFile(path, true, getter_AddRefs(localDir)); +#elif defined(XP_UNIX) + const char* homeDir = getenv("HOME"); + if (!homeDir || !*homeDir) return NS_ERROR_FAILURE; + +# ifdef ANDROID /* We want (ProfD == ProfLD) on Android. */ + aLocal = false; +# endif + + if (aLocal) { + // If $XDG_CACHE_HOME is defined use it, otherwise use $HOME/.cache. + const char* cacheHome = getenv("XDG_CACHE_HOME"); + if (cacheHome && *cacheHome) { + rv = NS_NewNativeLocalFile(nsDependentCString(cacheHome), true, + getter_AddRefs(localDir)); + } else { + rv = NS_NewNativeLocalFile(nsDependentCString(homeDir), true, + getter_AddRefs(localDir)); + if (NS_SUCCEEDED(rv)) rv = localDir->AppendNative(".cache"_ns); + } + } else { + rv = NS_NewNativeLocalFile(nsDependentCString(homeDir), true, + getter_AddRefs(localDir)); + } +#else +# error "Don't know how to get product dir on your platform" +#endif + + NS_IF_ADDREF(*aFile = localDir); + return rv; +} + +nsresult nsXREDirProvider::GetSysUserExtensionsDirectory(nsIFile** aFile) { + nsCOMPtr<nsIFile> localDir; + nsresult rv = GetUserDataDirectoryHome(getter_AddRefs(localDir), false); + NS_ENSURE_SUCCESS(rv, rv); + + rv = AppendSysUserExtensionPath(localDir); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnsureDirectoryExists(localDir); + NS_ENSURE_SUCCESS(rv, rv); + + localDir.forget(aFile); + return NS_OK; +} + +#if defined(XP_UNIX) || defined(XP_MACOSX) +nsresult nsXREDirProvider::GetSystemExtensionsDirectory(nsIFile** aFile) { + nsresult rv; + nsCOMPtr<nsIFile> localDir; + + rv = GetSystemParentDirectory(getter_AddRefs(localDir)); + if (NS_SUCCEEDED(rv)) { + constexpr auto sExtensions = +# if defined(XP_MACOSX) + "Extensions"_ns +# else + "extensions"_ns +# endif + ; + + rv = localDir->AppendNative(sExtensions); + if (NS_SUCCEEDED(rv)) { + localDir.forget(aFile); + } + } + return rv; +} +#endif + +nsresult nsXREDirProvider::GetUserDataDirectory(nsIFile** aFile, bool aLocal) { + nsCOMPtr<nsIFile> localDir; + + if (aLocal && gDataDirProfileLocal) { + return gDataDirProfileLocal->Clone(aFile); + } + if (!aLocal && gDataDirProfile) { + return gDataDirProfile->Clone(aFile); + } + + nsresult rv = GetUserDataDirectoryHome(getter_AddRefs(localDir), aLocal); + NS_ENSURE_SUCCESS(rv, rv); + + rv = AppendProfilePath(localDir, aLocal); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnsureDirectoryExists(localDir); + NS_ENSURE_SUCCESS(rv, rv); + + nsXREDirProvider::SetUserDataProfileDirectory(localDir, aLocal); + + localDir.forget(aFile); + return NS_OK; +} + +nsresult nsXREDirProvider::EnsureDirectoryExists(nsIFile* aDirectory) { + nsresult rv = aDirectory->Create(nsIFile::DIRECTORY_TYPE, 0700); + + if (rv == NS_ERROR_FILE_ALREADY_EXISTS) { + rv = NS_OK; + } + return rv; +} + +nsresult nsXREDirProvider::AppendSysUserExtensionPath(nsIFile* aFile) { + NS_ASSERTION(aFile, "Null pointer!"); + + nsresult rv; + +#if defined(XP_MACOSX) || defined(XP_WIN) + + static const char* const sXR = "Mozilla"; + rv = aFile->AppendNative(nsDependentCString(sXR)); + NS_ENSURE_SUCCESS(rv, rv); + + static const char* const sExtensions = "Extensions"; + rv = aFile->AppendNative(nsDependentCString(sExtensions)); + NS_ENSURE_SUCCESS(rv, rv); + +#elif defined(XP_UNIX) + + static const char* const sXR = ".mozilla"; + rv = aFile->AppendNative(nsDependentCString(sXR)); + NS_ENSURE_SUCCESS(rv, rv); + + static const char* const sExtensions = "extensions"; + rv = aFile->AppendNative(nsDependentCString(sExtensions)); + NS_ENSURE_SUCCESS(rv, rv); + +#else +# error "Don't know how to get XRE user extension path on your platform" +#endif + return NS_OK; +} + +nsresult nsXREDirProvider::AppendProfilePath(nsIFile* aFile, bool aLocal) { + NS_ASSERTION(aFile, "Null pointer!"); + + // If there is no XREAppData then there is no information to use to build + // the profile path so just do nothing. This should only happen in xpcshell + // tests. + if (!gAppData) { + return NS_OK; + } + + nsAutoCString profile; + nsAutoCString appName; + nsAutoCString vendor; + if (gAppData->profile) { + profile = gAppData->profile; + } else { + appName = gAppData->name; + vendor = gAppData->vendor; + } + + nsresult rv = NS_OK; + +#if defined(XP_MACOSX) + if (!profile.IsEmpty()) { + rv = AppendProfileString(aFile, profile.get()); + } else { + // Note that MacOS ignores the vendor when creating the profile hierarchy - + // all application preferences directories live alongside one another in + // ~/Library/Application Support/ + rv = aFile->AppendNative(appName); + } + NS_ENSURE_SUCCESS(rv, rv); + +#elif defined(XP_WIN) + if (!profile.IsEmpty()) { + rv = AppendProfileString(aFile, profile.get()); + } else { + if (!vendor.IsEmpty()) { + rv = aFile->AppendNative(vendor); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = aFile->AppendNative(appName); + } + NS_ENSURE_SUCCESS(rv, rv); + +#elif defined(ANDROID) + // The directory used for storing profiles + // The parent of this directory is set in GetUserDataDirectoryHome + // XXX: handle gAppData->profile properly + // XXXsmaug ...and the rest of the profile creation! + rv = aFile->AppendNative(nsDependentCString("mozilla")); + NS_ENSURE_SUCCESS(rv, rv); +#elif defined(XP_UNIX) + nsAutoCString folder; + // Make it hidden (by starting with "."), except when local (the + // profile is already under ~/.cache or XDG_CACHE_HOME). + if (!aLocal) folder.Assign('.'); + + if (!profile.IsEmpty()) { + // Skip any leading path characters + const char* profileStart = profile.get(); + while (*profileStart == '/' || *profileStart == '\\') profileStart++; + + // On the off chance that someone wanted their folder to be hidden don't + // let it become ".." + if (*profileStart == '.' && !aLocal) profileStart++; + + folder.Append(profileStart); + ToLowerCase(folder); + + rv = AppendProfileString(aFile, folder.BeginReading()); + } else { + if (!vendor.IsEmpty()) { + folder.Append(vendor); + ToLowerCase(folder); + + rv = aFile->AppendNative(folder); + NS_ENSURE_SUCCESS(rv, rv); + + folder.Truncate(); + } + + // This can be the case in tests. + if (!appName.IsEmpty()) { + folder.Append(appName); + ToLowerCase(folder); + + rv = aFile->AppendNative(folder); + } + } + NS_ENSURE_SUCCESS(rv, rv); + +#else +# error "Don't know how to get profile path on your platform" +#endif + return NS_OK; +} + +nsresult nsXREDirProvider::AppendProfileString(nsIFile* aFile, + const char* aPath) { + NS_ASSERTION(aFile, "Null file!"); + NS_ASSERTION(aPath, "Null path!"); + + nsAutoCString pathDup(aPath); + + char* path = pathDup.BeginWriting(); + + nsresult rv; + char* subdir; + while ((subdir = NS_strtok("/\\", &path))) { + rv = aFile->AppendNative(nsDependentCString(subdir)); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} diff --git a/toolkit/xre/nsXREDirProvider.h b/toolkit/xre/nsXREDirProvider.h new file mode 100644 index 0000000000..e25fccda22 --- /dev/null +++ b/toolkit/xre/nsXREDirProvider.h @@ -0,0 +1,158 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef _nsXREDirProvider_h__ +#define _nsXREDirProvider_h__ + +#include "nsIDirectoryService.h" +#include "nsIProfileMigrator.h" +#include "nsIFile.h" +#include "nsIXREDirProvider.h" + +#include "nsCOMPtr.h" +#include "nsCOMArray.h" +#include "mozilla/Attributes.h" +#ifdef MOZ_BACKGROUNDTASKS +# include "mozilla/BackgroundTasks.h" +#endif + +// {5573967d-f6cf-4c63-8e0e-9ac06e04d62b} +#define NS_XREDIRPROVIDER_CID \ + { \ + 0x5573967d, 0xf6cf, 0x4c63, { \ + 0x8e, 0x0e, 0x9a, 0xc0, 0x6e, 0x04, 0xd6, 0x2b \ + } \ + } +#define NS_XREDIRPROVIDER_CONTRACTID "@mozilla.org/xre/directory-provider;1" + +class nsXREDirProvider final : public nsIDirectoryServiceProvider2, + public nsIXREDirProvider, + public nsIProfileStartup { + public: + // we use a custom isupports implementation (no refcount) + NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override; + NS_IMETHOD_(MozExternalRefCountType) AddRef(void) override; + NS_IMETHOD_(MozExternalRefCountType) Release(void) override; + + NS_DECL_NSIDIRECTORYSERVICEPROVIDER + NS_DECL_NSIDIRECTORYSERVICEPROVIDER2 + NS_DECL_NSIXREDIRPROVIDER + NS_DECL_NSIPROFILESTARTUP + + nsXREDirProvider(); + + nsresult Initialize(nsIFile* aXULAppDir, nsIFile* aGREDir); + ~nsXREDirProvider(); + + static already_AddRefed<nsXREDirProvider> GetSingleton(); + + nsresult GetUserProfilesRootDir(nsIFile** aResult); + nsresult GetUserProfilesLocalDir(nsIFile** aResult); +#ifdef MOZ_BACKGROUNDTASKS + // A special location for non-ephemeral background tasks profiles, + // distinct from user profiles. + nsresult GetBackgroundTasksProfilesRootDir(nsIFile** aResult); +#endif + + nsresult GetLegacyInstallHash(nsAString& aPathHash); + + nsresult SetProfile(nsIFile* aProfileDir, nsIFile* aProfileLocalDir); + + void InitializeUserPrefs(); + void FinishInitializingUserPrefs(); + + void DoShutdown(); + + static nsresult GetUserAppDataDirectory(nsIFile** aFile) { + return GetUserDataDirectory(aFile, false); + } + static nsresult GetUserLocalDataDirectory(nsIFile** aFile) { + return GetUserDataDirectory(aFile, true); + } + + static nsresult GetUserDataDirectory(nsIFile** aFile, bool aLocal); + + /* make sure you clone it, if you need to do stuff to it */ + nsIFile* GetGREDir() { return mGREDir; } + nsIFile* GetGREBinDir() { return mGREBinDir; } + nsIFile* GetAppDir() { + if (mXULAppDir) return mXULAppDir; + return mGREDir; + } + + /** + * Get the directory under which update directory is created. + * This method may be called before XPCOM is started. aResult + * is a clone, it may be modified. + * + * If aGetOldLocation is true, this function will return the location of + * the update directory before it was moved from the user profile directory + * to a per-installation directory. This functionality is only meant to be + * used for migration of the update directory to the new location. It is only + * valid to request the old update location on Windows, since that is the only + * platform on which the update directory was migrated. + */ + nsresult GetUpdateRootDir(nsIFile** aResult, bool aGetOldLocation = false); + + /** + * Get the profile startup directory. + * This method may be called before XPCOM is started. aResult + * is a clone, it may be modified. + */ + nsresult GetProfileStartupDir(nsIFile** aResult); + + /** + * Get the profile directory. Only call this method + * when XPCOM is initialized! aResult is a clone, it may be modified. + */ + nsresult GetProfileDir(nsIFile** aResult); + + private: + nsresult GetFilesInternal(const char* aProperty, + nsISimpleEnumerator** aResult); + static nsresult GetUserDataDirectoryHome(nsIFile** aFile, bool aLocal); + static nsresult GetSysUserExtensionsDirectory(nsIFile** aFile); +#if defined(XP_UNIX) || defined(XP_MACOSX) + static nsresult GetSystemExtensionsDirectory(nsIFile** aFile); +#endif + static nsresult EnsureDirectoryExists(nsIFile* aDirectory); + + // Determine the profile path within the UAppData directory. This is different + // on every major platform. + static nsresult AppendProfilePath(nsIFile* aFile, bool aLocal); + + static nsresult AppendSysUserExtensionPath(nsIFile* aFile); + + // Internal helper that splits a path into components using the '/' and '\\' + // delimiters. + static inline nsresult AppendProfileString(nsIFile* aFile, const char* aPath); + + static nsresult SetUserDataProfileDirectory(nsCOMPtr<nsIFile>& aFile, + bool aLocal); + +#if defined(MOZ_CONTENT_TEMP_DIR) + // Load the temp directory for sandboxed content processes + nsresult LoadContentProcessTempDir(); +#endif + + void Append(nsIFile* aDirectory); + + // On OSX, mGREDir points to .app/Contents/Resources + nsCOMPtr<nsIFile> mGREDir; + // On OSX, mGREBinDir points to .app/Contents/MacOS + nsCOMPtr<nsIFile> mGREBinDir; + // On OSX, mXULAppDir points to .app/Contents/Resources/browser + nsCOMPtr<nsIFile> mXULAppDir; + nsCOMPtr<nsIFile> mProfileDir; + nsCOMPtr<nsIFile> mProfileLocalDir; + bool mAppStarted = false; + bool mPrefsInitialized = false; +#if defined(MOZ_CONTENT_TEMP_DIR) + nsCOMPtr<nsIFile> mContentTempDir; + nsCOMPtr<nsIFile> mContentProcessSandboxTempDir; +#endif +}; + +#endif diff --git a/toolkit/xre/platform.ini b/toolkit/xre/platform.ini new file mode 100644 index 0000000000..01c8b741a1 --- /dev/null +++ b/toolkit/xre/platform.ini @@ -0,0 +1,17 @@ +#if 0 +; 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/. +#endif +#filter substitution +#include @TOPOBJDIR@/buildid.h +#include @TOPOBJDIR@/source-repo.h +[Build] +BuildID=@MOZ_BUILDID@ +Milestone=@GRE_MILESTONE@ +#ifdef MOZ_SOURCE_REPO +SourceRepository=@MOZ_SOURCE_REPO@ +#endif +#ifdef MOZ_SOURCE_STAMP +SourceStamp=@MOZ_SOURCE_STAMP@ +#endif diff --git a/toolkit/xre/test/browser.ini b/toolkit/xre/test/browser.ini new file mode 100644 index 0000000000..7f6eaced59 --- /dev/null +++ b/toolkit/xre/test/browser.ini @@ -0,0 +1,4 @@ +[DEFAULT] + +[browser_checkdllblockliststate.js] +skip-if = os != "win" || ccov # Bug 1531789 diff --git a/toolkit/xre/test/browser_checkdllblockliststate.js b/toolkit/xre/test/browser_checkdllblockliststate.js new file mode 100644 index 0000000000..759c9b3635 --- /dev/null +++ b/toolkit/xre/test/browser_checkdllblockliststate.js @@ -0,0 +1,17 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +// Tests that the dll blocklist initializes correctly during test runs. +add_task(async function test() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + function (browser) { + ok( + Services.appinfo.windowsDLLBlocklistStatus, + "Windows dll blocklist status should be true, indicating it is " + + "running properly. A failure in this test is considered a " + + "release blocker." + ); + } + ); +}); diff --git a/toolkit/xre/test/gtest/TestAssembleCommandLineWin.cpp b/toolkit/xre/test/gtest/TestAssembleCommandLineWin.cpp new file mode 100644 index 0000000000..e36c62e0db --- /dev/null +++ b/toolkit/xre/test/gtest/TestAssembleCommandLineWin.cpp @@ -0,0 +1,227 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "gtest/gtest.h" + +#include "mozilla/AssembleCmdLine.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/gtest/MozAssertions.h" +#include "mozilla/UniquePtrExtensions.h" +#include "WinRemoteMessage.h" + +using namespace mozilla; + +template <typename T> +struct TestCase { + const T* mArgs[4]; + const wchar_t* mExpected; +}; + +#define ALPHA_IN_UTF8 "\xe3\x82\xa2\xe3\x83\xab\xe3\x83\x95\xe3\x82\xa1" +#define OMEGA_IN_UTF8 "\xe3\x82\xaa\xe3\x83\xa1\xe3\x82\xac" +#define ALPHA_IN_UTF16 L"\u30A2\u30EB\u30D5\u30A1" +#define OMEGA_IN_UTF16 L"\u30AA\u30E1\u30AC" +#define UPPER_CYRILLIC_P_IN_UTF8 "\xd0\xa0" +#define LOWER_CYRILLIC_P_IN_UTF8 "\xd1\x80" +#define UPPER_CYRILLIC_P_IN_UTF16 L"\u0420" +#define LOWER_CYRILLIC_P_IN_UTF16 L"\u0440" + +TestCase<char> testCases[] = { + // Copied from TestXREMakeCommandLineWin.ini + {{"a:\\", nullptr}, L"a:\\"}, + {{"a:\"", nullptr}, L"a:\\\""}, + {{"a:\\b c", nullptr}, L"\"a:\\b c\""}, + {{"a:\\b c\"", nullptr}, L"\"a:\\b c\\\"\""}, + {{"a:\\b c\\d e", nullptr}, L"\"a:\\b c\\d e\""}, + {{"a:\\b c\\d e\"", nullptr}, L"\"a:\\b c\\d e\\\"\""}, + {{"a:\\", nullptr}, L"a:\\"}, + {{"a:\"", "b:\\c d", nullptr}, L"a:\\\" \"b:\\c d\""}, + {{"a", "b:\" c:\\d", "e", nullptr}, L"a \"b:\\\" c:\\d\" e"}, + {{"abc", "d", "e", nullptr}, L"abc d e"}, + {{"a b c", "d", "e", nullptr}, L"\"a b c\" d e"}, + {{"a\\\\\\b", "de fg", "h", nullptr}, L"a\\\\\\b \"de fg\" h"}, + {{"a", "b", nullptr}, L"a b"}, + {{"a\tb", nullptr}, L"\"a\tb\""}, + {{"a\\\"b", "c", "d", nullptr}, L"a\\\\\\\"b c d"}, + {{"a\\\"b", "c", nullptr}, L"a\\\\\\\"b c"}, + {{"a\\\\\\b c", nullptr}, L"\"a\\\\\\b c\""}, + {{"\"a", nullptr}, L"\\\"a"}, + {{"\\a", nullptr}, L"\\a"}, + {{"\\\\\\a", nullptr}, L"\\\\\\a"}, + {{"\\\\\\\"a", nullptr}, L"\\\\\\\\\\\\\\\"a"}, + {{"a\\\"b c\" d e", nullptr}, L"\"a\\\\\\\"b c\\\" d e\""}, + {{"a\\\\\"b", "c d e", nullptr}, L"a\\\\\\\\\\\"b \"c d e\""}, + {{"a:\\b", "c\\" ALPHA_IN_UTF8, OMEGA_IN_UTF8 "\\d", nullptr}, + L"a:\\b c\\" ALPHA_IN_UTF16 L" " OMEGA_IN_UTF16 L"\\d"}, + {{"a:\\b", "c\\" ALPHA_IN_UTF8 " " OMEGA_IN_UTF8 "\\d", nullptr}, + L"a:\\b \"c\\" ALPHA_IN_UTF16 L" " OMEGA_IN_UTF16 L"\\d\""}, + {{ALPHA_IN_UTF8, OMEGA_IN_UTF8, nullptr}, + ALPHA_IN_UTF16 L" " OMEGA_IN_UTF16}, + + // More single-argument cases + {{"a\fb", nullptr}, L"\"a\fb\""}, + {{"a\nb", nullptr}, L"\"a\nb\""}, + {{"a\rb", nullptr}, L"\"a\rb\""}, + {{"a\vb", nullptr}, L"\"a\vb\""}, + {{"\"a\" \"b\"", nullptr}, L"\"\\\"a\\\" \\\"b\\\"\""}, + {{"\"a\\b\" \"c\\d\"", nullptr}, L"\"\\\"a\\b\\\" \\\"c\\d\\\"\""}, + {{"\\\\ \\\\", nullptr}, L"\"\\\\ \\\\\\\\\""}, + {{"\"\" \"\"", nullptr}, L"\"\\\"\\\" \\\"\\\"\""}, + {{ALPHA_IN_UTF8 "\\" OMEGA_IN_UTF8, nullptr}, + ALPHA_IN_UTF16 L"\\" OMEGA_IN_UTF16}, + {{ALPHA_IN_UTF8 " " OMEGA_IN_UTF8, nullptr}, + L"\"" ALPHA_IN_UTF16 L" " OMEGA_IN_UTF16 L"\""}, + + // Empty string cases + {{"", nullptr}, L"\"\""}, + {{"foo", "", nullptr}, L"foo \"\""}, + {{"", "bar", nullptr}, L"\"\" bar"}, + {{"foo", "", "bar", nullptr}, L"foo \"\" bar"}, +}; + +TEST(AssembleCommandLineWin, assembleCmdLine) +{ + for (const auto& testCase : testCases) { + UniqueFreePtr<wchar_t> assembled; + wchar_t* assembledRaw = nullptr; + EXPECT_EQ(assembleCmdLine(testCase.mArgs, &assembledRaw, CP_UTF8), 0); + assembled.reset(assembledRaw); + + EXPECT_STREQ(assembled.get(), testCase.mExpected); + } +} + +TEST(CommandLineParserWin, HandleCommandLine) +{ + CommandLineParserWin<char> parser; + for (const auto& testCase : testCases) { + NS_ConvertUTF16toUTF8 utf8(testCase.mExpected); + parser.HandleCommandLine(utf8); + + if (utf8.Length() == 0) { + EXPECT_EQ(parser.Argc(), 0); + continue; + } + + for (int i = 0; i < parser.Argc(); ++i) { + EXPECT_NE(testCase.mArgs[i], nullptr); + EXPECT_STREQ(parser.Argv()[i], testCase.mArgs[i]); + } + EXPECT_EQ(testCase.mArgs[parser.Argc()], nullptr); + } +} + +TEST(WinRemoteMessage, SendReceive) +{ + const char kCommandline[] = + "dummy.exe /arg1 --arg2 \"3rd arg\" " + "4th=\"" UPPER_CYRILLIC_P_IN_UTF8 " " LOWER_CYRILLIC_P_IN_UTF8 "\""; + const wchar_t kCommandlineW[] = + L"dummy.exe /arg1 --arg2 \"3rd arg\" " + L"4th=\"" UPPER_CYRILLIC_P_IN_UTF16 L" " LOWER_CYRILLIC_P_IN_UTF16 L"\""; + const wchar_t* kExpectedArgsW[] = { + L"-arg1", L"-arg2", L"3rd arg", + L"4th=" UPPER_CYRILLIC_P_IN_UTF16 L" " LOWER_CYRILLIC_P_IN_UTF16}; + + char workingDirA[MAX_PATH]; + wchar_t workingDirW[MAX_PATH]; + EXPECT_NE(getcwd(workingDirA, MAX_PATH), nullptr); + EXPECT_NE(_wgetcwd(workingDirW, MAX_PATH), nullptr); + + WinRemoteMessageSender v0(kCommandline); + WinRemoteMessageSender v1(kCommandline, workingDirA); + WinRemoteMessageSender v2(kCommandlineW, workingDirW); + + WinRemoteMessageReceiver receiver; + int32_t len; + nsAutoString arg; + nsCOMPtr<nsIFile> workingDir; + + receiver.Parse(v0.CopyData()); + EXPECT_NS_SUCCEEDED(receiver.CommandLineRunner()->GetLength(&len)); + EXPECT_EQ(static_cast<size_t>(len), ArrayLength(kExpectedArgsW)); + for (size_t i = 0; i < ArrayLength(kExpectedArgsW); ++i) { + EXPECT_TRUE( + NS_SUCCEEDED(receiver.CommandLineRunner()->GetArgument(i, arg))); + EXPECT_STREQ(arg.get(), kExpectedArgsW[i]); + } + EXPECT_EQ(receiver.CommandLineRunner()->GetWorkingDirectory( + getter_AddRefs(workingDir)), + NS_ERROR_NOT_INITIALIZED); + + receiver.Parse(v1.CopyData()); + EXPECT_NS_SUCCEEDED(receiver.CommandLineRunner()->GetLength(&len)); + EXPECT_EQ(static_cast<size_t>(len), ArrayLength(kExpectedArgsW)); + for (size_t i = 0; i < ArrayLength(kExpectedArgsW); ++i) { + EXPECT_TRUE( + NS_SUCCEEDED(receiver.CommandLineRunner()->GetArgument(i, arg))); + EXPECT_STREQ(arg.get(), kExpectedArgsW[i]); + } + EXPECT_TRUE(NS_SUCCEEDED(receiver.CommandLineRunner()->GetWorkingDirectory( + getter_AddRefs(workingDir)))); + EXPECT_NS_SUCCEEDED(workingDir->GetPath(arg)); + EXPECT_STREQ(arg.get(), workingDirW); + + receiver.Parse(v2.CopyData()); + EXPECT_NS_SUCCEEDED(receiver.CommandLineRunner()->GetLength(&len)); + EXPECT_EQ(static_cast<size_t>(len), ArrayLength(kExpectedArgsW)); + for (size_t i = 0; i < ArrayLength(kExpectedArgsW); ++i) { + EXPECT_TRUE( + NS_SUCCEEDED(receiver.CommandLineRunner()->GetArgument(i, arg))); + EXPECT_STREQ(arg.get(), kExpectedArgsW[i]); + } + EXPECT_TRUE(NS_SUCCEEDED(receiver.CommandLineRunner()->GetWorkingDirectory( + getter_AddRefs(workingDir)))); + EXPECT_NS_SUCCEEDED(workingDir->GetPath(arg)); + EXPECT_STREQ(arg.get(), workingDirW); +} + +TEST(WinRemoteMessage, NonNullTerminatedBuffer) +{ + // Reserve two pages and commit the first one + const uint32_t kPageSize = 4096; + UniquePtr<void, VirtualFreeDeleter> pages( + ::VirtualAlloc(nullptr, kPageSize * 2, MEM_RESERVE, PAGE_NOACCESS)); + EXPECT_TRUE(pages); + EXPECT_TRUE( + ::VirtualAlloc(pages.get(), kPageSize, MEM_COMMIT, PAGE_READWRITE)); + + // Test strings with lengths between 0 and |kMaxBufferSize| bytes + const int kMaxBufferSize = 10; + + // Set a string just before the boundary between the two pages. + uint8_t* bufferEnd = reinterpret_cast<uint8_t*>(pages.get()) + kPageSize; + memset(bufferEnd - kMaxBufferSize, '$', kMaxBufferSize); + + nsCOMPtr<nsIFile> workingDir; + COPYDATASTRUCT copyData = {}; + for (int i = 0; i < kMaxBufferSize; ++i) { + WinRemoteMessageReceiver receiver; + + copyData.cbData = i; + copyData.lpData = bufferEnd - i; + + copyData.dwData = + static_cast<ULONG_PTR>(WinRemoteMessageVersion::CommandLineOnly); + EXPECT_NS_SUCCEEDED(receiver.Parse(©Data)); + EXPECT_EQ(receiver.CommandLineRunner()->GetWorkingDirectory( + getter_AddRefs(workingDir)), + NS_ERROR_NOT_INITIALIZED); + + copyData.dwData = static_cast<ULONG_PTR>( + WinRemoteMessageVersion::CommandLineAndWorkingDir); + EXPECT_NS_SUCCEEDED(receiver.Parse(©Data)); + EXPECT_EQ(receiver.CommandLineRunner()->GetWorkingDirectory( + getter_AddRefs(workingDir)), + NS_ERROR_NOT_INITIALIZED); + + copyData.dwData = static_cast<ULONG_PTR>( + WinRemoteMessageVersion::CommandLineAndWorkingDirInUtf16); + EXPECT_NS_SUCCEEDED(receiver.Parse(©Data)); + EXPECT_EQ(receiver.CommandLineRunner()->GetWorkingDirectory( + getter_AddRefs(workingDir)), + NS_ERROR_NOT_INITIALIZED); + } +} diff --git a/toolkit/xre/test/gtest/TestCmdLineAndEnvUtils.cpp b/toolkit/xre/test/gtest/TestCmdLineAndEnvUtils.cpp new file mode 100644 index 0000000000..a5f8a19a9c --- /dev/null +++ b/toolkit/xre/test/gtest/TestCmdLineAndEnvUtils.cpp @@ -0,0 +1,372 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 <memory> +#include <ostream> +#include <string> +#include <string_view> +#include <type_traits> +#include <vector> + +#include "gtest/gtest.h" + +#include "mozilla/CmdLineAndEnvUtils.h" + +// Insulate these test-classes from the outside world. +// +// (In particular, distinguish this `CommandLine` from the one in ipc/chromium. +// The compiler has no issues here, but the MSVC debugger gets somewhat +// confused.) +namespace testzilla { + +// Auxiliary class to simplify the declaration of test-cases for +// EnsureCommandlineSafe. +class CommandLine { + const size_t size; + std::vector<std::string> _data_8; + std::vector<std::wstring> _data_16; + // _should_ be const all the way through, but... + char const** argv_8; + wchar_t const** argv_16; + + public: + inline int argc() const { return (int)(unsigned int)(size); } + + template <typename CharT> + CharT const** argv() const; + + CommandLine(CommandLine const&) = delete; + CommandLine(CommandLine&& that) = delete; + + template <typename Container> + explicit CommandLine(Container const& container) + : size{std::size(container) + 1}, // plus one for argv[0] + argv_8(new const char*[size + 1]), // plus one again for argv[argc] + argv_16(new const wchar_t*[size + 1]) { + _data_8.reserve(size + 1); + _data_16.reserve(size + 1); + size_t pos = 0; + + const auto append = [&](const char* val) { + size_t const len = ::strlen(val); + _data_8.emplace_back(val, val + len); + _data_16.emplace_back(val, val + len); + argv_8[pos] = _data_8.back().data(); + argv_16[pos] = _data_16.back().data(); + ++pos; + }; + + append("GECKOAPP.COM"); // argv[0] may be anything + for (const char* item : container) { + append(item); + } + assert(pos == size); + + // C99 §5.1.2.2.1 states "argv[argc] shall be a null pointer", and + // `CheckArg` implicitly relies on this. + argv_8[pos] = nullptr; + argv_16[pos] = nullptr; + } + + // debug output + friend std::ostream& operator<<(std::ostream& o, CommandLine const& cl) { + for (const auto& item : cl._data_8) { + o << '"'; + for (const char c : item) { + if (c == '"') { + o << "\\\""; + } else { + o << c; + } + } + o << "\", "; + } + return o; + } +}; + +template <> +char const** CommandLine::argv<char>() const { + return argv_8; +} +template <> +wchar_t const** CommandLine::argv<wchar_t>() const { + return argv_16; +} + +enum TestCaseState : bool { + FAIL = false, + PASS = true, +}; +constexpr TestCaseState operator!(TestCaseState s) { + return TestCaseState(!bool(s)); +} + +#ifdef XP_WIN +constexpr static const TestCaseState WIN_ONLY = PASS; +#else +constexpr static const TestCaseState WIN_ONLY = FAIL; +#endif + +using NarrowTestCase = std::pair<const char*, const char*>; +constexpr std::pair<TestCaseState, NarrowTestCase> kStrMatches8[] = { + {PASS, {"", ""}}, + + {PASS, {"i", "i"}}, + {PASS, {"i", "I"}}, + + {FAIL, {"", "i"}}, + {FAIL, {"i", ""}}, + {FAIL, {"i", "j"}}, + + {PASS, {"mozilla", "mozilla"}}, + {PASS, {"mozilla", "Mozilla"}}, + {PASS, {"mozilla", "MOZILLA"}}, + {PASS, {"mozilla", "mOZILLA"}}, + {PASS, {"mozilla", "mOZIlLa"}}, + {PASS, {"mozilla", "MoZiLlA"}}, + + {FAIL, {"mozilla", ""}}, + {FAIL, {"mozilla", "mozill"}}, + {FAIL, {"mozilla", "mozillo"}}, + {FAIL, {"mozilla", "mozillam"}}, + {FAIL, {"mozilla", "mozilla-"}}, + {FAIL, {"mozilla", "-mozilla"}}, + {FAIL, {"-mozilla", "mozilla"}}, + + // numbers are permissible + {PASS, {"m0zi11a", "m0zi11a"}}, + {PASS, {"m0zi11a", "M0ZI11A"}}, + {PASS, {"m0zi11a", "m0Zi11A"}}, + + {FAIL, {"mozilla", "m0zi11a"}}, + {FAIL, {"m0zi11a", "mozilla"}}, + + // capital letters are not accepted in the left comparand + {FAIL, {"I", "i"}}, + {FAIL, {"I", "i"}}, + {FAIL, {"Mozilla", "mozilla"}}, + {FAIL, {"Mozilla", "Mozilla"}}, + + // punctuation other than `-` is rejected + {FAIL, {"*", "*"}}, + {FAIL, {"*", "word"}}, + {FAIL, {".", "."}}, + {FAIL, {".", "a"}}, + {FAIL, {"_", "_"}}, + + // spaces are rejected + {FAIL, {" ", " "}}, + {FAIL, {"two words", "two words"}}, + + // non-ASCII characters are rejected + // + // (the contents of this test case may differ depending on the source and + // execution character sets, but the result should always be failure) + {FAIL, {"à", "a"}}, + {FAIL, {"a", "à"}}, + {FAIL, {"à", "à"}}, +}; + +#ifdef XP_WIN +using WideTestCase = std::pair<const char*, const wchar_t*>; +std::pair<TestCaseState, WideTestCase> kStrMatches16[] = { + // (Turkish 'İ' may lowercase to 'i' in some locales, but + // we explicitly prevent that from being relevant) + {FAIL, {"i", L"İ"}}, + {FAIL, {"mozilla", L"ṁozilla"}}, +}; +#endif + +constexpr const char* kRequiredArgs[] = {"aleph", "beth"}; + +std::pair<TestCaseState, std::vector<const char*>> const kCommandLines[] = { + // the basic admissible forms + {PASS, {"-osint", "-aleph", "http://www.example.com/"}}, + {PASS, {"-osint", "-beth", "http://www.example.com/"}}, + + // GNU-style double-dashes are also allowed + {PASS, {"--osint", "-aleph", "http://www.example.com/"}}, + {PASS, {"--osint", "-beth", "http://www.example.com/"}}, + {PASS, {"--osint", "--aleph", "http://www.example.com/"}}, + {PASS, {"--osint", "--beth", "http://www.example.com/"}}, + + // on Windows, slashes are also permitted + {WIN_ONLY, {"-osint", "/aleph", "http://www.example.com/"}}, + {WIN_ONLY, {"--osint", "/aleph", "http://www.example.com/"}}, + // - These should pass on Windows because they're well-formed. + // - These should pass elsewhere because the first parameter is + // just a local filesystem path, not an option. + {PASS, {"/osint", "/aleph", "http://www.example.com/"}}, + {PASS, {"/osint", "-aleph", "http://www.example.com/"}}, + {PASS, {"/osint", "--aleph", "http://www.example.com/"}}, + + // the required argument's argument may not itself be a flag + {FAIL, {"-osint", "-aleph", "-anything"}}, + {FAIL, {"-osint", "-aleph", "--anything"}}, + // - On Windows systems this is a switch and should be rejected. + // - On non-Windows systems this is potentially a valid local path. + {!WIN_ONLY, {"-osint", "-aleph", "/anything"}}, + + // wrong order + {FAIL, {"-osint", "http://www.example.com/", "-aleph"}}, + {FAIL, {"-aleph", "-osint", "http://www.example.com/"}}, + {FAIL, {"-aleph", "http://www.example.com/", "-osint"}}, + {FAIL, {"http://www.example.com/", "-osint", "-aleph"}}, + {FAIL, {"http://www.example.com/", "-aleph", "-osint"}}, + + // missing arguments + {FAIL, {"-osint", "http://www.example.com/"}}, + {FAIL, {"-osint", "-aleph"}}, + {FAIL, {"-osint"}}, + + // improper arguments + {FAIL, {"-osint", "-other-argument", "http://www.example.com/"}}, + {FAIL, {"-osint", "", "http://www.example.com/"}}, + + // additional arguments + {FAIL, {"-osint", "-aleph", "http://www.example.com/", "-other-argument"}}, + {FAIL, {"-osint", "-other-argument", "-aleph", "http://www.example.com/"}}, + {FAIL, {"-osint", "-aleph", "-other-argument", "http://www.example.com/"}}, + {FAIL, {"-osint", "-aleph", "http://www.example.com/", "-a", "-b"}}, + {FAIL, {"-osint", "-aleph", "-a", "http://www.example.com/", "-b"}}, + {FAIL, {"-osint", "-a", "-aleph", "http://www.example.com/", "-b"}}, + + // multiply-occurring otherwise-licit arguments + {FAIL, {"-osint", "-aleph", "-beth", "http://www.example.com/"}}, + {FAIL, {"-osint", "-aleph", "-aleph", "http://www.example.com/"}}, + + // if -osint is not supplied, anything goes + {PASS, {}}, + {PASS, {"http://www.example.com"}}, + {PASS, {"-aleph", "http://www.example.com/"}}, + {PASS, {"-beth", "http://www.example.com/"}}, + {PASS, {"-aleph", "http://www.example.com/", "-other-argument"}}, + {PASS, {"-aleph", "-aleph", "http://www.example.com/"}}, +}; + +constexpr static char const* const kOptionalArgs[] = {"mozilla", "allizom"}; + +// Additional test cases for optional parameters. +// +// Test cases marked PASS should pass iff the above optional parameters are +// permitted. (Test cases marked FAIL should fail regardless, and are only +// grouped here for convenience and semantic coherence.) +std::pair<TestCaseState, std::vector<const char*>> kCommandLinesOpt[] = { + // one permitted optional argument + {PASS, {"-osint", "-mozilla", "-aleph", "http://www.example.com/"}}, + {PASS, {"-osint", "-allizom", "-aleph", "http://www.example.com/"}}, + + // multiple permitted optional arguments + {PASS, + {"-osint", "-mozilla", "-allizom", "-aleph", "http://www.example.com/"}}, + {PASS, + {"-osint", "-allizom", "-mozilla", "-aleph", "http://www.example.com/"}}, + + // optional arguments in the wrong place + {FAIL, {"-mozilla", "-osint", "-aleph", "http://www.example.com/"}}, + {FAIL, {"-osint", "-aleph", "-mozilla", "http://www.example.com/"}}, + {FAIL, {"-osint", "-aleph", "http://www.example.com/", "-mozilla"}}, + + // optional arguments in both right and wrong places + {FAIL, + {"-mozilla", "-osint", "-allizom", "-aleph", "http://www.example.com/"}}, + {FAIL, + {"-osint", "-allizom", "-aleph", "-mozilla", "http://www.example.com/"}}, + {FAIL, + {"-osint", "-allizom", "-aleph", "http://www.example.com/", "-mozilla"}}, +}; + +enum WithOptionalState : bool { + NoOptionalArgs = false, + WithOptionalArgs = true, +}; + +template <typename CharT> +bool TestCommandLineImpl(CommandLine const& cl, + WithOptionalState withOptional) { + int argc = cl.argc(); + // EnsureCommandlineSafe's signature isn't const-correct here for annoying + // reasons, but it is indeed non-mutating. + CharT** argv = const_cast<CharT**>(cl.argv<CharT>()); + if (withOptional) { + return mozilla::internal::EnsureCommandlineSafeImpl( + argc, argv, kRequiredArgs, kOptionalArgs); + } + return mozilla::internal::EnsureCommandlineSafeImpl(argc, argv, + kRequiredArgs); +} + +// Test that `args` produces `expectation`. On Windows, test against both +// wide-character and narrow-character implementations. +void TestCommandLine(TestCaseState expectation, CommandLine const& cl, + WithOptionalState withOptional) { + EXPECT_EQ(TestCommandLineImpl<char>(cl, withOptional), expectation) + << "cl is: " << cl << std::endl + << "withOptional is: " << bool(withOptional); +#ifdef XP_WIN + EXPECT_EQ(TestCommandLineImpl<wchar_t>(cl, withOptional), expectation) + << "cl is: " << cl << std::endl + << "withOptional is: " << bool(withOptional); +#endif +} +} // namespace testzilla + +/*********************** + * Test cases + */ + +TEST(CmdLineAndEnvUtils, strimatch) +{ + using namespace testzilla; + using mozilla::strimatch; + for (auto const& [result, data] : kStrMatches8) { + auto const& [left, right] = data; + EXPECT_EQ(strimatch(left, right), result) + << '<' << left << "> !~ <" << right << '>'; + +#ifdef XP_WIN + wchar_t right_wide[200]; + ::mbstowcs(right_wide, right, 200); + EXPECT_EQ(strimatch(left, right_wide), result) + << '<' << left << "> !~ L<" << right << '>'; +#endif + } + +#ifdef XP_WIN + for (auto const& [result, data] : kStrMatches16) { + auto const& [left, right] = data; + EXPECT_EQ(strimatch(left, right), result) + << '<' << left << "> !~ L<" << right << '>'; + } +#endif +} + +TEST(CmdLineAndEnvUtils, ensureSafe) +{ + using namespace testzilla; + for (auto const& [result, data] : kCommandLines) { + CommandLine const cl(data); + TestCommandLine(result, cl, NoOptionalArgs); + } + for (auto const& [_unused, data] : kCommandLinesOpt) { + MOZ_UNUSED(_unused); // silence gcc + CommandLine const cl(data); + TestCommandLine(FAIL, cl, NoOptionalArgs); + } +} + +TEST(CmdLineAndEnvUtils, ensureSafeWithOptional) +{ + using namespace testzilla; + for (auto const& [result, data] : kCommandLines) { + CommandLine const cl(data); + TestCommandLine(result, cl, WithOptionalArgs); + } + for (auto const& [result, data] : kCommandLinesOpt) { + CommandLine const cl(data); + TestCommandLine(result, cl, WithOptionalArgs); + } +} diff --git a/toolkit/xre/test/gtest/TestCompatVersionCompare.cpp b/toolkit/xre/test/gtest/TestCompatVersionCompare.cpp new file mode 100644 index 0000000000..bce64aacd5 --- /dev/null +++ b/toolkit/xre/test/gtest/TestCompatVersionCompare.cpp @@ -0,0 +1,53 @@ +/* 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 "nsAppRunner.h" +#include "nsString.h" + +void CheckCompatVersionCompare(const nsCString& aOldCompatVersion, + const nsCString& aNewCompatVersion, + bool aExpectedSame, bool aExpectedDowngrade) { + printf("Comparing '%s' to '%s'.\n", aOldCompatVersion.get(), + aNewCompatVersion.get()); + + int32_t result = CompareCompatVersions(aOldCompatVersion, aNewCompatVersion); + + ASSERT_EQ(aExpectedSame, result == 0) + << "Version sameness check should match."; + ASSERT_EQ(aExpectedDowngrade, result > 0) + << "Version downgrade check should match."; +} + +void CheckExpectedResult(const char* aOldAppVersion, const char* aNewAppVersion, + bool aExpectedSame, bool aExpectedDowngrade) { + nsCString oldCompatVersion; + BuildCompatVersion(aOldAppVersion, "", "", oldCompatVersion); + + nsCString newCompatVersion; + BuildCompatVersion(aNewAppVersion, "", "", newCompatVersion); + + CheckCompatVersionCompare(oldCompatVersion, newCompatVersion, aExpectedSame, + aExpectedDowngrade); +} + +TEST(CompatVersionCompare, CompareVersionChange) +{ + // Identical + CheckExpectedResult("67.0", "67.0", true, false); + + // Version changes + CheckExpectedResult("67.0", "68.0", false, false); + CheckExpectedResult("68.0", "67.0", false, true); + CheckExpectedResult("67.0", "67.0.1", true, false); + CheckExpectedResult("67.0.1", "67.0", true, false); + CheckExpectedResult("67.0.1", "67.0.1", true, false); + CheckExpectedResult("67.0.1", "67.0.2", true, false); + CheckExpectedResult("67.0.2", "67.0.1", true, false); + + // Check that if the last run was safe mode then we consider this an upgrade. + CheckCompatVersionCompare( + "Safe Mode"_ns, "67.0.1_20000000000000/20000000000000"_ns, false, false); +} diff --git a/toolkit/xre/test/gtest/TestGeckoArgs.cpp b/toolkit/xre/test/gtest/TestGeckoArgs.cpp new file mode 100644 index 0000000000..42c7a41b2b --- /dev/null +++ b/toolkit/xre/test/gtest/TestGeckoArgs.cpp @@ -0,0 +1,179 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "gtest/gtest.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/GeckoArgs.h" + +using namespace mozilla; +using namespace mozilla::geckoargs; + +static CommandLineArg<const char*> kCharParam{"-charParam", "charparam"}; +static CommandLineArg<uint64_t> kUint64Param{"-Uint64Param", "uint64param"}; +static CommandLineArg<bool> kFlag{"-Flag", "flag"}; + +template <size_t N> +bool CheckArgv(char** aArgv, const char* const (&aExpected)[N]) { + for (size_t i = 0; i < N; ++i) { + if (aArgv[i] == nullptr && aExpected[i] == nullptr) { + return true; + } + if (aArgv[i] == nullptr || aExpected[i] == nullptr) { + return false; + } + if (strcmp(aArgv[i], aExpected[i]) != 0) { + return false; + } + } + return true; +} + +char kFirefox[] = "$HOME/bin/firefox/firefox-bin"; + +TEST(GeckoArgs, const_char_ptr) +{ + char kCharParamStr[] = "-charParam"; + char kCharParamValue[] = "paramValue"; + + { + char* argv[] = {kFirefox, kCharParamStr, kCharParamValue, nullptr}; + int argc = ArrayLength(argv); + EXPECT_EQ(argc, 4); + + Maybe<const char*> charParam = kCharParam.Get(argc, argv); + EXPECT_TRUE(charParam.isSome()); + EXPECT_EQ(*charParam, kCharParamValue); + + const char* const expArgv[] = {kFirefox, nullptr}; + EXPECT_TRUE(CheckArgv(argv, expArgv)); + } + { + char kBlahBlah[] = "-blahblah"; + char* argv[] = {kFirefox, kCharParamStr, kBlahBlah, nullptr}; + int argc = ArrayLength(argv); + EXPECT_EQ(argc, 4); + + Maybe<const char*> charParam = kCharParam.Get(argc, argv); + EXPECT_TRUE(charParam.isNothing()); + + const char* const expArgv[] = {kFirefox, kBlahBlah, nullptr}; + EXPECT_TRUE(CheckArgv(argv, expArgv)); + } + { + std::vector<std::string> extraArgs; + EXPECT_EQ(extraArgs.size(), 0U); + kCharParam.Put("ParamValue", extraArgs); + EXPECT_EQ(extraArgs.size(), 2U); + EXPECT_EQ(extraArgs[0], "-charParam"); + EXPECT_EQ(extraArgs[1], "ParamValue"); + } + { EXPECT_EQ(kCharParam.Name(), "-charParam"); } +} + +TEST(GeckoArgs, uint64) +{ + char kUint64ParamStr[] = "-Uint64Param"; + + { + char* argv[] = {kFirefox, kUint64ParamStr, nullptr}; + int argc = ArrayLength(argv); + EXPECT_EQ(argc, 3); + + Maybe<uint64_t> uint64Param = kUint64Param.Get(argc, argv); + EXPECT_TRUE(uint64Param.isNothing()); + + const char* const expArgv[] = {kFirefox, nullptr}; + EXPECT_TRUE(CheckArgv(argv, expArgv)); + } + { + char* argv[] = {kFirefox, nullptr}; + int argc = ArrayLength(argv); + EXPECT_EQ(argc, 2); + + Maybe<uint64_t> uint64Param = kUint64Param.Get(argc, argv); + EXPECT_TRUE(uint64Param.isNothing()); + + const char* const expArgv[] = {kFirefox, nullptr}; + EXPECT_TRUE(CheckArgv(argv, expArgv)); + } + { + char kUint64ParamValue[] = "42"; + char* argv[] = {kFirefox, kUint64ParamStr, kUint64ParamValue, nullptr}; + int argc = ArrayLength(argv); + EXPECT_EQ(argc, 4); + + Maybe<uint64_t> uint64Param = kUint64Param.Get(argc, argv); + EXPECT_TRUE(uint64Param.isSome()); + EXPECT_EQ(*uint64Param, 42U); + + const char* const expArgv[] = {kFirefox, nullptr}; + EXPECT_TRUE(CheckArgv(argv, expArgv)); + } + { + char kUint64ParamValue[] = "aa"; + char* argv[] = {kFirefox, kUint64ParamStr, kUint64ParamValue, nullptr}; + int argc = ArrayLength(argv); + EXPECT_EQ(argc, 4); + + Maybe<uint64_t> uint64Param = kUint64Param.Get(argc, argv); + EXPECT_TRUE(uint64Param.isNothing()); + + const char* const expArgv[] = {kFirefox, nullptr}; + EXPECT_TRUE(CheckArgv(argv, expArgv)); + } + { + std::vector<std::string> extraArgs; + EXPECT_EQ(extraArgs.size(), 0U); + kUint64Param.Put(1234, extraArgs); + EXPECT_EQ(extraArgs.size(), 2U); + EXPECT_EQ(extraArgs[0], "-Uint64Param"); + EXPECT_EQ(extraArgs[1], "1234"); + } + { EXPECT_EQ(kUint64Param.Name(), "-Uint64Param"); } +} + +TEST(GeckoArgs, bool) +{ + char kFlagStr[] = "-Flag"; + + { + char* argv[] = {kFirefox, kFlagStr, nullptr}; + int argc = ArrayLength(argv); + EXPECT_EQ(argc, 3); + + Maybe<bool> Flag = kFlag.Get(argc, argv); + EXPECT_TRUE(Flag.isSome()); + EXPECT_TRUE(*Flag); + + const char* const expArgv[] = {kFirefox, nullptr}; + EXPECT_TRUE(CheckArgv(argv, expArgv)); + } + { + char* argv[] = {kFirefox, nullptr}; + int argc = ArrayLength(argv); + EXPECT_EQ(argc, 2); + + Maybe<bool> Flag = kFlag.Get(argc, argv); + EXPECT_TRUE(Flag.isNothing()); + + const char* const expArgv[] = {kFirefox, nullptr}; + EXPECT_TRUE(CheckArgv(argv, expArgv)); + } + { + std::vector<std::string> extraArgs; + EXPECT_EQ(extraArgs.size(), 0U); + kFlag.Put(true, extraArgs); + EXPECT_EQ(extraArgs.size(), 1U); + EXPECT_EQ(extraArgs[0], "-Flag"); + } + { + std::vector<std::string> extraArgs; + EXPECT_EQ(extraArgs.size(), 0U); + kFlag.Put(false, extraArgs); + EXPECT_EQ(extraArgs.size(), 0U); + } + { EXPECT_EQ(kFlag.Name(), "-Flag"); } +} diff --git a/toolkit/xre/test/gtest/moz.build b/toolkit/xre/test/gtest/moz.build new file mode 100644 index 0000000000..bec881fa72 --- /dev/null +++ b/toolkit/xre/test/gtest/moz.build @@ -0,0 +1,24 @@ +# -*- Mode: python; c-basic-offset: 4; 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/. + +Library("xretest") + +UNIFIED_SOURCES = [ + "TestCmdLineAndEnvUtils.cpp", + "TestCompatVersionCompare.cpp", + "TestGeckoArgs.cpp", +] + +LOCAL_INCLUDES += [ + "/toolkit/components/remote", +] + +if CONFIG["OS_TARGET"] == "WINNT": + UNIFIED_SOURCES += [ + "TestAssembleCommandLineWin.cpp", + ] + +FINAL_LIBRARY = "xul-gtest" diff --git a/toolkit/xre/test/marionette/gen_win32k_tests.py b/toolkit/xre/test/marionette/gen_win32k_tests.py new file mode 100644 index 0000000000..e0a39776db --- /dev/null +++ b/toolkit/xre/test/marionette/gen_win32k_tests.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 + +import re + +RE_DEFAULT = re.compile(r"\[D=([TF])\] ") +RE_TRANSITION = re.compile(r"([a-zA-Z0-9 \[\]=#_-]+) *->(.*)") +RE_ASSERTION = re.compile( + r"\[A S=([a-zA-Z01_]+) SS=([a-zA-Z01_]+) ES=([a-zA-Z01_]+) P=([a-zA-Z_]+) ESP=([a-zA-Z_]+)\]" +) +RE_ASSERTION_SHORTHAND = re.compile(r"\[A#([0-9T]+)\]") + +# ====================================================================== +# ====================================================================== + +testnum = 1 + + +def start_test(line): + global testnum + output.write( + """ + def test_{0}(self): + # {1}...\n""".format( + testnum, line[0:80] + ) + ) + testnum += 1 + + +def set_default(d): + output.write( + """ + if self.default_is is not {0}: + return\n""".format( + "True" if d == "T" else "False" + ) + ) + + +def enroll(e): + if e.endswith("-C"): + e = e[:-2] + output.write("\n # Re-set enrollment pref, like Normandy would do\n") + output.write( + " self.set_enrollment_status(ExperimentStatus.ENROLLED_{0})\n".format( + e.upper() + ) + ) + + +def set_pref(enabled): + output.write( + "\n self.marionette.set_pref(Prefs.WIN32K, {0})\n".format(str(enabled)) + ) + + +def set_e10s(enable): + if enable: + output.write( + """ + app_version = self.execute_script("return Services.appinfo.version") + self.restart(env={ENV_DISABLE_E10S: app_version}) + self.set_env(ENV_DISABLE_E10S, "null")\n""" + ) + else: + raise Exception("Not implemented") + + +def set_header(enable): + if enable: + output.write( + """ + self.restart(env={ENV_DISABLE_WIN32K: "1"})\n""" + ) + else: + output.write( + """ + self.set_env(ENV_DISABLE_WIN32K, "")\n""" + ) + + +def set_bad_requirements(enabled): + output.write( + """ + self.marionette.set_pref(Prefs.WEBGL, {0})\n""".format( + "False" if enabled else "True" + ) + ) + + +def restart(): + output.write("\n self.restart()\n") + + +def print_assertion(assertion): + if not assertion: + return + output.write( + """ + self.check_win32k_status( + status=ContentWin32kLockdownState.{0}, + sessionStatus=ContentWin32kLockdownState.{1}, + experimentStatus=ExperimentStatus.{2}, + pref={3}, + enrollmentStatusPref=ExperimentStatus.{4}, + )\n""".format( + *assertion + ) + ) + + +# ====================================================================== +# ====================================================================== + +TESTS = open("win32k_tests.txt", "r").readlines() + +output = open("test_win32k_enrollment.py", "w", newline="\n") +header = open("test_win32k_enrollment.template.py", "r") +for l in header: + output.write(l) + +mappings = {} +for line in TESTS: + line = line.strip() + if not line: + continue + + if line.startswith("#"): + continue + elif line.startswith("> "): + line = line[2:] + key, value = line.split(":") + mappings[key.strip()] = value.strip() + continue + elif line.startswith("-"): + line = line[1:].strip() + elif line.startswith("+"): + line = line[1:].strip() + import pdb + + pdb.set_trace() + else: + raise Exception("unknown line type: " + line) + + # We can't handle Safe Mode right now + if "Safe Mode" in line: + continue + + # If we have no assertions defined, skip the test entirely + if "[A" not in line: + continue + + if not RE_DEFAULT.match(line): + raise Exception("'{0}' does not match the default regex".format(line)) + default = RE_DEFAULT.search(line).groups(1)[0] + + start_test(line) + + set_default(default) + + line = line[6:] + + while line: + # this is a horrible hack for the line ending to avoid making the + # regex more complicated and having to fix it + if not line.endswith(" ->"): + line += " ->" + + groups = RE_TRANSITION.search(line).groups() + start = groups[0].strip() + end = groups[1].strip() + + if RE_ASSERTION.search(start): + assertion = RE_ASSERTION.search(start).groups() + start = start[0 : start.index("[")].strip() + elif RE_ASSERTION_SHORTHAND.search(start): + key = RE_ASSERTION_SHORTHAND.search(start).groups()[0] + assertion = RE_ASSERTION.search(mappings[key]).groups() + start = start[0 : start.index("[")].strip() + else: + assertion = "" + + if start == "Nothing": + pass + elif start.startswith("Enrolled "): + enroll(start[9:]) + elif start == "E10S": + set_e10s(True) + elif start == "Header-On": + set_header(True) + elif start == "Header-Off": + set_header(False) + elif start == "Bad Requirements": + set_bad_requirements(True) + elif start == "Restart": + restart() + elif start == "On": + set_pref(True) + elif start == "Off": + set_pref(False) + else: + raise Exception("Unknown Action: " + start) + + print_assertion(assertion) + + line = end.strip() + + if RE_ASSERTION.match(line): + print_assertion(RE_ASSERTION.search(line).groups()) + elif RE_ASSERTION_SHORTHAND.search(line): + key = RE_ASSERTION_SHORTHAND.search(line).groups()[0] + print_assertion(RE_ASSERTION.search(mappings[key]).groups()) diff --git a/toolkit/xre/test/marionette/marionette.ini b/toolkit/xre/test/marionette/marionette.ini new file mode 100644 index 0000000000..bb49d6c5de --- /dev/null +++ b/toolkit/xre/test/marionette/marionette.ini @@ -0,0 +1,6 @@ +[test_fission_autostart.py] +[test_win32k_enrollment.py] +skip-if = + os != 'win' + ccov # Bug 1757102 +[test_exitcode.py] diff --git a/toolkit/xre/test/marionette/test_exitcode.py b/toolkit/xre/test/marionette/test_exitcode.py new file mode 100644 index 0000000000..4ce7306122 --- /dev/null +++ b/toolkit/xre/test/marionette/test_exitcode.py @@ -0,0 +1,31 @@ +from marionette_harness import MarionetteTestCase + + +class TestFissionAutostart(MarionetteTestCase): + def test_normal_exit(self): + self.marionette.set_context(self.marionette.CONTEXT_CHROME) + + def call_quit(): + self.marionette.execute_script( + """ + Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit); + """, + sandbox="system", + ) + + self.marionette.quit(callback=call_quit) + self.assertEqual(self.marionette.instance.runner.returncode, 0) + + def test_exit_code(self): + self.marionette.set_context(self.marionette.CONTEXT_CHROME) + + def call_quit(): + self.marionette.execute_script( + """ + Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit, 5); + """, + sandbox="system", + ) + + self.marionette.quit(callback=call_quit) + self.assertEqual(self.marionette.instance.runner.returncode, 5) diff --git a/toolkit/xre/test/marionette/test_fission_autostart.py b/toolkit/xre/test/marionette/test_fission_autostart.py new file mode 100644 index 0000000000..e34087aca9 --- /dev/null +++ b/toolkit/xre/test/marionette/test_fission_autostart.py @@ -0,0 +1,247 @@ +from contextlib import contextmanager + +from marionette_harness import MarionetteTestCase + + +class ExperimentStatus: + UNENROLLED = 0 + ENROLLED_CONTROL = 1 + ENROLLED_TREATMENT = 2 + DISQUALIFIED = 3 + + +class Prefs: + FISSION_AUTOSTART = "fission.autostart" + FISSION_AUTOSTART_SESSION = "fission.autostart.session" + + +ENV_ENABLE_FISSION = "MOZ_FORCE_ENABLE_FISSION" +ENV_DISABLE_FISSION = "MOZ_FORCE_DISABLE_FISSION" +ENV_DISABLE_E10S = "MOZ_FORCE_DISABLE_E10S" + + +DECISION_STATUS = { + "experimentControl": 1, + "experimentTreatment": 2, + "disabledByE10sEnv": 3, + "enabledByEnv": 4, + "disabledByEnv": 5, + "enabledByDefault": 7, + "disabledByDefault": 8, + "enabledByUserPref": 9, + "disabledByUserPref": 10, + "disabledByE10sOther": 11, +} + + +class TestFissionAutostart(MarionetteTestCase): + SANDBOX_NAME = "fission-autostart" + + def execute_script(self, code, *args, **kwargs): + with self.marionette.using_context(self.marionette.CONTEXT_CHROME): + return self.marionette.execute_script( + code, new_sandbox=False, sandbox=self.SANDBOX_NAME, *args, **kwargs + ) + + def get_fission_status(self): + return self.execute_script( + r""" + let win = Services.wm.getMostRecentWindow("navigator:browser"); + return { + fissionAutostart: Services.appinfo.fissionAutostart, + decisionStatus: Services.appinfo.fissionDecisionStatus, + decisionStatusString: Services.appinfo.fissionDecisionStatusString, + useRemoteSubframes: win.docShell.nsILoadContext.useRemoteSubframes, + fissionAutostartSession: Services.prefs.getBoolPref("fission.autostart.session"), + dynamicFissionAutostart: Services.prefs.getBoolPref("fission.autostart"), + }; + """ + ) + + def check_fission_status(self, enabled, decision, dynamic=None): + if dynamic is None: + dynamic = enabled + + expected = { + "fissionAutostart": enabled, + "decisionStatus": DECISION_STATUS[decision], + "decisionStatusString": decision, + "useRemoteSubframes": enabled, + "fissionAutostartSession": enabled, + "dynamicFissionAutostart": dynamic, + } + + status = self.get_fission_status() + + for prop, value in expected.items(): + self.assertEqual( + status[prop], + value, + "%s should have the value `%r`, but has `%r`" + % (prop, value, status[prop]), + ) + + def set_env(self, env, value): + self.execute_script( + "env.set(arguments[0], arguments[1]);", script_args=(env, value) + ) + + def get_env(self, env): + return self.execute_script("return env.get(arguments[0]);", script_args=(env,)) + + def restart(self, prefs=None, env=None): + if prefs: + self.marionette.set_prefs(prefs) + + if env: + for name, value in env.items(): + self.set_env(name, value) + + self.marionette.restart(in_app=True, clean=False) + self.setUpSession() + + # Sanity check our environment. + if prefs: + for key, val in prefs.items(): + if val is not None: + self.assertEqual(self.marionette.get_pref(key), val) + if env: + for key, val in env.items(): + self.assertEqual(self.get_env(key), val or "") + + def setUpSession(self): + self.marionette.set_context(self.marionette.CONTEXT_CHROME) + + self.execute_script( + r""" + // We're running in a function, in a sandbox, that inherits from an + // X-ray wrapped window. Anything we want to be globally available + // needs to be defined on that window. + window.env = Services.env; + """ + ) + + @contextmanager + def full_restart(self): + profile = self.marionette.instance.profile + try: + self.marionette.quit() + yield profile + finally: + self.marionette.start_session() + self.setUpSession() + + def setUp(self): + super(TestFissionAutostart, self).setUp() + + # If we have configured marionette to require a particular value for + # `fission.autostart`, remove it as a forced pref until `tearDown`, and + # perform a clean restart, so we run this test without the pref + # pre-configured. + self.fissionRequired = None + if Prefs.FISSION_AUTOSTART in self.marionette.instance.required_prefs: + self.fissionRequired = self.marionette.instance.required_prefs[ + Prefs.FISSION_AUTOSTART + ] + del self.marionette.instance.required_prefs[Prefs.FISSION_AUTOSTART] + self.marionette.restart(in_app=False, clean=True) + + self.setUpSession() + + # Fission status must start out with `enabledByDefault` + self.check_fission_status( + enabled=True, + decision="enabledByDefault", + ) + + def tearDown(self): + if self.fissionRequired is not None: + self.marionette.instance.required_prefs[ + Prefs.FISSION_AUTOSTART + ] = self.fissionRequired + self.marionette.restart(in_app=False, clean=True) + + super(TestFissionAutostart, self).tearDown() + + def test_runtime_changes(self): + """Tests that changes to preferences during runtime do not have any + effect on the current session.""" + + self.check_fission_status( + enabled=True, + decision="enabledByDefault", + ) + + self.restart(prefs={Prefs.FISSION_AUTOSTART: False}) + + self.check_fission_status( + enabled=False, + decision="disabledByUserPref", + ) + + self.marionette.set_pref(Prefs.FISSION_AUTOSTART, True) + self.check_fission_status( + enabled=False, + decision="disabledByUserPref", + dynamic=True, + ) + + self.marionette.clear_pref(Prefs.FISSION_AUTOSTART) + self.check_fission_status( + enabled=False, + decision="disabledByUserPref", + dynamic=True, + ) + + self.restart() + self.check_fission_status( + enabled=True, + decision="enabledByDefault", + ) + + def test_fission_precedence(self): + self.check_fission_status( + enabled=True, + decision="enabledByDefault", + ) + + self.restart( + prefs={Prefs.FISSION_AUTOSTART: False}, env={ENV_ENABLE_FISSION: "1"} + ) + self.check_fission_status( + enabled=True, + decision="enabledByEnv", + dynamic=False, + ) + + self.restart( + prefs={Prefs.FISSION_AUTOSTART: True}, + env={ENV_DISABLE_FISSION: "1", ENV_ENABLE_FISSION: ""}, + ) + self.check_fission_status( + enabled=False, + decision="disabledByEnv", + dynamic=True, + ) + + self.restart( + prefs={Prefs.FISSION_AUTOSTART: False}, env={ENV_DISABLE_FISSION: ""} + ) + self.check_fission_status( + enabled=False, + decision="disabledByUserPref", + ) + + self.restart(prefs={Prefs.FISSION_AUTOSTART: None}) + self.check_fission_status( + enabled=True, + decision="enabledByDefault", + ) + + app_version = self.execute_script("return Services.appinfo.version") + self.restart(env={ENV_DISABLE_E10S: app_version}) + self.check_fission_status( + enabled=False, + decision="disabledByE10sEnv", + dynamic=True, + ) diff --git a/toolkit/xre/test/marionette/test_win32k_enrollment.py b/toolkit/xre/test/marionette/test_win32k_enrollment.py new file mode 100644 index 0000000000..d09331319b --- /dev/null +++ b/toolkit/xre/test/marionette/test_win32k_enrollment.py @@ -0,0 +1,2506 @@ +from contextlib import contextmanager + +from marionette_harness import MarionetteTestCase + + +class ExperimentStatus: + UNENROLLED = 0 + ENROLLED_CONTROL = 1 + ENROLLED_TREATMENT = 2 + DISQUALIFIED = 3 + + +class ContentWin32kLockdownState: + LockdownEnabled = 1 + MissingWebRender = 2 + OperatingSystemNotSupported = 3 + PrefNotSet = 4 + MissingRemoteWebGL = 5 + MissingNonNativeTheming = 6 + DisabledByEnvVar = 7 + DisabledBySafeMode = 8 + DisabledByE10S = 9 + DisabledByUserPref = 10 + EnabledByUserPref = 11 + DisabledByControlGroup = 12 + EnabledByTreatmentGroup = 13 + DisabledByDefault = 14 + EnabledByDefault = 15 + + +class Prefs: + ENROLLMENT_STATUS = "security.sandbox.content.win32k-experiment.enrollmentStatus" + STARTUP_ENROLLMENT_STATUS = ( + "security.sandbox.content.win32k-experiment.startupEnrollmentStatus" + ) + WIN32K = "security.sandbox.content.win32k-disable" + WEBGL = "webgl.out-of-process" + + +ENV_DISABLE_WIN32K = "MOZ_ENABLE_WIN32K" +ENV_DISABLE_E10S = "MOZ_FORCE_DISABLE_E10S" + + +class TestWin32kAutostart(MarionetteTestCase): + SANDBOX_NAME = "win32k-autostart" + + def execute_script(self, code, *args, **kwargs): + with self.marionette.using_context(self.marionette.CONTEXT_CHROME): + return self.marionette.execute_script( + code, new_sandbox=False, sandbox=self.SANDBOX_NAME, *args, **kwargs + ) + + def get_win32k_status(self): + return self.execute_script( + r""" + let win = Services.wm.getMostRecentWindow("navigator:browser"); + let ses = "security.sandbox.content.win32k-experiment.startupEnrollmentStatus"; + return { + win32kSessionStatus: Services.appinfo.win32kSessionStatus, + win32kStatus: Services.appinfo.win32kLiveStatusTestingOnly, + win32kExperimentStatus: Services.appinfo.win32kExperimentStatus, + win32kPref: Services.prefs.getBoolPref("security.sandbox.content.win32k-disable"), + win32kStartupEnrollmentStatusPref: Services.prefs.getIntPref(ses), + }; + """ + ) + + def check_win32k_status( + self, status, sessionStatus, experimentStatus, pref, enrollmentStatusPref + ): + # We CANNOT check win32kEnrollmentStatusPref after a restart because we only set this + # pref on the default branch, and it goes away after a restart, so we only check + # the startupEnrollmentStatusPref + expected = { + "win32kSessionStatus": sessionStatus, + "win32kStatus": status, + "win32kExperimentStatus": experimentStatus, + "win32kPref": pref, + "win32kStartupEnrollmentStatusPref": enrollmentStatusPref, + } + + status = self.get_win32k_status() + + for prop, value in expected.items(): + self.assertEqual( + status[prop], + value, + "%s should have the value `%r`, but has `%r`" + % (prop, value, status[prop]), + ) + + def set_env(self, env, value): + self.execute_script( + "env.set(arguments[0], arguments[1]);", script_args=(env, value) + ) + + def get_env(self, env): + return self.execute_script("return env.get(arguments[0]);", script_args=(env,)) + + def set_enrollment_status(self, status): + self.marionette.set_pref(Prefs.ENROLLMENT_STATUS, status, default_branch=True) + + updated_status = self.marionette.get_pref(Prefs.ENROLLMENT_STATUS) + self.assertTrue( + status == updated_status or updated_status == ExperimentStatus.DISQUALIFIED + ) + startup_status = self.marionette.get_pref(Prefs.STARTUP_ENROLLMENT_STATUS) + self.assertEqual( + startup_status, + updated_status, + "Startup enrollment status (%r) should match " + "session status (%r)" % (startup_status, updated_status), + ) + + def restart(self, prefs=None, env=None): + if prefs: + self.marionette.set_prefs(prefs) + + if env: + for name, value in env.items(): + self.set_env(name, value) + + self.marionette.restart(in_app=True, clean=False) + self.setUpSession() + + # Sanity check our environment. + if prefs: + for key, val in prefs.items(): + if val is not None: + self.assertEqual(self.marionette.get_pref(key), val) + if env: + for key, val in env.items(): + self.assertEqual(self.get_env(key), val or "") + + def setUpSession(self): + self.marionette.set_context(self.marionette.CONTEXT_CHROME) + + self.execute_script( + r""" + // We're running in a function, in a sandbox, that inherits from an + // X-ray wrapped window. Anything we want to be globally available + // needs to be defined on that window. + window.env = Services.env; + """ + ) + + @contextmanager + def full_restart(self): + profile = self.marionette.instance.profile + try: + self.marionette.quit() + yield profile + finally: + self.marionette.start_session() + self.setUpSession() + + def setUp(self): + super(TestWin32kAutostart, self).setUp() + + # If we have configured marionette to require a particular value for + # `win32k.autostart`, remove it as a forced pref until `tearDown`, and + # perform a clean restart, so we run this test without the pref + # pre-configured. + self.win32kRequired = None + if Prefs.WIN32K in self.marionette.instance.required_prefs: + self.win32kRequired = self.marionette.instance.required_prefs[Prefs.WIN32K] + del self.marionette.instance.required_prefs[Prefs.WIN32K] + self.marionette.restart(in_app=False, clean=True) + + self.setUpSession() + + # Marionette doesn't let you set preferences on the default branch before startup + # so we can't test the default=False and default=True scenarios in one test + # What we can do is generate all the tests, and then only run the runs for which + # the default is. (And run the other ones locally to make sure they work before + # we land it.) + prefJS = 'return Services.prefs.getBoolPref("security.sandbox.content.win32k-disable");' + self.default_is = self.execute_script(prefJS) + + if self.default_is is False: + # Win32k status must start out with `disabledByDefault` + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + else: + # Win32k status must start out with `enabledByDefault` + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def tearDown(self): + if self.win32kRequired is not None: + self.marionette.instance.required_prefs[Prefs.WIN32K] = self.win32kRequired + self.marionette.restart(in_app=False, clean=True) + + super(TestWin32kAutostart, self).tearDown() + + def test_1(self): + # [D=F] Nothing [A#1] -> Enrolled Control [A S=DisabledByDefault SS=DisabledByDefa... + + if self.default_is is not False: + return + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + def test_2(self): + # [D=F] Nothing [A#1] -> Enrolled Treatment [A S=DisabledByDefault SS=DisabledByDe... + + if self.default_is is not False: + return + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + def test_3(self): + # [D=F] Nothing [A#1] -> On [A S=EnabledByUserPref SS=DisabledByDefault ES=UNENROL... + + if self.default_is is not False: + return + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByUserPref, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_4(self): + # [D=F] Nothing [A#1] -> Off [A#1] -> Restart [A#1]... + + if self.default_is is not False: + return + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.marionette.set_pref(Prefs.WIN32K, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_5(self): + # [D=F] Nothing [A#1] -> On -> Bad Requirements [A S=MissingRemoteWebGL SS=Disable... + + if self.default_is is not False: + return + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.marionette.set_pref(Prefs.WEBGL, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_6(self): + # [D=F] Nothing [A#1] -> On -> E10S [A S=DisabledByE10S SS=DisabledByE10S ES=UNENR... + + if self.default_is is not False: + return + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.marionette.set_pref(Prefs.WIN32K, True) + + app_version = self.execute_script("return Services.appinfo.version") + self.restart(env={ENV_DISABLE_E10S: app_version}) + self.set_env(ENV_DISABLE_E10S, "null") + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByE10S, + sessionStatus=ContentWin32kLockdownState.DisabledByE10S, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_7(self): + # [D=F] Nothing [A#1] -> On -> Header-On [A S=DisabledByEnvVar SS=DisabledByEnvVar... + + if self.default_is is not False: + return + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.restart(env={ENV_DISABLE_WIN32K: "1"}) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByEnvVar, + sessionStatus=ContentWin32kLockdownState.DisabledByEnvVar, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.set_env(ENV_DISABLE_WIN32K, "") + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.DisabledByEnvVar, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_8(self): + # [D=T] Nothing [A#1T] -> Enrolled Control [A S=EnabledByDefault SS=EnabledByDefau... + + if self.default_is is not True: + return + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + def test_9(self): + # [D=T] Nothing [A#1T] -> Enrolled Treatment [A S=EnabledByDefault SS=EnabledByDef... + + if self.default_is is not True: + return + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + def test_10(self): + # [D=T] Nothing [A#1T] -> On [A S=EnabledByDefault SS=EnabledByDefault ES=UNENROLL... + + if self.default_is is not True: + return + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_11(self): + # [D=T] Nothing [A#1T] -> Off [A S=DisabledByUserPref SS=EnabledByDefault ES=UNENR... + + if self.default_is is not True: + return + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.marionette.set_pref(Prefs.WIN32K, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByUserPref, + sessionStatus=ContentWin32kLockdownState.DisabledByUserPref, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_12(self): + # [D=T] Nothing [A#1T] -> On -> Bad Requirements [A S=MissingRemoteWebGL SS=Enable... + + if self.default_is is not True: + return + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.marionette.set_pref(Prefs.WEBGL, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_13(self): + # [D=T] Nothing [A#1T] -> On -> E10S [A S=DisabledByE10S SS=DisabledByE10S ES=UNEN... + + if self.default_is is not True: + return + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.marionette.set_pref(Prefs.WIN32K, True) + + app_version = self.execute_script("return Services.appinfo.version") + self.restart(env={ENV_DISABLE_E10S: app_version}) + self.set_env(ENV_DISABLE_E10S, "null") + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByE10S, + sessionStatus=ContentWin32kLockdownState.DisabledByE10S, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_14(self): + # [D=T] Nothing [A#1T] -> On -> Header-On [A S=DisabledByEnvVar SS=DisabledByEnvVa... + + if self.default_is is not True: + return + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.restart(env={ENV_DISABLE_WIN32K: "1"}) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByEnvVar, + sessionStatus=ContentWin32kLockdownState.DisabledByEnvVar, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.set_env(ENV_DISABLE_WIN32K, "") + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByEnvVar, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_15(self): + # [D=F] On [A#3] -> Restart [A#4] -> Enrolled Control [A S=EnabledByUserPref SS=En... + + if self.default_is is not False: + return + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByUserPref, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByUserPref, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByUserPref, + experimentStatus=ExperimentStatus.DISQUALIFIED, + pref=True, + enrollmentStatusPref=ExperimentStatus.DISQUALIFIED, + ) + + def test_16(self): + # [D=F] On [A#3] -> Restart [A#4] -> Enrolled Treatment [A S=EnabledByUserPref SS=... + + if self.default_is is not False: + return + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByUserPref, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByUserPref, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByUserPref, + experimentStatus=ExperimentStatus.DISQUALIFIED, + pref=True, + enrollmentStatusPref=ExperimentStatus.DISQUALIFIED, + ) + + def test_17(self): + # [D=F] On [A#3] -> Restart [A#4] -> Off [A S=DisabledByDefault SS=EnabledByUserPr... + + if self.default_is is not False: + return + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByUserPref, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.marionette.set_pref(Prefs.WIN32K, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByUserPref, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_18(self): + # [D=F] On [A#3] -> Restart [A#4] -> Bad Requirements [A S=MissingRemoteWebGL SS=E... + + if self.default_is is not False: + return + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByUserPref, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.marionette.set_pref(Prefs.WEBGL, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.EnabledByUserPref, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_19(self): + # [D=F] On [A#3] -> Restart [A#4] -> E10S [A S=DisabledByE10S SS=DisabledByE10S ES... + + if self.default_is is not False: + return + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByUserPref, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + app_version = self.execute_script("return Services.appinfo.version") + self.restart(env={ENV_DISABLE_E10S: app_version}) + self.set_env(ENV_DISABLE_E10S, "null") + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByE10S, + sessionStatus=ContentWin32kLockdownState.DisabledByE10S, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByUserPref, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_20(self): + # [D=F] On [A#3] -> Restart [A#4] -> Header-On [A S=DisabledByEnvVar SS=DisabledBy... + + if self.default_is is not False: + return + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByUserPref, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart(env={ENV_DISABLE_WIN32K: "1"}) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByEnvVar, + sessionStatus=ContentWin32kLockdownState.DisabledByEnvVar, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.set_env(ENV_DISABLE_WIN32K, "") + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByUserPref, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_21(self): + # [D=T] On [A#3T] -> Restart [A#4T] -> Enrolled Control [A S=EnabledByDefault SS=E... + + if self.default_is is not True: + return + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + def test_22(self): + # [D=T] On [A#3T] -> Restart [A#4T] -> Enrolled Treatment [A S=EnabledByDefault SS... + + if self.default_is is not True: + return + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + def test_23(self): + # [D=T] On [A#3T] -> Restart [A#4T] -> Off [A S=DisabledByUserPref SS=EnabledByDef... + + if self.default_is is not True: + return + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.marionette.set_pref(Prefs.WIN32K, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByUserPref, + sessionStatus=ContentWin32kLockdownState.DisabledByUserPref, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_24(self): + # [D=T] On [A#3T] -> Restart [A#4T] -> Bad Requirements [A S=MissingRemoteWebGL SS... + + if self.default_is is not True: + return + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.marionette.set_pref(Prefs.WEBGL, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_25(self): + # [D=T] On [A#3T] -> Restart [A#4T] -> E10S [A S=DisabledByE10S SS=DisabledByE10S ... + + if self.default_is is not True: + return + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + app_version = self.execute_script("return Services.appinfo.version") + self.restart(env={ENV_DISABLE_E10S: app_version}) + self.set_env(ENV_DISABLE_E10S, "null") + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByE10S, + sessionStatus=ContentWin32kLockdownState.DisabledByE10S, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_26(self): + # [D=T] On [A#3T] -> Restart [A#4T] -> Header-On [A S=DisabledByEnvVar SS=Disabled... + + if self.default_is is not True: + return + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart(env={ENV_DISABLE_WIN32K: "1"}) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByEnvVar, + sessionStatus=ContentWin32kLockdownState.DisabledByEnvVar, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.set_env(ENV_DISABLE_WIN32K, "") + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_27(self): + # [D=F] Enrolled Control [A#5] -> Restart [A#6] -> Enrolled Control-C -> On [A S=E... + + if self.default_is is not False: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=True, + enrollmentStatusPref=ExperimentStatus.DISQUALIFIED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByUserPref, + experimentStatus=ExperimentStatus.DISQUALIFIED, + pref=True, + enrollmentStatusPref=ExperimentStatus.DISQUALIFIED, + ) + + def test_28(self): + # [D=F] Enrolled Control [A#5] -> Restart [A#6] -> Off [A#6]... + + if self.default_is is not False: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.marionette.set_pref(Prefs.WIN32K, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + def test_29(self): + # [D=F] Enrolled Control [A#5] -> Restart [A#6] -> Enrolled Control-C -> Bad Requi... + + if self.default_is is not False: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.marionette.set_pref(Prefs.WEBGL, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.DISQUALIFIED, + pref=False, + enrollmentStatusPref=ExperimentStatus.DISQUALIFIED, + ) + + def test_30(self): + # [D=F] Enrolled Control [A#5] -> Restart [A#6] -> Enrolled Control-C -> E10S [A S... + + if self.default_is is not False: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + app_version = self.execute_script("return Services.appinfo.version") + self.restart(env={ENV_DISABLE_E10S: app_version}) + self.set_env(ENV_DISABLE_E10S, "null") + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByE10S, + sessionStatus=ContentWin32kLockdownState.DisabledByE10S, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + def test_31(self): + # [D=F] Enrolled Control [A#5] -> Restart [A#6] -> Enrolled Control-C -> Header-On... + + if self.default_is is not False: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.restart(env={ENV_DISABLE_WIN32K: "1"}) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByEnvVar, + sessionStatus=ContentWin32kLockdownState.DisabledByEnvVar, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.set_env(ENV_DISABLE_WIN32K, "") + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + def test_32(self): + # [D=T] Enrolled Control [A#5T] -> Restart [A#6T] -> Enrolled Control-C -> On [A S... + + if self.default_is is not True: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + def test_33(self): + # [D=T] Enrolled Control [A#5T] -> Restart [A#6T] -> Off [A S=DisabledByUserPref S... + + if self.default_is is not True: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.marionette.set_pref(Prefs.WIN32K, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByUserPref, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=False, + enrollmentStatusPref=ExperimentStatus.DISQUALIFIED, + ) + + def test_34(self): + # [D=T] Enrolled Control [A#5T] -> Restart [A#6T] -> Enrolled Control-C -> Bad Req... + + if self.default_is is not True: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.marionette.set_pref(Prefs.WEBGL, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.DISQUALIFIED, + pref=True, + enrollmentStatusPref=ExperimentStatus.DISQUALIFIED, + ) + + def test_35(self): + # [D=T] Enrolled Control [A#5T] -> Restart [A#6T] -> Enrolled Control-C -> E10S [A... + + if self.default_is is not True: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + app_version = self.execute_script("return Services.appinfo.version") + self.restart(env={ENV_DISABLE_E10S: app_version}) + self.set_env(ENV_DISABLE_E10S, "null") + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByE10S, + sessionStatus=ContentWin32kLockdownState.DisabledByE10S, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + def test_36(self): + # [D=T] Enrolled Control [A#5T] -> Restart [A#6T] -> Enrolled Control-C -> Header-... + + if self.default_is is not True: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.restart(env={ENV_DISABLE_WIN32K: "1"}) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByEnvVar, + sessionStatus=ContentWin32kLockdownState.DisabledByEnvVar, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.set_env(ENV_DISABLE_WIN32K, "") + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByControlGroup, + sessionStatus=ContentWin32kLockdownState.DisabledByControlGroup, + experimentStatus=ExperimentStatus.ENROLLED_CONTROL, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + def test_37(self): + # [D=F] Enrolled Treatment [A#7] -> Restart [A#8] -> Enrolled Treatment-C -> On [A... + + if self.default_is is not False: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=True, + enrollmentStatusPref=ExperimentStatus.DISQUALIFIED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByUserPref, + experimentStatus=ExperimentStatus.DISQUALIFIED, + pref=True, + enrollmentStatusPref=ExperimentStatus.DISQUALIFIED, + ) + + def test_38(self): + # [D=F] Enrolled Treatment [A#7] -> Restart [A#8] -> Enrolled Treatment-C -> Off [... + + if self.default_is is not False: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.marionette.set_pref(Prefs.WIN32K, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + def test_39(self): + # [D=F] Enrolled Treatment [A#7] -> Restart [A#8] -> Enrolled Treatment-C -> Bad R... + + if self.default_is is not False: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.marionette.set_pref(Prefs.WEBGL, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.DISQUALIFIED, + pref=False, + enrollmentStatusPref=ExperimentStatus.DISQUALIFIED, + ) + + def test_40(self): + # [D=F] Enrolled Treatment [A#7] -> Restart [A#8] -> Enrolled Treatment-C -> E10S ... + + if self.default_is is not False: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + app_version = self.execute_script("return Services.appinfo.version") + self.restart(env={ENV_DISABLE_E10S: app_version}) + self.set_env(ENV_DISABLE_E10S, "null") + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByE10S, + sessionStatus=ContentWin32kLockdownState.DisabledByE10S, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + def test_41(self): + # [D=F] Enrolled Treatment [A#7] -> Restart [A#8] -> Enrolled Treatment-C -> Heade... + + if self.default_is is not False: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.restart(env={ENV_DISABLE_WIN32K: "1"}) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByEnvVar, + sessionStatus=ContentWin32kLockdownState.DisabledByEnvVar, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.set_env(ENV_DISABLE_WIN32K, "") + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + def test_42(self): + # [D=T] Enrolled Treatment [A#7T] -> Restart [A#8T] -> Enrolled Treatment-C -> On ... + + if self.default_is is not True: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + def test_43(self): + # [D=T] Enrolled Treatment [A#7T] -> Restart [A#8T] -> Enrolled Treatment-C -> Off... + + if self.default_is is not True: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.marionette.set_pref(Prefs.WIN32K, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByUserPref, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=False, + enrollmentStatusPref=ExperimentStatus.DISQUALIFIED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByUserPref, + sessionStatus=ContentWin32kLockdownState.DisabledByUserPref, + experimentStatus=ExperimentStatus.DISQUALIFIED, + pref=False, + enrollmentStatusPref=ExperimentStatus.DISQUALIFIED, + ) + + def test_44(self): + # [D=T] Enrolled Treatment [A#7T] -> Restart [A#8T] -> Enrolled Treatment-C -> Bad... + + if self.default_is is not True: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.marionette.set_pref(Prefs.WEBGL, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.DISQUALIFIED, + pref=True, + enrollmentStatusPref=ExperimentStatus.DISQUALIFIED, + ) + + def test_45(self): + # [D=T] Enrolled Treatment [A#7T] -> Restart [A#8T] -> Enrolled Treatment-C -> E10... + + if self.default_is is not True: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + app_version = self.execute_script("return Services.appinfo.version") + self.restart(env={ENV_DISABLE_E10S: app_version}) + self.set_env(ENV_DISABLE_E10S, "null") + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByE10S, + sessionStatus=ContentWin32kLockdownState.DisabledByE10S, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + def test_46(self): + # [D=T] Enrolled Treatment [A#7T] -> Restart [A#8T] -> Enrolled Treatment-C -> Hea... + + if self.default_is is not True: + return + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.restart(env={ENV_DISABLE_WIN32K: "1"}) + + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByEnvVar, + sessionStatus=ContentWin32kLockdownState.DisabledByEnvVar, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.set_env(ENV_DISABLE_WIN32K, "") + + # Re-set enrollment pref, like Normandy would do + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByTreatmentGroup, + sessionStatus=ContentWin32kLockdownState.EnabledByTreatmentGroup, + experimentStatus=ExperimentStatus.ENROLLED_TREATMENT, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + def test_47(self): + # [D=F] Bad Requirements [A#9] -> Restart [A#10] -> Enrolled Control [A S=MissingR... + + if self.default_is is not False: + return + + self.marionette.set_pref(Prefs.WEBGL, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.DISQUALIFIED, + pref=False, + enrollmentStatusPref=ExperimentStatus.DISQUALIFIED, + ) + + def test_48(self): + # [D=F] Bad Requirements [A#9] -> Restart [A#10] -> Enrolled Treatment [A S=Missin... + + if self.default_is is not False: + return + + self.marionette.set_pref(Prefs.WEBGL, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.DISQUALIFIED, + pref=False, + enrollmentStatusPref=ExperimentStatus.DISQUALIFIED, + ) + + def test_49(self): + # [D=F] Bad Requirements [A#9] -> Restart [A#10] -> On [A S=MissingRemoteWebGL SS=... + + if self.default_is is not False: + return + + self.marionette.set_pref(Prefs.WEBGL, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_50(self): + # [D=F] Bad Requirements [A#9] -> Restart [A#10] -> Off [A S=MissingRemoteWebGL SS... + + if self.default_is is not False: + return + + self.marionette.set_pref(Prefs.WEBGL, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.marionette.set_pref(Prefs.WIN32K, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_51(self): + # [D=T] Bad Requirements [A#9T] -> Restart [A#10T] -> Enrolled Control [A S=Missin... + + if self.default_is is not True: + return + + self.marionette.set_pref(Prefs.WEBGL, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_CONTROL, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.DISQUALIFIED, + pref=True, + enrollmentStatusPref=ExperimentStatus.DISQUALIFIED, + ) + + def test_52(self): + # [D=T] Bad Requirements [A#9T] -> Restart [A#10T] -> Enrolled Treatment [A S=Miss... + + if self.default_is is not True: + return + + self.marionette.set_pref(Prefs.WEBGL, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.ENROLLED_TREATMENT, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.DISQUALIFIED, + pref=True, + enrollmentStatusPref=ExperimentStatus.DISQUALIFIED, + ) + + def test_53(self): + # [D=T] Bad Requirements [A#9T] -> Restart [A#10T] -> On [A S=MissingRemoteWebGL S... + + if self.default_is is not True: + return + + self.marionette.set_pref(Prefs.WEBGL, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.marionette.set_pref(Prefs.WIN32K, True) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def test_54(self): + # [D=T] Bad Requirements [A#9T] -> Restart [A#10T] -> Off [A S=MissingRemoteWebGL ... + + if self.default_is is not True: + return + + self.marionette.set_pref(Prefs.WEBGL, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.restart() + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + self.marionette.set_pref(Prefs.WIN32K, False) + + self.check_win32k_status( + status=ContentWin32kLockdownState.MissingRemoteWebGL, + sessionStatus=ContentWin32kLockdownState.MissingRemoteWebGL, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) diff --git a/toolkit/xre/test/marionette/test_win32k_enrollment.template.py b/toolkit/xre/test/marionette/test_win32k_enrollment.template.py new file mode 100644 index 0000000000..30dc7fed33 --- /dev/null +++ b/toolkit/xre/test/marionette/test_win32k_enrollment.template.py @@ -0,0 +1,204 @@ +from contextlib import contextmanager + +from marionette_harness import MarionetteTestCase + + +class ExperimentStatus: + UNENROLLED = 0 + ENROLLED_CONTROL = 1 + ENROLLED_TREATMENT = 2 + DISQUALIFIED = 3 + + +class ContentWin32kLockdownState: + LockdownEnabled = 1 + MissingWebRender = 2 + OperatingSystemNotSupported = 3 + PrefNotSet = 4 + MissingRemoteWebGL = 5 + MissingNonNativeTheming = 6 + DisabledByEnvVar = 7 + DisabledBySafeMode = 8 + DisabledByE10S = 9 + DisabledByUserPref = 10 + EnabledByUserPref = 11 + DisabledByControlGroup = 12 + EnabledByTreatmentGroup = 13 + DisabledByDefault = 14 + EnabledByDefault = 15 + + +class Prefs: + ENROLLMENT_STATUS = "security.sandbox.content.win32k-experiment.enrollmentStatus" + STARTUP_ENROLLMENT_STATUS = ( + "security.sandbox.content.win32k-experiment.startupEnrollmentStatus" + ) + WIN32K = "security.sandbox.content.win32k-disable" + WEBGL = "webgl.out-of-process" + + +ENV_DISABLE_WIN32K = "MOZ_ENABLE_WIN32K" +ENV_DISABLE_E10S = "MOZ_FORCE_DISABLE_E10S" + + +class TestWin32kAutostart(MarionetteTestCase): + SANDBOX_NAME = "win32k-autostart" + + def execute_script(self, code, *args, **kwargs): + with self.marionette.using_context(self.marionette.CONTEXT_CHROME): + return self.marionette.execute_script( + code, new_sandbox=False, sandbox=self.SANDBOX_NAME, *args, **kwargs + ) + + def get_win32k_status(self): + return self.execute_script( + r""" + let win = Services.wm.getMostRecentWindow("navigator:browser"); + let ses = "security.sandbox.content.win32k-experiment.startupEnrollmentStatus"; + return { + win32kSessionStatus: Services.appinfo.win32kSessionStatus, + win32kStatus: Services.appinfo.win32kLiveStatusTestingOnly, + win32kExperimentStatus: Services.appinfo.win32kExperimentStatus, + win32kPref: Services.prefs.getBoolPref("security.sandbox.content.win32k-disable"), + win32kStartupEnrollmentStatusPref: Services.prefs.getIntPref(ses), + }; + """ + ) + + def check_win32k_status( + self, status, sessionStatus, experimentStatus, pref, enrollmentStatusPref + ): + # We CANNOT check win32kEnrollmentStatusPref after a restart because we only set this + # pref on the default branch, and it goes away after a restart, so we only check + # the startupEnrollmentStatusPref + expected = { + "win32kSessionStatus": sessionStatus, + "win32kStatus": status, + "win32kExperimentStatus": experimentStatus, + "win32kPref": pref, + "win32kStartupEnrollmentStatusPref": enrollmentStatusPref, + } + + status = self.get_win32k_status() + + for prop, value in expected.items(): + self.assertEqual( + status[prop], + value, + "%s should have the value `%r`, but has `%r`" + % (prop, value, status[prop]), + ) + + def set_env(self, env, value): + self.execute_script( + "env.set(arguments[0], arguments[1]);", script_args=(env, value) + ) + + def get_env(self, env): + return self.execute_script("return env.get(arguments[0]);", script_args=(env,)) + + def set_enrollment_status(self, status): + self.marionette.set_pref(Prefs.ENROLLMENT_STATUS, status, default_branch=True) + + updated_status = self.marionette.get_pref(Prefs.ENROLLMENT_STATUS) + self.assertTrue( + status == updated_status or updated_status == ExperimentStatus.DISQUALIFIED + ) + startup_status = self.marionette.get_pref(Prefs.STARTUP_ENROLLMENT_STATUS) + self.assertEqual( + startup_status, + updated_status, + "Startup enrollment status (%r) should match " + "session status (%r)" % (startup_status, updated_status), + ) + + def restart(self, prefs=None, env=None): + if prefs: + self.marionette.set_prefs(prefs) + + if env: + for name, value in env.items(): + self.set_env(name, value) + + self.marionette.restart(in_app=True, clean=False) + self.setUpSession() + + # Sanity check our environment. + if prefs: + for key, val in prefs.items(): + if val is not None: + self.assertEqual(self.marionette.get_pref(key), val) + if env: + for key, val in env.items(): + self.assertEqual(self.get_env(key), val or "") + + def setUpSession(self): + self.marionette.set_context(self.marionette.CONTEXT_CHROME) + + self.execute_script( + r""" + // We're running in a function, in a sandbox, that inherits from an + // X-ray wrapped window. Anything we want to be globally available + // needs to be defined on that window. + window.env = Services.env; + """ + ) + + @contextmanager + def full_restart(self): + profile = self.marionette.instance.profile + try: + self.marionette.quit() + yield profile + finally: + self.marionette.start_session() + self.setUpSession() + + def setUp(self): + super(TestWin32kAutostart, self).setUp() + + # If we have configured marionette to require a particular value for + # `win32k.autostart`, remove it as a forced pref until `tearDown`, and + # perform a clean restart, so we run this test without the pref + # pre-configured. + self.win32kRequired = None + if Prefs.WIN32K in self.marionette.instance.required_prefs: + self.win32kRequired = self.marionette.instance.required_prefs[Prefs.WIN32K] + del self.marionette.instance.required_prefs[Prefs.WIN32K] + self.marionette.restart(in_app=False, clean=True) + + self.setUpSession() + + # Marionette doesn't let you set preferences on the default branch before startup + # so we can't test the default=False and default=True scenarios in one test + # What we can do is generate all the tests, and then only run the runs for which + # the default is. (And run the other ones locally to make sure they work before + # we land it.) + prefJS = 'return Services.prefs.getBoolPref("security.sandbox.content.win32k-disable");' + self.default_is = self.execute_script(prefJS) + + if self.default_is is False: + # Win32k status must start out with `disabledByDefault` + self.check_win32k_status( + status=ContentWin32kLockdownState.DisabledByDefault, + sessionStatus=ContentWin32kLockdownState.DisabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=False, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + else: + # Win32k status must start out with `enabledByDefault` + self.check_win32k_status( + status=ContentWin32kLockdownState.EnabledByDefault, + sessionStatus=ContentWin32kLockdownState.EnabledByDefault, + experimentStatus=ExperimentStatus.UNENROLLED, + pref=True, + enrollmentStatusPref=ExperimentStatus.UNENROLLED, + ) + + def tearDown(self): + if self.win32kRequired is not None: + self.marionette.instance.required_prefs[Prefs.WIN32K] = self.win32kRequired + self.marionette.restart(in_app=False, clean=True) + + super(TestWin32kAutostart, self).tearDown() diff --git a/toolkit/xre/test/marionette/win32k_tests.txt b/toolkit/xre/test/marionette/win32k_tests.txt new file mode 100644 index 0000000000..76068de268 --- /dev/null +++ b/toolkit/xre/test/marionette/win32k_tests.txt @@ -0,0 +1,167 @@ +#############################################
+# This file serves as the test case list for gen_win32k_tests.py
+
+#############################################
+# Only here for reference:
+
+# MissingWebRender = 2
+# OperatingSystemNotSupported = 3
+# MissingRemoteWebGL = 5
+# DisabledByEnvVar = 7
+# DisabledBySafeMode = 8
+# DisabledByE10S = 9
+# DisabledByUserPref = 10
+# EnabledByUserPref = 11
+# DisabledByControlGroup = 12
+# EnabledByTreatmentGroup = 13
+# DisabledByDefault = 14
+# EnabledByDefault = 15
+
+# UNENROLLED
+# ENROLLED_CONTROL
+# ENROLLED_TREATMENT
+# DISQUALIFIED
+
+#############################################
+# Assertion Mappings
+# Shorthand values for the test cases below. Instead of writing out the whole thing
+# you can just do [A#5]
+
+ > 1 : [A S=DisabledByDefault SS=DisabledByDefault ES=UNENROLLED P=False ESP=UNENROLLED]
+ > 1T : [A S=EnabledByDefault SS=EnabledByDefault ES=UNENROLLED P=True ESP=UNENROLLED]
+ > 2 : [A S=EnabledByDefault SS=EnabledByDefault ES=UNENROLLED P=True ESP=UNENROLLED]
+ > 3 : [A S=EnabledByUserPref SS=DisabledByDefault ES=UNENROLLED P=True ESP=UNENROLLED]
+ > 3T : [A S=EnabledByDefault SS=EnabledByDefault ES=UNENROLLED P=True ESP=UNENROLLED]
+ > 4 : [A S=EnabledByUserPref SS=EnabledByUserPref ES=UNENROLLED P=True ESP=UNENROLLED]
+ > 4T : [A S=EnabledByDefault SS=EnabledByDefault ES=UNENROLLED P=True ESP=UNENROLLED]
+ > 5 : [A S=DisabledByDefault SS=DisabledByDefault ES=UNENROLLED P=False ESP=ENROLLED_CONTROL]
+ > 5T : [A S=EnabledByDefault SS=EnabledByDefault ES=UNENROLLED P=True ESP=ENROLLED_CONTROL]
+ > 6 : [A S=DisabledByControlGroup SS=DisabledByControlGroup ES=ENROLLED_CONTROL P=False ESP=ENROLLED_CONTROL]
+ > 6T : [A S=DisabledByControlGroup SS=DisabledByControlGroup ES=ENROLLED_CONTROL P=True ESP=ENROLLED_CONTROL]
+ > 7 : [A S=DisabledByDefault SS=DisabledByDefault ES=UNENROLLED P=False ESP=ENROLLED_TREATMENT]
+ > 7T : [A S=EnabledByDefault SS=EnabledByDefault ES=UNENROLLED P=True ESP=ENROLLED_TREATMENT]
+ > 8 : [A S=EnabledByTreatmentGroup SS=EnabledByTreatmentGroup ES=ENROLLED_TREATMENT P=False ESP=ENROLLED_TREATMENT]
+ > 8T : [A S=EnabledByTreatmentGroup SS=EnabledByTreatmentGroup ES=ENROLLED_TREATMENT P=True ESP=ENROLLED_TREATMENT]
+ > 9 : [A S=MissingRemoteWebGL SS=DisabledByDefault ES=UNENROLLED P=False ESP=UNENROLLED]
+ > 9T : [A S=MissingRemoteWebGL SS=EnabledByDefault ES=UNENROLLED P=True ESP=UNENROLLED]
+ > 10 : [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=UNENROLLED P=False ESP=UNENROLLED]
+ > 10T: [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=UNENROLLED P=True ESP=UNENROLLED]
+
+
+# TODO
+# - win32k env var tests
+
+
+#############################################
+# Tests
+
+# Syntax:
+# [D=T] - is the default value for win32k lockdown pref 'false' or 'true'
+# Action -> Action
+# Valid Actions:
+# Nothing
+# Enrolled Control/Treatment
+# On/Off (changes win32k pref)
+# Restart
+# E10s (Restarts with E10s disabled)
+# Header-On - Restart with MOZ_ENABLE_WIN32K set
+# Header-Off - unset MOZ_ENABLE_WIN32K but do not restart
+# Bad Requirements - sets webg out of process to false, invalidating win32k
+# Enrolled /Control/Treatment]-C:
+# The enrollment pref is set by normandy on _every_ startup on the
+# default branch, where it doesn't persist.
+# This action represents normandy doing its normal set that
+# occurs after a restart. If Normandy doesn't do it, the
+# startupEnrollmentPref is set back to 0 after restart, which is expected.
+# [A S= SS= ES= P= ESP=] - trigger an assertion check for the listed values
+# S - 'Status' or the current value of calculating GetWin32kLockdownState()
+# SS - 'Session Status' or the current value of set-once-at-startup static variable gWin32kStatus
+# ES - 'Experiment Status' or the value of gWin32kExperimentStatus
+# P - 'Pref' or the value of security.sandbox.content.win32k-disable
+# ESP- 'Enrollment Status Pref' or the value of security.sandbox.content.win32k-experiment.startupEnrollmentStatus
+# [A#5] - trigger an assertion check from the mapping table above
+
+
+# Safe Mode tests are currently not generated, as I don't know how to make marionette restart in safe mode
+
+#################
+ - [D=F] Nothing [A#1] -> Enrolled Control [A S=DisabledByDefault SS=DisabledByDefault ES=UNENROLLED P=False ESP=ENROLLED_CONTROL] -> Restart [A S=DisabledByControlGroup SS=DisabledByControlGroup ES=ENROLLED_CONTROL P=False ESP=ENROLLED_CONTROL]
+ - [D=F] Nothing [A#1] -> Enrolled Treatment [A S=DisabledByDefault SS=DisabledByDefault ES=UNENROLLED P=False ESP=ENROLLED_TREATMENT] -> Restart [A S=EnabledByTreatmentGroup SS=EnabledByTreatmentGroup ES=ENROLLED_TREATMENT P=False ESP=ENROLLED_TREATMENT]
+ - [D=F] Nothing [A#1] -> On [A S=EnabledByUserPref SS=DisabledByDefault ES=UNENROLLED P=True ESP=UNENROLLED] -> Restart [A S=EnabledByUserPref SS=EnabledByUserPref ES=UNENROLLED P=True ESP=UNENROLLED]
+ - [D=F] Nothing [A#1] -> Off [A#1] -> Restart [A#1]
+ - [D=F] Nothing [A#1] -> On -> Bad Requirements [A S=MissingRemoteWebGL SS=DisabledByDefault ES=UNENROLLED P=True ESP=UNENROLLED] -> Restart [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=UNENROLLED P=True ESP=UNENROLLED]
+ - [D=F] Nothing [A#1] -> On -> E10S [A S=DisabledByE10S SS=DisabledByE10S ES=UNENROLLED P=True ESP=UNENROLLED]
+ - [D=F] Nothing [A#1] -> On -> Header-On [A S=DisabledByEnvVar SS=DisabledByEnvVar ES=UNENROLLED P=True ESP=UNENROLLED] -> Header-Off [A S=EnabledByUserPref SS=DisabledByEnvVar ES=UNENROLLED P=True ESP=UNENROLLED]
+ - [D=F] Nothing [A#1] -> Safe Mode -> Restart
+
+#################
+ - [D=T] Nothing [A#1T] -> Enrolled Control [A S=EnabledByDefault SS=EnabledByDefault ES=UNENROLLED P=True ESP=ENROLLED_CONTROL] -> Restart [A S=DisabledByControlGroup SS=DisabledByControlGroup ES=ENROLLED_CONTROL P=True ESP=ENROLLED_CONTROL]
+ - [D=T] Nothing [A#1T] -> Enrolled Treatment [A S=EnabledByDefault SS=EnabledByDefault ES=UNENROLLED P=True ESP=ENROLLED_TREATMENT] -> Restart [A S=EnabledByTreatmentGroup SS=EnabledByTreatmentGroup ES=ENROLLED_TREATMENT P=True ESP=ENROLLED_TREATMENT]
+ - [D=T] Nothing [A#1T] -> On [A S=EnabledByDefault SS=EnabledByDefault ES=UNENROLLED P=True ESP=UNENROLLED] -> Restart [A S=EnabledByDefault SS=EnabledByDefault ES=UNENROLLED P=True ESP=UNENROLLED]
+ - [D=T] Nothing [A#1T] -> Off [A S=DisabledByUserPref SS=EnabledByDefault ES=UNENROLLED P=False ESP=UNENROLLED] -> Restart [A S=DisabledByUserPref SS=DisabledByUserPref ES=UNENROLLED P=False ESP=UNENROLLED]
+ - [D=T] Nothing [A#1T] -> On -> Bad Requirements [A S=MissingRemoteWebGL SS=EnabledByDefault ES=UNENROLLED P=True ESP=UNENROLLED] -> Restart [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=UNENROLLED P=True ESP=UNENROLLED]
+ - [D=T] Nothing [A#1T] -> On -> E10S [A S=DisabledByE10S SS=DisabledByE10S ES=UNENROLLED P=True ESP=UNENROLLED]
+ - [D=T] Nothing [A#1T] -> On -> Header-On [A S=DisabledByEnvVar SS=DisabledByEnvVar ES=UNENROLLED P=True ESP=UNENROLLED] -> Header-Off [A S=EnabledByDefault SS=DisabledByEnvVar ES=UNENROLLED P=True ESP=UNENROLLED]
+ - [D=T] Nothing [A#1T] -> Safe Mode -> Restart
+
+#################
+ - [D=F] On [A#3] -> Restart [A#4] -> Enrolled Control [A S=EnabledByUserPref SS=EnabledByUserPref ES=UNENROLLED P=True ESP=ENROLLED_CONTROL] -> Restart [A S=EnabledByUserPref SS=EnabledByUserPref ES=DISQUALIFIED P=True ESP=DISQUALIFIED]
+ - [D=F] On [A#3] -> Restart [A#4] -> Enrolled Treatment [A S=EnabledByUserPref SS=EnabledByUserPref ES=UNENROLLED P=True ESP=ENROLLED_TREATMENT] -> Restart [A S=EnabledByUserPref SS=EnabledByUserPref ES=DISQUALIFIED P=True ESP=DISQUALIFIED]
+ - [D=F] On [A#3] -> Restart [A#4] -> Off [A S=DisabledByDefault SS=EnabledByUserPref ES=UNENROLLED P=False ESP=UNENROLLED] -> Restart [A S=DisabledByDefault SS=DisabledByDefault ES=UNENROLLED P=False ESP=UNENROLLED]
+ - [D=F] On [A#3] -> Restart [A#4] -> Bad Requirements [A S=MissingRemoteWebGL SS=EnabledByUserPref ES=UNENROLLED P=True ESP=UNENROLLED]
+ - [D=F] On [A#3] -> Restart [A#4] -> E10S [A S=DisabledByE10S SS=DisabledByE10S ES=UNENROLLED P=True ESP=UNENROLLED] -> Restart [A#4]
+ - [D=F] On [A#3] -> Restart [A#4] -> Header-On [A S=DisabledByEnvVar SS=DisabledByEnvVar ES=UNENROLLED P=True ESP=UNENROLLED] -> Header-Off -> Restart [A#4]
+ - [D=F] On [A#3] -> Restart [A#4] -> Safe Mode
+
+#################
+ - [D=T] On [A#3T] -> Restart [A#4T] -> Enrolled Control [A S=EnabledByDefault SS=EnabledByDefault ES=UNENROLLED P=True ESP=ENROLLED_CONTROL] -> Restart [A S=DisabledByControlGroup SS=DisabledByControlGroup ES=ENROLLED_CONTROL P=True ESP=ENROLLED_CONTROL]
+ - [D=T] On [A#3T] -> Restart [A#4T] -> Enrolled Treatment [A S=EnabledByDefault SS=EnabledByDefault ES=UNENROLLED P=True ESP=ENROLLED_TREATMENT] -> Restart [A S=EnabledByTreatmentGroup SS=EnabledByTreatmentGroup ES=ENROLLED_TREATMENT P=True ESP=ENROLLED_TREATMENT]
+ - [D=T] On [A#3T] -> Restart [A#4T] -> Off [A S=DisabledByUserPref SS=EnabledByDefault ES=UNENROLLED P=False ESP=UNENROLLED] -> Restart [A S=DisabledByUserPref SS=DisabledByUserPref ES=UNENROLLED P=False ESP=UNENROLLED]
+ - [D=T] On [A#3T] -> Restart [A#4T] -> Bad Requirements [A S=MissingRemoteWebGL SS=EnabledByDefault ES=UNENROLLED P=True ESP=UNENROLLED]
+ - [D=T] On [A#3T] -> Restart [A#4T] -> E10S [A S=DisabledByE10S SS=DisabledByE10S ES=UNENROLLED P=True ESP=UNENROLLED] -> Restart [A#4T]
+ - [D=T] On [A#3T] -> Restart [A#4T] -> Header-On [A S=DisabledByEnvVar SS=DisabledByEnvVar ES=UNENROLLED P=True ESP=UNENROLLED] -> Header-Off -> Restart [A#4T]
+ - [D=T] On [A#3T] -> Restart [A#4T] -> Safe Mode
+
+#################
+ - [D=F] Enrolled Control [A#5] -> Restart [A#6] -> Enrolled Control-C -> On [A S=EnabledByUserPref SS=DisabledByControlGroup ES=ENROLLED_CONTROL P=True ESP=DISQUALIFIED] -> Restart [A S=EnabledByUserPref SS=EnabledByUserPref ES=DISQUALIFIED P=True ESP=DISQUALIFIED]
+ - [D=F] Enrolled Control [A#5] -> Restart [A#6] -> Off [A#6]
+ - [D=F] Enrolled Control [A#5] -> Restart [A#6] -> Enrolled Control-C -> Bad Requirements [A S=MissingRemoteWebGL SS=DisabledByControlGroup ES=ENROLLED_CONTROL P=False ESP=ENROLLED_CONTROL] -> Restart [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=DISQUALIFIED P=False ESP=DISQUALIFIED]
+ - [D=F] Enrolled Control [A#5] -> Restart [A#6] -> Enrolled Control-C -> E10S [A S=DisabledByE10S SS=DisabledByE10S ES=ENROLLED_CONTROL P=False ESP=ENROLLED_CONTROL] -> Enrolled Control-C -> Restart [A#6]
+ - [D=F] Enrolled Control [A#5] -> Restart [A#6] -> Enrolled Control-C -> Header-On [A S=DisabledByEnvVar SS=DisabledByEnvVar ES=ENROLLED_CONTROL P=False ESP=ENROLLED_CONTROL] -> Header-Off -> Enrolled Control-C -> Restart [A#6]
+ - [D=F] Enrolled Control [A#5] -> Restart [A#6] -> Safe Mode ->
+
+#################
+ - [D=T] Enrolled Control [A#5T] -> Restart [A#6T] -> Enrolled Control-C -> On [A S=DisabledByControlGroup SS=DisabledByControlGroup ES=ENROLLED_CONTROL P=True ESP=ENROLLED_CONTROL] -> Restart [A S=DisabledByControlGroup SS=DisabledByControlGroup ES=ENROLLED_CONTROL P=True ESP=ENROLLED_CONTROL]
+ - [D=T] Enrolled Control [A#5T] -> Restart [A#6T] -> Off [A S=DisabledByUserPref SS=DisabledByControlGroup ES=ENROLLED_CONTROL P=False ESP=DISQUALIFIED]
+ - [D=T] Enrolled Control [A#5T] -> Restart [A#6T] -> Enrolled Control-C -> Bad Requirements [A S=MissingRemoteWebGL SS=DisabledByControlGroup ES=ENROLLED_CONTROL P=True ESP=ENROLLED_CONTROL] -> Restart [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=DISQUALIFIED P=True ESP=DISQUALIFIED]
+ - [D=T] Enrolled Control [A#5T] -> Restart [A#6T] -> Enrolled Control-C -> E10S [A S=DisabledByE10S SS=DisabledByE10S ES=ENROLLED_CONTROL P=True ESP=ENROLLED_CONTROL] -> Enrolled Control-C -> Restart [A#6T]
+ - [D=T] Enrolled Control [A#5T] -> Restart [A#6T] -> Enrolled Control-C -> Header-On [A S=DisabledByEnvVar SS=DisabledByEnvVar ES=ENROLLED_CONTROL P=True ESP=ENROLLED_CONTROL] -> Header-Off -> Enrolled Control-C -> Restart [A#6T]
+ - [D=T] Enrolled Control [A#5T] -> Restart [A#6T] -> Safe Mode ->
+
+#################
+ - [D=F] Enrolled Treatment [A#7] -> Restart [A#8] -> Enrolled Treatment-C -> On [A S=EnabledByUserPref SS=EnabledByTreatmentGroup ES=ENROLLED_TREATMENT P=True ESP=DISQUALIFIED] -> Restart [A S=EnabledByUserPref SS=EnabledByUserPref ES=DISQUALIFIED P=True ESP=DISQUALIFIED]
+ - [D=F] Enrolled Treatment [A#7] -> Restart [A#8] -> Enrolled Treatment-C -> Off [A#8] -> Restart [A#8]
+ - [D=F] Enrolled Treatment [A#7] -> Restart [A#8] -> Enrolled Treatment-C -> Bad Requirements [A S=MissingRemoteWebGL SS=EnabledByTreatmentGroup ES=ENROLLED_TREATMENT P=False ESP=ENROLLED_TREATMENT] -> Restart [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=DISQUALIFIED P=False ESP=DISQUALIFIED]
+ - [D=F] Enrolled Treatment [A#7] -> Restart [A#8] -> Enrolled Treatment-C -> E10S [A S=DisabledByE10S SS=DisabledByE10S ES=ENROLLED_TREATMENT P=False ESP=ENROLLED_TREATMENT] -> Enrolled Treatment-C -> Restart [A#8]
+ - [D=F] Enrolled Treatment [A#7] -> Restart [A#8] -> Enrolled Treatment-C -> Header-On [A S=DisabledByEnvVar SS=DisabledByEnvVar ES=ENROLLED_TREATMENT P=False ESP=ENROLLED_TREATMENT] -> Header-Off -> Enrolled Treatment-C -> Restart [A#8]
+ - [D=F] Enrolled Treatment [A#7] -> Restart [A#8] -> Safe Mode ->
+
+#################
+ - [D=T] Enrolled Treatment [A#7T] -> Restart [A#8T] -> Enrolled Treatment-C -> On [A S=EnabledByTreatmentGroup SS=EnabledByTreatmentGroup ES=ENROLLED_TREATMENT P=True ESP=ENROLLED_TREATMENT] -> Restart [A S=EnabledByTreatmentGroup SS=EnabledByTreatmentGroup ES=ENROLLED_TREATMENT P=True ESP=ENROLLED_TREATMENT]
+ - [D=T] Enrolled Treatment [A#7T] -> Restart [A#8T] -> Enrolled Treatment-C -> Off [A S=DisabledByUserPref SS=EnabledByTreatmentGroup ES=ENROLLED_TREATMENT P=False ESP=DISQUALIFIED] -> Restart [A S=DisabledByUserPref SS=DisabledByUserPref ES=DISQUALIFIED P=False ESP=DISQUALIFIED]
+ - [D=T] Enrolled Treatment [A#7T] -> Restart [A#8T] -> Enrolled Treatment-C -> Bad Requirements [A S=MissingRemoteWebGL SS=EnabledByTreatmentGroup ES=ENROLLED_TREATMENT P=True ESP=ENROLLED_TREATMENT] -> Restart [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=DISQUALIFIED P=True ESP=DISQUALIFIED]
+ - [D=T] Enrolled Treatment [A#7T] -> Restart [A#8T] -> Enrolled Treatment-C -> E10S [A S=DisabledByE10S SS=DisabledByE10S ES=ENROLLED_TREATMENT P=True ESP=ENROLLED_TREATMENT] -> Enrolled Treatment-C -> Restart [A#8T]
+ - [D=T] Enrolled Treatment [A#7T] -> Restart [A#8T] -> Enrolled Treatment-C -> Header-On [A S=DisabledByEnvVar SS=DisabledByEnvVar ES=ENROLLED_TREATMENT P=True ESP=ENROLLED_TREATMENT] -> Header-Off -> Enrolled Treatment-C -> Restart [A#8T]
+ - [D=T] Enrolled Treatment [A#7T] -> Restart [A#8T] -> Safe Mode ->
+
+#################
+ - [D=F] Bad Requirements [A#9] -> Restart [A#10] -> Enrolled Control [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=UNENROLLED P=False ESP=ENROLLED_CONTROL] -> Restart [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=DISQUALIFIED P=False ESP=DISQUALIFIED]
+ - [D=F] Bad Requirements [A#9] -> Restart [A#10] -> Enrolled Treatment [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=UNENROLLED P=False ESP=ENROLLED_TREATMENT] -> Restart [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=DISQUALIFIED P=False ESP=DISQUALIFIED]
+ - [D=F] Bad Requirements [A#9] -> Restart [A#10] -> On [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=UNENROLLED P=True ESP=UNENROLLED]
+ - [D=F] Bad Requirements [A#9] -> Restart [A#10] -> Off [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=UNENROLLED P=False ESP=UNENROLLED]
+
+#################
+ - [D=T] Bad Requirements [A#9T] -> Restart [A#10T] -> Enrolled Control [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=UNENROLLED P=True ESP=ENROLLED_CONTROL] -> Restart [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=DISQUALIFIED P=True ESP=DISQUALIFIED]
+ - [D=T] Bad Requirements [A#9T] -> Restart [A#10T] -> Enrolled Treatment [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=UNENROLLED P=True ESP=ENROLLED_TREATMENT] -> Restart [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=DISQUALIFIED P=True ESP=DISQUALIFIED]
+ - [D=T] Bad Requirements [A#9T] -> Restart [A#10T] -> On [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=UNENROLLED P=True ESP=UNENROLLED]
+ - [D=T] Bad Requirements [A#9T] -> Restart [A#10T] -> Off [A S=MissingRemoteWebGL SS=MissingRemoteWebGL ES=UNENROLLED P=False ESP=UNENROLLED]
diff --git a/toolkit/xre/test/mochitest.ini b/toolkit/xre/test/mochitest.ini new file mode 100644 index 0000000000..ccbb08ed97 --- /dev/null +++ b/toolkit/xre/test/mochitest.ini @@ -0,0 +1,3 @@ +[DEFAULT] + +[test_fpuhandler.html] diff --git a/toolkit/xre/test/show_hash.js b/toolkit/xre/test/show_hash.js new file mode 100644 index 0000000000..28674e6840 --- /dev/null +++ b/toolkit/xre/test/show_hash.js @@ -0,0 +1,4 @@ +const xre = Cc["@mozilla.org/xre/directory-provider;1"].getService( + Ci.nsIXREDirProvider +); +dump(`${xre.getInstallHash(false)}\n`); diff --git a/toolkit/xre/test/test_fpuhandler.html b/toolkit/xre/test/test_fpuhandler.html new file mode 100644 index 0000000000..a3869f2ecf --- /dev/null +++ b/toolkit/xre/test/test_fpuhandler.html @@ -0,0 +1,36 @@ +<head> + <title>Floating-point exception handler test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + +<body onload="runTest()"> + <embed id="plugin1" type="application/x-test" width="400" height="400"></embed> + + <script class="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + function doDiv(x, y) { + var z; + z = x / y; + + for (let i = 0 + x; i < 1000; ++i) + z = y / x; + + z = x / y; + return z; + } + + function runTest() { + ok(isNaN(doDiv(0.0, 0.0)), "Undefined division-by-zero doesn't crash"); + + try { + document.getElementById("plugin1").enableFPExceptions(); + } catch (e) { + ok(true, "No special code to set the FPU bit in the testplugin."); + SimpleTest.finish(); + return; + } + + ok(isNaN(doDiv(0.0, 0.0)), "Undefined division-by-zero doesn't crash again."); + SimpleTest.finish(); + } + </script> diff --git a/toolkit/xre/test/test_install_hash.js b/toolkit/xre/test/test_install_hash.js new file mode 100644 index 0000000000..9dc331fac5 --- /dev/null +++ b/toolkit/xre/test/test_install_hash.js @@ -0,0 +1,146 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This runs the xpcshell binary with different cases for the executable path. + * They should all result in the same installation hash. + */ + +const { Subprocess } = ChromeUtils.importESModule( + "resource://gre/modules/Subprocess.sys.mjs" +); + +const XRE = Cc["@mozilla.org/xre/directory-provider;1"].getService( + Ci.nsIXREDirProvider +); +const HASH = XRE.getInstallHash(false); +const EXE = Services.dirsvc.get("XREExeF", Ci.nsIFile); +const SCRIPT = do_get_file("show_hash.js", false); + +async function getHash(bin) { + try { + // If this test is running through firefox.exe -xpcshell, we need + // to make sure to execute the script through it as well. + let args = []; + if (!bin.leafName.startsWith("xpcshell")) { + args.push("-xpcshell"); + } + args.push(SCRIPT.path); + + let proc = await Subprocess.call({ + command: bin.path, + arguments: args, + }); + + let result = ""; + let string; + while ((string = await proc.stdout.readString())) { + result += string; + } + + return result.trim(); + } catch (e) { + if (e.errorCode == Subprocess.ERROR_BAD_EXECUTABLE) { + return null; + } + throw e; + } +} + +// Walks through a path's entries and calls a mutator function to change the +// case of each. +function mutatePath(path, mutator) { + let parts = []; + let index = 0; + while (path.parent != null) { + parts.push(mutator(path.leafName, index++)); + path = path.parent; + } + + while (parts.length) { + path.append(parts.pop()); + } + + return path; +} + +// Counts how many path parts a mutator will be called for. +function countParts(path) { + let index = 0; + while (path.parent != null) { + path = path.parent; + index++; + } + return index; +} + +add_task(async function testSameBinary() { + // Running with the same binary path should definitely work and give the same + // hash. + Assert.equal( + await getHash(EXE), + HASH, + "Should have the same hash when running the same binary." + ); +}); + +add_task(async function testUpperCase() { + let upper = mutatePath(EXE, p => p.toLocaleUpperCase()); + let hash = await getHash(upper); + + // We may not get a hash if any part of the filesystem is case sensitive. + if (hash) { + Assert.equal( + hash, + HASH, + `Should have seen the same hash from ${upper.path}.` + ); + } +}); + +add_task(async function testLowerCase() { + let lower = mutatePath(EXE, p => p.toLocaleLowerCase()); + let hash = await getHash(lower); + + // We may not get a hash if any part of the filesystem is case sensitive. + if (hash) { + Assert.equal( + hash, + HASH, + `Should have seen the same hash from ${lower.path}.` + ); + } +}); + +add_task(async function testEachPart() { + // We need to check the case where only some of the directories in the path + // are case insensitive. + + let count = countParts(EXE); + for (let i = 0; i < count; i++) { + let upper = mutatePath(EXE, (p, index) => + index == i ? p.toLocaleUpperCase() : p + ); + let lower = mutatePath(EXE, (p, index) => + index == i ? p.toLocaleLowerCase() : p + ); + + let upperHash = await getHash(upper); + if (upperHash) { + Assert.equal( + upperHash, + HASH, + `Should have seen the same hash from ${upper.path}.` + ); + } + + let lowerHash = await getHash(lower); + if (lowerHash) { + Assert.equal( + lowerHash, + HASH, + `Should have seen the same hash from ${lower.path}.` + ); + } + } +}); diff --git a/toolkit/xre/test/test_launch_without_hang.js b/toolkit/xre/test/test_launch_without_hang.js new file mode 100644 index 0000000000..feacae36d4 --- /dev/null +++ b/toolkit/xre/test/test_launch_without_hang.js @@ -0,0 +1,256 @@ +// 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/. + +// bug 1360493 +// Launch the browser a number of times, testing startup hangs. + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const APP_TIMER_TIMEOUT_MS = 1000 * 15; +const TRY_COUNT = 50; + +// Sets a group of environment variables, returning the old values. +// newVals AND return value is an array of { key: "", value: "" } +function setEnvironmentVariables(newVals) { + let oldVals = []; + for (let i = 0; i < newVals.length; ++i) { + let key = newVals[i].key; + let value = newVals[i].value; + let oldObj = { key }; + if (Services.env.exists(key)) { + oldObj.value = Services.env.get(key); + } else { + oldObj.value = null; + } + + Services.env.set(key, value); + oldVals.push(oldObj); + } + return oldVals; +} + +function getFirefoxExecutableFilename() { + if (AppConstants.platform === "win") { + return AppConstants.MOZ_APP_NAME + ".exe"; + } + return AppConstants.MOZ_APP_NAME; +} + +// Returns a nsIFile to the firefox.exe executable file +function getFirefoxExecutableFile() { + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file = Services.dirsvc.get("GreBinD", Ci.nsIFile); + + file.append(getFirefoxExecutableFilename()); + return file; +} + +// Takes an executable and arguments, and wraps it in a call to the system shell. +// Technique adapted from \toolkit\mozapps\update\tests\unit_service_updater\xpcshellUtilsAUS.js +// to avoid child process console output polluting the xpcshell log. +// returns { file: (nsIFile), args: [] } +function wrapLaunchInShell(file, args) { + let ret = {}; + + if (AppConstants.platform === "win") { + ret.file = Services.dirsvc.get("WinD", Ci.nsIFile); + ret.file.append("System32"); + ret.file.append("cmd.exe"); + ret.args = ["/D", "/Q", "/C", file.path].concat(args).concat([">nul"]); + } else { + ret.file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + ret.file.initWithPath("/usr/bin/env"); + ret.args = [file.path].concat(args).concat(["> /dev/null"]); + } + + Assert.ok( + ret.file.exists(), + "Executable file should exist: " + ret.file.path + ); + + return ret; +} + +// Needed because process.kill() kills the console, not its child process, firefox. +function terminateFirefox(completion) { + let executableName = getFirefoxExecutableFilename(); + let file; + let args; + + if (AppConstants.platform === "win") { + file = Services.dirsvc.get("WinD", Ci.nsIFile); + file.append("System32"); + file.append("taskkill.exe"); + args = ["/F", "/IM", executableName]; + } else { + file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath("/usr/bin/killall"); + args = [executableName]; + } + + info("launching application: " + file.path); + info(" with args: " + args.join(" ")); + + let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); + process.init(file); + + let processObserver = { + observe: function PO_observe(aSubject, aTopic, aData) { + info("topic: " + aTopic + ", process exitValue: " + process.exitValue); + + Assert.equal( + process.exitValue, + 0, + "Terminate firefox process exit value should be 0" + ); + Assert.equal( + aTopic, + "process-finished", + "Terminate firefox observer topic should be process-finished" + ); + + if (completion) { + completion(); + } + }, + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + }; + + process.runAsync(args, args.length, processObserver); + + info(" with pid: " + process.pid); +} + +// Launches file with args asynchronously, failing if the process did not +// exit within timeoutMS milliseconds. If a timeout occurs, handler() +// is called. +function launchProcess(file, args, env, timeoutMS, handler, attemptCount) { + let state = {}; + + state.attempt = attemptCount; + + state.processObserver = { + observe: function PO_observe(aSubject, aTopic, aData) { + if (!state.appTimer) { + // the app timer has been canceled; this process has timed out already so don't process further. + handler(false); + return; + } + + info( + "topic: " + aTopic + ", process exitValue: " + state.process.exitValue + ); + + info("Restoring environment variables"); + setEnvironmentVariables(state.oldEnv); + + state.appTimer.cancel(); + state.appTimer = null; + + Assert.equal( + state.process.exitValue, + 0, + "the application process exit value should be 0" + ); + Assert.equal( + aTopic, + "process-finished", + "the application process observer topic should be process-finished" + ); + + handler(true); + }, + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + }; + + // The timer callback to kill the process if it takes too long. + state.appTimerCallback = { + notify: function TC_notify(aTimer) { + state.appTimer = null; + + info("Restoring environment variables"); + setEnvironmentVariables(state.oldEnv); + + if (state.process.isRunning) { + info("attempting to kill process"); + + // This will cause the shell process to exit as well, triggering our process observer. + terminateFirefox(function terminateFirefoxCompletion() { + Assert.ok(false, "Launch application timer expired"); + }); + } + }, + QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]), + }; + + info("launching application: " + file.path); + info(" with args: " + args.join(" ")); + info(" with environment: "); + for (let i = 0; i < env.length; ++i) { + info(" " + env[i].key + "=" + env[i].value); + } + + state.process = Cc["@mozilla.org/process/util;1"].createInstance( + Ci.nsIProcess + ); + state.process.init(file); + + state.appTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + state.appTimer.initWithCallback( + state.appTimerCallback, + timeoutMS, + Ci.nsITimer.TYPE_ONE_SHOT + ); + + state.oldEnv = setEnvironmentVariables(env); + + state.process.runAsync(args, args.length, state.processObserver); + + info(" with pid: " + state.process.pid); +} + +function run_test() { + do_test_pending(); + + let env = [ + { key: "MOZ_CRASHREPORTER_DISABLE", value: null }, + { key: "MOZ_CRASHREPORTER", value: "1" }, + { key: "MOZ_CRASHREPORTER_NO_REPORT", value: "1" }, + { key: "MOZ_CRASHREPORTER_SHUTDOWN", value: "1" }, + { key: "XPCOM_DEBUG_BREAK", value: "stack-and-abort" }, + ]; + + let triesStarted = 1; + + let handler = function launchFirefoxHandler(okToContinue) { + triesStarted++; + if (triesStarted <= TRY_COUNT && okToContinue) { + testTry(); + } else { + do_test_finished(); + } + }; + + let testTry = function testTry() { + let shell = wrapLaunchInShell(getFirefoxExecutableFile(), [ + "-no-remote", + "-test-launch-without-hang", + ]); + info("Try attempt #" + triesStarted); + launchProcess( + shell.file, + shell.args, + env, + APP_TIMER_TIMEOUT_MS, + handler, + triesStarted + ); + }; + + testTry(); +} diff --git a/toolkit/xre/test/win/Makefile.in b/toolkit/xre/test/win/Makefile.in new file mode 100644 index 0000000000..5a67b8eac8 --- /dev/null +++ b/toolkit/xre/test/win/Makefile.in @@ -0,0 +1,11 @@ +# 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/. + +MOZ_WINCONSOLE = 1 + +include $(topsrcdir)/config/rules.mk + +check:: + @echo 'Running TestXREMakeCommandLineWin tests' + @$(RUN_TEST_PROGRAM) $(FINAL_TARGET)/TestXREMakeCommandLineWin.exe diff --git a/toolkit/xre/test/win/TestLauncherRegistryInfo.cpp b/toolkit/xre/test/win/TestLauncherRegistryInfo.cpp new file mode 100644 index 0000000000..ab5edcf1ca --- /dev/null +++ b/toolkit/xre/test/win/TestLauncherRegistryInfo.cpp @@ -0,0 +1,783 @@ +/* -*- 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 "mozilla/LauncherRegistryInfo.h" +#include "mozilla/NativeNt.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Unused.h" +#include "nsWindowsHelpers.h" + +#include "LauncherRegistryInfo.cpp" + +#include <string> + +static const char kMsgStart[] = "TEST-FAILED | LauncherRegistryInfo | "; + +static const wchar_t kRegKeyPath[] = L"SOFTWARE\\" EXPAND_STRING_MACRO( + MOZ_APP_VENDOR) L"\\" EXPAND_STRING_MACRO(MOZ_APP_BASENAME) L"\\Launcher"; +static const wchar_t kBrowserSuffix[] = L"|Browser"; +static const wchar_t kLauncherSuffix[] = L"|Launcher"; +static const wchar_t kImageSuffix[] = L"|Image"; +static const wchar_t kTelemetrySuffix[] = L"|Telemetry"; + +static std::wstring gBrowserValue; +static std::wstring gLauncherValue; +static std::wstring gImageValue; +static std::wstring gTelemetryValue; + +static DWORD gMyImageTimestamp; + +#define RUN_TEST(result, fn) \ + if ((result = fn()).isErr()) { \ + const mozilla::LauncherError& err = result.inspectErr(); \ + printf("%s%s | %08lx (%s:%d)\n", kMsgStart, #fn, err.mError.AsHResult(), \ + err.mFile, err.mLine); \ + return 1; \ + } + +#define EXPECT_COMMIT_IS_OK() \ + do { \ + mozilla::LauncherVoidResult vr2 = info.Commit(); \ + if (vr2.isErr()) { \ + return vr2; \ + } \ + } while (0) + +#define EXPECT_CHECK_RESULT_IS(desired, expected) \ + do { \ + mozilla::LauncherResult<mozilla::LauncherRegistryInfo::ProcessType> \ + result = info.Check(mozilla::LauncherRegistryInfo::desired); \ + if (result.isErr()) { \ + return result.propagateErr(); \ + } \ + if (result.unwrap() != mozilla::LauncherRegistryInfo::expected) { \ + return LAUNCHER_ERROR_FROM_HRESULT(E_FAIL); \ + } \ + } while (0) + +#define EXPECT_ENABLED_STATE_IS(expected) \ + do { \ + mozilla::LauncherResult<mozilla::LauncherRegistryInfo::EnabledState> \ + enabled = info.IsEnabled(); \ + if (enabled.isErr()) { \ + return enabled.propagateErr(); \ + } \ + if (enabled.unwrap() != mozilla::LauncherRegistryInfo::expected) { \ + return LAUNCHER_ERROR_FROM_HRESULT(E_UNEXPECTED); \ + } \ + } while (0) + +#define EXPECT_TELEMETRY_IS_ENABLED(expected) \ + do { \ + mozilla::LauncherResult<bool> enabled = info.IsTelemetryEnabled(); \ + if (enabled.isErr()) { \ + return enabled.propagateErr(); \ + } \ + if (enabled.unwrap() != expected) { \ + return LAUNCHER_ERROR_FROM_HRESULT(E_UNEXPECTED); \ + } \ + } while (0) + +#define EXPECT_REG_DWORD_EXISTS_AND_EQ(name, expected) \ + do { \ + mozilla::LauncherResult<mozilla::Maybe<DWORD>> result = \ + ReadRegistryValueData<DWORD>(name, REG_DWORD); \ + if (result.isErr()) { \ + return result.propagateErr(); \ + } \ + if (result.inspect().isNothing() || \ + result.inspect().value() != expected) { \ + return LAUNCHER_ERROR_FROM_HRESULT(E_UNEXPECTED); \ + } \ + } while (0) + +#define EXPECT_REG_QWORD_EXISTS(name) \ + do { \ + mozilla::LauncherResult<mozilla::Maybe<uint64_t>> result = \ + ReadRegistryValueData<uint64_t>(name, REG_QWORD); \ + if (result.isErr()) { \ + return result.propagateErr(); \ + } \ + if (result.inspect().isNothing()) { \ + return LAUNCHER_ERROR_FROM_HRESULT(E_UNEXPECTED); \ + } \ + } while (0) + +#define EXPECT_REG_QWORD_EXISTS_AND_EQ(name, expected) \ + do { \ + mozilla::LauncherResult<mozilla::Maybe<uint64_t>> result = \ + ReadRegistryValueData<uint64_t>(name, REG_QWORD); \ + if (result.isErr()) { \ + return result.propagateErr(); \ + } \ + if (result.inspect().isNothing() || \ + result.inspect().value() != expected) { \ + return LAUNCHER_ERROR_FROM_HRESULT(E_UNEXPECTED); \ + } \ + } while (0) + +#define EXPECT_REG_DWORD_DOES_NOT_EXIST(name) \ + do { \ + mozilla::LauncherResult<mozilla::Maybe<DWORD>> result = \ + ReadRegistryValueData<DWORD>(name, REG_DWORD); \ + if (result.isErr()) { \ + return result.propagateErr(); \ + } \ + if (result.inspect().isSome()) { \ + return LAUNCHER_ERROR_FROM_HRESULT(E_UNEXPECTED); \ + } \ + } while (0) + +#define EXPECT_REG_QWORD_DOES_NOT_EXIST(name) \ + do { \ + mozilla::LauncherResult<mozilla::Maybe<uint64_t>> result = \ + ReadRegistryValueData<uint64_t>(name, REG_QWORD); \ + if (result.isErr()) { \ + return result.propagateErr(); \ + } \ + if (result.inspect().isSome()) { \ + return LAUNCHER_ERROR_FROM_HRESULT(E_UNEXPECTED); \ + } \ + } while (0) + +template <typename T> +static mozilla::LauncherResult<mozilla::Maybe<T>> ReadRegistryValueData( + const std::wstring& name, DWORD expectedType) { + T data; + DWORD dataLen = sizeof(data); + DWORD type; + LSTATUS status = ::RegGetValueW(HKEY_CURRENT_USER, kRegKeyPath, name.c_str(), + RRF_RT_ANY, &type, &data, &dataLen); + if (status == ERROR_FILE_NOT_FOUND) { + return mozilla::Maybe<T>(); + } + + if (status != ERROR_SUCCESS) { + return LAUNCHER_ERROR_FROM_WIN32(status); + } + + if (type != expectedType) { + return LAUNCHER_ERROR_FROM_WIN32(ERROR_DATATYPE_MISMATCH); + } + + return mozilla::Some(data); +} + +template <typename T> +static mozilla::LauncherVoidResult WriteRegistryValueData( + const std::wstring& name, DWORD type, T data) { + LSTATUS status = ::RegSetKeyValueW(HKEY_CURRENT_USER, kRegKeyPath, + name.c_str(), type, &data, sizeof(T)); + if (status != ERROR_SUCCESS) { + return LAUNCHER_ERROR_FROM_WIN32(status); + } + + return mozilla::Ok(); +} + +static mozilla::LauncherVoidResult DeleteRegistryValueData( + const std::wstring& name) { + LSTATUS status = + ::RegDeleteKeyValueW(HKEY_CURRENT_USER, kRegKeyPath, name.c_str()); + if (status == ERROR_SUCCESS || status == ERROR_FILE_NOT_FOUND) { + return mozilla::Ok(); + } + + return LAUNCHER_ERROR_FROM_WIN32(status); +} + +static mozilla::LauncherVoidResult DeleteAllRegstryValues() { + // Unblock commit via ReflectPrefToRegistry + // (We need to set false, and then true to bypass the early return) + mozilla::LauncherRegistryInfo info; + mozilla::LauncherVoidResult vr = info.ReflectPrefToRegistry(false); + vr = info.ReflectPrefToRegistry(true); + if (vr.isErr()) { + return vr; + } + + vr = DeleteRegistryValueData(gImageValue); + if (vr.isErr()) { + return vr; + } + + vr = DeleteRegistryValueData(gLauncherValue); + if (vr.isErr()) { + return vr; + } + + vr = DeleteRegistryValueData(gBrowserValue); + if (vr.isErr()) { + return vr; + } + + return DeleteRegistryValueData(gTelemetryValue); +} + +bool GetInstallHash(const char16_t*, mozilla::UniquePtr<NS_tchar[]>& result) { + return true; +} + +static mozilla::LauncherVoidResult SetupEnabledScenario() { + // Reset the registry state to an enabled state. First, we delete all existing + // registry values (if any). + mozilla::LauncherVoidResult vr = DeleteAllRegstryValues(); + if (vr.isErr()) { + return vr; + } + + // Now we run Check(Launcher)... + mozilla::LauncherRegistryInfo info; + EXPECT_CHECK_RESULT_IS(ProcessType::Launcher, ProcessType::Launcher); + EXPECT_COMMIT_IS_OK(); + // ...and Check(Browser) + EXPECT_CHECK_RESULT_IS(ProcessType::Browser, ProcessType::Browser); + EXPECT_COMMIT_IS_OK(); + + // By this point we are considered to be fully enabled. + return mozilla::Ok(); +} + +static mozilla::LauncherVoidResult TestEmptyRegistry() { + mozilla::LauncherVoidResult vr = DeleteAllRegstryValues(); + if (vr.isErr()) { + return vr; + } + + mozilla::LauncherRegistryInfo info; + EXPECT_CHECK_RESULT_IS(ProcessType::Launcher, ProcessType::Launcher); + EXPECT_COMMIT_IS_OK(); + + // LauncherRegistryInfo should have created Launcher and Image values + EXPECT_REG_DWORD_EXISTS_AND_EQ(gImageValue, gMyImageTimestamp); + EXPECT_REG_QWORD_EXISTS(gLauncherValue); + EXPECT_REG_QWORD_DOES_NOT_EXIST(gBrowserValue); + + EXPECT_ENABLED_STATE_IS(EnabledState::FailDisabled); + + return mozilla::Ok(); +} + +static mozilla::LauncherVoidResult TestNormal() { + mozilla::LauncherVoidResult vr = DeleteAllRegstryValues(); + if (vr.isErr()) { + return vr; + } + vr = WriteRegistryValueData<DWORD>(gImageValue, REG_DWORD, gMyImageTimestamp); + if (vr.isErr()) { + return vr; + } + vr = WriteRegistryValueData<uint64_t>(gLauncherValue, REG_QWORD, QPCNowRaw()); + if (vr.isErr()) { + return vr; + } + + mozilla::LauncherRegistryInfo info; + EXPECT_CHECK_RESULT_IS(ProcessType::Browser, ProcessType::Browser); + EXPECT_COMMIT_IS_OK(); + + // Make sure the browser timestamp is newer than the launcher's + mozilla::LauncherResult<mozilla::Maybe<uint64_t>> launcherTs = + ReadRegistryValueData<uint64_t>(gLauncherValue, REG_QWORD); + if (launcherTs.isErr()) { + return launcherTs.propagateErr(); + } + mozilla::LauncherResult<mozilla::Maybe<uint64_t>> browserTs = + ReadRegistryValueData<uint64_t>(gBrowserValue, REG_QWORD); + if (browserTs.isErr()) { + return browserTs.propagateErr(); + } + if (launcherTs.inspect().isNothing() || browserTs.inspect().isNothing() || + browserTs.inspect().value() <= launcherTs.inspect().value()) { + return LAUNCHER_ERROR_FROM_HRESULT(E_FAIL); + } + + EXPECT_ENABLED_STATE_IS(EnabledState::Enabled); + + return mozilla::Ok(); +} + +static mozilla::LauncherVoidResult TestBrowserNoLauncher() { + mozilla::LauncherVoidResult vr = SetupEnabledScenario(); + if (vr.isErr()) { + return vr; + } + vr = DeleteRegistryValueData(gLauncherValue); + if (vr.isErr()) { + return vr; + } + + mozilla::LauncherRegistryInfo info; + EXPECT_CHECK_RESULT_IS(ProcessType::Launcher, ProcessType::Browser); + EXPECT_COMMIT_IS_OK(); + + // Verify that we still don't have a launcher timestamp + EXPECT_REG_QWORD_DOES_NOT_EXIST(gLauncherValue); + // Verify that the browser timestamp is now zero + EXPECT_REG_QWORD_EXISTS_AND_EQ(gBrowserValue, 0ULL); + + EXPECT_ENABLED_STATE_IS(EnabledState::ForceDisabled); + + return mozilla::Ok(); +} + +static mozilla::LauncherVoidResult TestLauncherNoBrowser() { + constexpr uint64_t launcherTs = 0x77777777; + mozilla::LauncherVoidResult vr = DeleteAllRegstryValues(); + if (vr.isErr()) { + return vr; + } + vr = WriteRegistryValueData<DWORD>(gImageValue, REG_DWORD, gMyImageTimestamp); + if (vr.isErr()) { + return vr; + } + vr = WriteRegistryValueData<uint64_t>(gLauncherValue, REG_QWORD, launcherTs); + if (vr.isErr()) { + return vr; + } + + mozilla::LauncherRegistryInfo info; + EXPECT_CHECK_RESULT_IS(ProcessType::Launcher, ProcessType::Browser); + EXPECT_COMMIT_IS_OK(); + + // Launcher's timestamps is kept intact while browser's is set to 0. + EXPECT_REG_QWORD_EXISTS_AND_EQ(gLauncherValue, launcherTs); + EXPECT_REG_QWORD_EXISTS_AND_EQ(gBrowserValue, 0ULL); + + EXPECT_ENABLED_STATE_IS(EnabledState::FailDisabled); + + return mozilla::Ok(); +} + +static mozilla::LauncherVoidResult TestBrowserLessThanLauncher() { + constexpr uint64_t launcherTs = 0x77777777, browserTs = 0x66666666; + mozilla::LauncherVoidResult vr = DeleteAllRegstryValues(); + if (vr.isErr()) { + return vr; + } + vr = WriteRegistryValueData<DWORD>(gImageValue, REG_DWORD, gMyImageTimestamp); + if (vr.isErr()) { + return vr; + } + vr = WriteRegistryValueData<uint64_t>(gLauncherValue, REG_QWORD, launcherTs); + if (vr.isErr()) { + return vr; + } + vr = WriteRegistryValueData<uint64_t>(gBrowserValue, REG_QWORD, browserTs); + if (vr.isErr()) { + return vr; + } + + mozilla::LauncherRegistryInfo info; + EXPECT_CHECK_RESULT_IS(ProcessType::Launcher, ProcessType::Browser); + EXPECT_COMMIT_IS_OK(); + + // Launcher's timestamps is kept intact while browser's is set to 0. + EXPECT_REG_QWORD_EXISTS_AND_EQ(gLauncherValue, launcherTs); + EXPECT_REG_QWORD_EXISTS_AND_EQ(gBrowserValue, 0ULL); + + EXPECT_ENABLED_STATE_IS(EnabledState::FailDisabled); + + return mozilla::Ok(); +} + +static mozilla::LauncherVoidResult TestImageTimestampChange() { + // This should reset the timestamps and then essentially run like + // TestEmptyRegistry + mozilla::LauncherVoidResult vr = DeleteAllRegstryValues(); + if (vr.isErr()) { + return vr; + } + vr = WriteRegistryValueData<DWORD>(gImageValue, REG_DWORD, 0x12345678); + if (vr.isErr()) { + return vr; + } + vr = WriteRegistryValueData<uint64_t>(gLauncherValue, REG_QWORD, 1ULL); + if (vr.isErr()) { + return vr; + } + vr = WriteRegistryValueData<uint64_t>(gBrowserValue, REG_QWORD, 2ULL); + if (vr.isErr()) { + return vr; + } + + mozilla::LauncherRegistryInfo info; + EXPECT_CHECK_RESULT_IS(ProcessType::Launcher, ProcessType::Launcher); + EXPECT_COMMIT_IS_OK(); + + EXPECT_REG_DWORD_EXISTS_AND_EQ(gImageValue, gMyImageTimestamp); + EXPECT_REG_QWORD_EXISTS(gLauncherValue); + EXPECT_REG_QWORD_DOES_NOT_EXIST(gBrowserValue); + + return mozilla::Ok(); +} + +static mozilla::LauncherVoidResult TestImageTimestampChangeWhenDisabled() { + mozilla::LauncherVoidResult vr = DeleteAllRegstryValues(); + if (vr.isErr()) { + return vr; + } + vr = WriteRegistryValueData<DWORD>(gImageValue, REG_DWORD, 0x12345678); + if (vr.isErr()) { + return vr; + } + vr = WriteRegistryValueData<uint64_t>(gBrowserValue, REG_QWORD, 0ULL); + if (vr.isErr()) { + return vr; + } + + mozilla::LauncherRegistryInfo info; + EXPECT_CHECK_RESULT_IS(ProcessType::Launcher, ProcessType::Browser); + EXPECT_COMMIT_IS_OK(); + + EXPECT_REG_DWORD_EXISTS_AND_EQ(gImageValue, gMyImageTimestamp); + EXPECT_REG_QWORD_DOES_NOT_EXIST(gLauncherValue); + EXPECT_REG_QWORD_EXISTS_AND_EQ(gBrowserValue, 0); + + EXPECT_ENABLED_STATE_IS(EnabledState::ForceDisabled); + + return mozilla::Ok(); +} + +static mozilla::LauncherVoidResult TestDisableDueToFailure() { + mozilla::LauncherVoidResult vr = SetupEnabledScenario(); + if (vr.isErr()) { + return vr; + } + + // Check that we are indeed enabled. + mozilla::LauncherRegistryInfo info; + EXPECT_ENABLED_STATE_IS(EnabledState::Enabled); + + // Now call DisableDueToFailure + mozilla::LauncherVoidResult lvr = info.DisableDueToFailure(); + if (lvr.isErr()) { + return lvr.propagateErr(); + } + + // We should now be FailDisabled + EXPECT_ENABLED_STATE_IS(EnabledState::FailDisabled); + + // If we delete the launcher timestamp, IsEnabled should then return + // ForceDisabled. + vr = DeleteRegistryValueData(gLauncherValue); + if (vr.isErr()) { + return vr; + } + EXPECT_ENABLED_STATE_IS(EnabledState::ForceDisabled); + + return mozilla::Ok(); +} + +static mozilla::LauncherVoidResult TestPrefReflection() { + // Reset the registry to a known good state. + mozilla::LauncherVoidResult vr = SetupEnabledScenario(); + if (vr.isErr()) { + return vr; + } + + // Let's see what happens when we flip the pref to OFF. + mozilla::LauncherRegistryInfo info; + mozilla::LauncherVoidResult reflectOk = info.ReflectPrefToRegistry(false); + if (reflectOk.isErr()) { + return reflectOk.propagateErr(); + } + + // Launcher timestamp should be non-existent. + EXPECT_REG_QWORD_DOES_NOT_EXIST(gLauncherValue); + // Browser timestamp should be zero + EXPECT_REG_QWORD_EXISTS_AND_EQ(gBrowserValue, 0ULL); + // IsEnabled should give us ForceDisabled + EXPECT_ENABLED_STATE_IS(EnabledState::ForceDisabled); + + // Now test to see what happens when the pref is set to ON. + reflectOk = info.ReflectPrefToRegistry(true); + if (reflectOk.isErr()) { + return reflectOk.propagateErr(); + } + + // Launcher and browser timestamps should be non-existent. + EXPECT_REG_QWORD_DOES_NOT_EXIST(gLauncherValue); + EXPECT_REG_QWORD_DOES_NOT_EXIST(gBrowserValue); + + // IsEnabled should give us Enabled. + EXPECT_ENABLED_STATE_IS(EnabledState::Enabled); + + return mozilla::Ok(); +} + +static mozilla::LauncherVoidResult TestTelemetryConfig() { + mozilla::LauncherVoidResult vr = DeleteAllRegstryValues(); + if (vr.isErr()) { + return vr; + } + + mozilla::LauncherRegistryInfo info; + EXPECT_TELEMETRY_IS_ENABLED(false); + + mozilla::LauncherVoidResult reflectOk = + info.ReflectTelemetryPrefToRegistry(false); + if (reflectOk.isErr()) { + return reflectOk.propagateErr(); + } + EXPECT_TELEMETRY_IS_ENABLED(false); + + reflectOk = info.ReflectTelemetryPrefToRegistry(true); + if (reflectOk.isErr()) { + return reflectOk.propagateErr(); + } + EXPECT_TELEMETRY_IS_ENABLED(true); + + return mozilla::Ok(); +} + +static mozilla::LauncherVoidResult TestCommitAbort() { + mozilla::LauncherVoidResult vr = SetupEnabledScenario(); + if (vr.isErr()) { + return vr; + } + + // Retrieve the current timestamps to compare later + mozilla::LauncherResult<mozilla::Maybe<uint64_t>> launcherValue = + ReadRegistryValueData<uint64_t>(gLauncherValue, REG_QWORD); + if (launcherValue.isErr() || launcherValue.inspect().isNothing()) { + return launcherValue.propagateErr(); + } + mozilla::LauncherResult<mozilla::Maybe<uint64_t>> browserValue = + ReadRegistryValueData<uint64_t>(gBrowserValue, REG_QWORD); + if (browserValue.isErr() || browserValue.inspect().isNothing()) { + return browserValue.propagateErr(); + } + uint64_t launcherTs = launcherValue.inspect().value(); + uint64_t browserTs = browserValue.inspect().value(); + + vr = []() -> mozilla::LauncherVoidResult { + mozilla::LauncherRegistryInfo info; + EXPECT_CHECK_RESULT_IS(ProcessType::Launcher, ProcessType::Launcher); + // No commit + return mozilla::Ok(); + }(); + if (vr.isErr()) { + return vr; + } + + // Exiting the scope discards the change. + mozilla::LauncherRegistryInfo info; + EXPECT_REG_DWORD_EXISTS_AND_EQ(gImageValue, gMyImageTimestamp); + EXPECT_REG_QWORD_EXISTS_AND_EQ(gLauncherValue, launcherTs); + EXPECT_REG_QWORD_EXISTS_AND_EQ(gBrowserValue, browserTs); + + // Commit -> Check -> Abort -> Commit + EXPECT_COMMIT_IS_OK(); + EXPECT_CHECK_RESULT_IS(ProcessType::Launcher, ProcessType::Launcher); + info.Abort(); + EXPECT_COMMIT_IS_OK(); + + // Nothing is changed. + EXPECT_REG_DWORD_EXISTS_AND_EQ(gImageValue, gMyImageTimestamp); + EXPECT_REG_QWORD_EXISTS_AND_EQ(gLauncherValue, launcherTs); + EXPECT_REG_QWORD_EXISTS_AND_EQ(gBrowserValue, browserTs); + EXPECT_ENABLED_STATE_IS(EnabledState::Enabled); + + return mozilla::Ok(); +} + +static mozilla::LauncherVoidResult TestDisableDuringLauncherLaunch() { + mozilla::LauncherVoidResult vr = SetupEnabledScenario(); + if (vr.isErr()) { + return vr; + } + + mozilla::LauncherResult<mozilla::Maybe<uint64_t>> launcherTs = + ReadRegistryValueData<uint64_t>(gLauncherValue, REG_QWORD); + if (launcherTs.isErr()) { + return launcherTs.propagateErr(); + } + if (launcherTs.inspect().isNothing()) { + return LAUNCHER_ERROR_FROM_HRESULT(E_UNEXPECTED); + } + + vr = []() -> mozilla::LauncherVoidResult { + mozilla::LauncherRegistryInfo info; + EXPECT_CHECK_RESULT_IS(ProcessType::Launcher, ProcessType::Launcher); + + // Call DisableDueToFailure with a different instance + mozilla::LauncherVoidResult vr = []() -> mozilla::LauncherVoidResult { + mozilla::LauncherRegistryInfo info; + mozilla::LauncherVoidResult vr = info.DisableDueToFailure(); + if (vr.isErr()) { + return vr.propagateErr(); + } + return mozilla::Ok(); + }(); + if (vr.isErr()) { + return vr; + } + + // Commit after disable. + EXPECT_COMMIT_IS_OK(); + + return mozilla::Ok(); + }(); + if (vr.isErr()) { + return vr; + } + + // Make sure we're still FailDisabled and the launcher's timestamp is not + // updated + mozilla::LauncherRegistryInfo info; + EXPECT_ENABLED_STATE_IS(EnabledState::FailDisabled); + EXPECT_REG_QWORD_EXISTS_AND_EQ(gLauncherValue, launcherTs.inspect().value()); + + return mozilla::Ok(); +} + +static mozilla::LauncherVoidResult TestDisableDuringBrowserLaunch() { + mozilla::LauncherVoidResult vr = SetupEnabledScenario(); + if (vr.isErr()) { + return vr; + } + + mozilla::LauncherRegistryInfo info; + EXPECT_CHECK_RESULT_IS(ProcessType::Launcher, ProcessType::Launcher); + EXPECT_COMMIT_IS_OK(); + + vr = []() -> mozilla::LauncherVoidResult { + mozilla::LauncherRegistryInfo info; + EXPECT_CHECK_RESULT_IS(ProcessType::Browser, ProcessType::Browser); + + // Call DisableDueToFailure with a different instance + mozilla::LauncherVoidResult vr = []() -> mozilla::LauncherVoidResult { + mozilla::LauncherRegistryInfo info; + mozilla::LauncherVoidResult vr = info.DisableDueToFailure(); + if (vr.isErr()) { + return vr.propagateErr(); + } + return mozilla::Ok(); + }(); + if (vr.isErr()) { + return vr; + } + + // Commit after disable. + EXPECT_COMMIT_IS_OK(); + + return mozilla::Ok(); + }(); + if (vr.isErr()) { + return vr; + } + + // Make sure we're still FailDisabled + EXPECT_ENABLED_STATE_IS(EnabledState::FailDisabled); + + return mozilla::Ok(); +} + +static mozilla::LauncherVoidResult TestReEnable() { + mozilla::LauncherVoidResult vr = SetupEnabledScenario(); + if (vr.isErr()) { + return vr; + } + + // Make FailDisabled + mozilla::LauncherRegistryInfo info; + vr = info.DisableDueToFailure(); + if (vr.isErr()) { + return vr.propagateErr(); + } + EXPECT_ENABLED_STATE_IS(EnabledState::FailDisabled); + + // Attempt to launch when FailDisabled: Still be FailDisabled + EXPECT_CHECK_RESULT_IS(ProcessType::Launcher, ProcessType::Browser); + EXPECT_COMMIT_IS_OK(); + EXPECT_ENABLED_STATE_IS(EnabledState::FailDisabled); + + // Change the timestamp + vr = WriteRegistryValueData<DWORD>(gImageValue, REG_DWORD, 0x12345678); + if (vr.isErr()) { + return vr; + } + + // Attempt to launch again: Launcher comes back + EXPECT_CHECK_RESULT_IS(ProcessType::Launcher, ProcessType::Launcher); + EXPECT_COMMIT_IS_OK(); + + // Make ForceDisabled + vr = info.ReflectPrefToRegistry(false); + if (vr.isErr()) { + return vr.propagateErr(); + } + EXPECT_ENABLED_STATE_IS(EnabledState::ForceDisabled); + + // Attempt to launch when ForceDisabled: Still be ForceDisabled + EXPECT_CHECK_RESULT_IS(ProcessType::Launcher, ProcessType::Browser); + EXPECT_COMMIT_IS_OK(); + EXPECT_ENABLED_STATE_IS(EnabledState::ForceDisabled); + + // Change the timestamp + vr = WriteRegistryValueData<DWORD>(gImageValue, REG_DWORD, 0x12345678); + if (vr.isErr()) { + return vr; + } + + // Attempt to launch again: Still be ForceDisabled + EXPECT_CHECK_RESULT_IS(ProcessType::Launcher, ProcessType::Browser); + EXPECT_COMMIT_IS_OK(); + EXPECT_ENABLED_STATE_IS(EnabledState::ForceDisabled); + + return mozilla::Ok(); +} + +int main(int argc, char* argv[]) { + auto fullPath = mozilla::GetFullBinaryPath(); + if (!fullPath) { + return 1; + } + + // Global setup for all tests + gBrowserValue = fullPath.get(); + gBrowserValue += kBrowserSuffix; + + gLauncherValue = fullPath.get(); + gLauncherValue += kLauncherSuffix; + + gImageValue = fullPath.get(); + gImageValue += kImageSuffix; + + gTelemetryValue = fullPath.get(); + gTelemetryValue += kTelemetrySuffix; + + mozilla::LauncherResult<DWORD> timestamp = 0; + RUN_TEST(timestamp, GetCurrentImageTimestamp); + gMyImageTimestamp = timestamp.unwrap(); + + auto onExit = mozilla::MakeScopeExit( + []() { mozilla::Unused << DeleteAllRegstryValues(); }); + + mozilla::LauncherVoidResult vr = mozilla::Ok(); + + // All testcases should call SetupEnabledScenario() or + // DeleteAllRegstryValues() to be order-independent + RUN_TEST(vr, TestEmptyRegistry); + RUN_TEST(vr, TestNormal); + RUN_TEST(vr, TestBrowserNoLauncher); + RUN_TEST(vr, TestLauncherNoBrowser); + RUN_TEST(vr, TestBrowserLessThanLauncher); + RUN_TEST(vr, TestImageTimestampChange); + RUN_TEST(vr, TestImageTimestampChangeWhenDisabled); + RUN_TEST(vr, TestDisableDueToFailure); + RUN_TEST(vr, TestPrefReflection); + RUN_TEST(vr, TestTelemetryConfig); + RUN_TEST(vr, TestCommitAbort); + RUN_TEST(vr, TestDisableDuringLauncherLaunch); + RUN_TEST(vr, TestDisableDuringBrowserLaunch); + RUN_TEST(vr, TestReEnable); + + return 0; +} diff --git a/toolkit/xre/test/win/TestXREMakeCommandLineWin.cpp b/toolkit/xre/test/win/TestXREMakeCommandLineWin.cpp new file mode 100644 index 0000000000..c12550c3d6 --- /dev/null +++ b/toolkit/xre/test/win/TestXREMakeCommandLineWin.cpp @@ -0,0 +1,266 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 <stdio.h> +#include <stdlib.h> +#include <windows.h> +// Support for _setmode +#include <fcntl.h> +#include <io.h> + +#include "nsWindowsRestart.cpp" + +// CommandLineToArgvW may return different values for argv[0] since it contains +// the path to the binary that was executed so we prepend an argument that is +// quoted with a space to prevent argv[1] being appended to argv[0]. +#define DUMMY_ARG1 L"\"arg 1\" " + +#ifndef MAXPATHLEN +# ifdef PATH_MAX +# define MAXPATHLEN PATH_MAX +# elif defined(MAX_PATH) +# define MAXPATHLEN MAX_PATH +# elif defined(_MAX_PATH) +# define MAXPATHLEN _MAX_PATH +# elif defined(CCHMAXPATH) +# define MAXPATHLEN CCHMAXPATH +# else +# define MAXPATHLEN 1024 +# endif +#endif + +#define TEST_NAME L"XRE MakeCommandLine" +#define MAX_TESTS 100 + +// Verbose output can be enabled by defining VERBOSE 1 +#define VERBOSE 0 + +// Compares compareCmdLine with the output of MakeCommandLine. This is +// accomplished by converting inCmdLine to an argument list with +// CommandLineToArgvW and converting it back to a command line with +// MakeCommandLine. +static int verifyCmdLineCreation(wchar_t* inCmdLine, wchar_t* compareCmdLine, + bool passes, int testNum) { + int rv = 0; + int i; + int inArgc; + int outArgc; + bool isEqual; + + // When debugging with command lines containing Unicode characters greater + // than 255 you can set the mode for stdout to Unicode so the console will + // receive the correct characters though it won't display them properly unless + // the console's font has been set to one that can display the characters. You + // can also redirect the console output to a file that has been saved as + // Unicode to view the characters. + // _setmode(_fileno(stdout), _O_WTEXT); + + // Prepend an additional argument to the command line. CommandLineToArgvW + // handles argv[0] differently than other arguments since argv[0] is the path + // to the binary being executed and MakeCommandLine only handles argv[1] and + // larger. + wchar_t* inCmdLineNew = (wchar_t*)malloc( + (wcslen(DUMMY_ARG1) + wcslen(inCmdLine) + 1) * sizeof(wchar_t)); + wcscpy(inCmdLineNew, DUMMY_ARG1); + wcscat(inCmdLineNew, inCmdLine); + LPWSTR* inArgv = CommandLineToArgvW(inCmdLineNew, &inArgc); + + auto outCmdLine = mozilla::MakeCommandLine(inArgc - 1, inArgv + 1); + wchar_t* outCmdLineNew = (wchar_t*)malloc( + (wcslen(DUMMY_ARG1) + wcslen(outCmdLine.get()) + 1) * sizeof(wchar_t)); + wcscpy(outCmdLineNew, DUMMY_ARG1); + wcscat(outCmdLineNew, outCmdLine.get()); + LPWSTR* outArgv = CommandLineToArgvW(outCmdLineNew, &outArgc); + + if (VERBOSE) { + wprintf(L"\n"); + wprintf(L"Verbose Output\n"); + wprintf(L"--------------\n"); + wprintf(L"Input command line : >%s<\n", inCmdLine); + wprintf(L"MakeComandLine output: >%s<\n", outCmdLine.get()); + wprintf(L"Expected command line: >%s<\n", compareCmdLine); + + wprintf(L"input argc : %d\n", inArgc - 1); + wprintf(L"output argc: %d\n", outArgc - 1); + + for (i = 1; i < inArgc; ++i) { + wprintf(L"input argv[%d] : >%s<\n", i - 1, inArgv[i]); + } + + for (i = 1; i < outArgc; ++i) { + wprintf(L"output argv[%d]: >%s<\n", i - 1, outArgv[i]); + } + wprintf(L"\n"); + } + + isEqual = (inArgc == outArgc); + if (!isEqual) { + wprintf(L"TEST-%s-FAIL | %s | ARGC Comparison (check %2d)\n", + passes ? L"UNEXPECTED" : L"KNOWN", TEST_NAME, testNum); + if (passes) { + rv = 1; + } + LocalFree(inArgv); + LocalFree(outArgv); + free(inCmdLineNew); + free(outCmdLineNew); + return rv; + } + + for (i = 1; i < inArgc; ++i) { + isEqual = (wcscmp(inArgv[i], outArgv[i]) == 0); + if (!isEqual) { + wprintf(L"TEST-%s-FAIL | %s | ARGV Comparison (check %2d)\n", + passes ? L"UNEXPECTED" : L"KNOWN", TEST_NAME, testNum); + if (passes) { + rv = 1; + } + LocalFree(inArgv); + LocalFree(outArgv); + free(inCmdLineNew); + free(outCmdLineNew); + return rv; + } + } + + isEqual = (wcscmp(outCmdLine.get(), compareCmdLine) == 0); + if (!isEqual) { + wprintf(L"TEST-%s-FAIL | %s | Command Line Comparison (check %2d)\n", + passes ? L"UNEXPECTED" : L"KNOWN", TEST_NAME, testNum); + if (passes) { + rv = 1; + } + LocalFree(inArgv); + LocalFree(outArgv); + free(inCmdLineNew); + free(outCmdLineNew); + return rv; + } + + if (rv == 0) { + if (passes) { + wprintf(L"TEST-PASS | %s | check %2d\n", TEST_NAME, testNum); + } else { + wprintf(L"TEST-UNEXPECTED-PASS | %s | check %2d\n", TEST_NAME, testNum); + rv = 1; + } + } + + LocalFree(inArgv); + LocalFree(outArgv); + free(inCmdLineNew); + free(outCmdLineNew); + return rv; +} + +int wmain(int argc, wchar_t* argv[]) { + int i; + int rv = 0; + + if (argc > 1 && (_wcsicmp(argv[1], L"-check-one") != 0 || argc != 3)) { + fwprintf(stderr, + L"Displays and validates output from MakeCommandLine.\n\n"); + fwprintf(stderr, L"Usage: %s -check-one <test number>\n\n", argv[0]); + fwprintf(stderr, + L" <test number>\tSpecifies the test number to run from the\n"); + fwprintf(stderr, L"\t\tTestXREMakeCommandLineWin.ini file.\n"); + return 255; + } + + wchar_t inifile[MAXPATHLEN]; + if (!::GetModuleFileNameW(0, inifile, MAXPATHLEN)) { + wprintf(L"TEST-UNEXPECTED-FAIL | %s | GetModuleFileNameW\n", TEST_NAME); + return 2; + } + + WCHAR* slash = wcsrchr(inifile, '\\'); + if (!slash) { + wprintf(L"TEST-UNEXPECTED-FAIL | %s | wcsrchr\n", TEST_NAME); + return 3; + } + + wcscpy(slash + 1, L"TestXREMakeCommandLineWin.ini\0"); + + for (i = 0; i < MAX_TESTS; ++i) { + wchar_t sInputVal[MAXPATHLEN]; + wchar_t sOutputVal[MAXPATHLEN]; + wchar_t sPassesVal[MAXPATHLEN]; + wchar_t sInputKey[MAXPATHLEN]; + wchar_t sOutputKey[MAXPATHLEN]; + wchar_t sPassesKey[MAXPATHLEN]; + + if (argc > 2 && _wcsicmp(argv[1], L"-check-one") == 0 && argc == 3) { + i = _wtoi(argv[2]); + } + + _snwprintf(sInputKey, MAXPATHLEN, L"input_%d", i); + _snwprintf(sOutputKey, MAXPATHLEN, L"output_%d", i); + _snwprintf(sPassesKey, MAXPATHLEN, L"passes_%d", i); + + if (!GetPrivateProfileStringW(L"MakeCommandLineTests", sInputKey, nullptr, + sInputVal, MAXPATHLEN, inifile)) { + if (i == 0 || (argc > 2 && _wcsicmp(argv[1], L"-check-one") == 0)) { + wprintf(L"TEST-UNEXPECTED-FAIL | %s | see following explanation:\n", + TEST_NAME); + wprintf( + L"ERROR: Either the TestXREMakeCommandLineWin.ini file doesn't " + L"exist\n"); + if (argc > 1 && _wcsicmp(argv[1], L"-check-one") == 0 && argc == 3) { + wprintf( + L"ERROR: or the test is not defined in the MakeCommandLineTests " + L"section.\n"); + } else { + wprintf( + L"ERROR: or it has no tests defined in the MakeCommandLineTests " + L"section.\n"); + } + wprintf(L"ERROR: File: %s\n", inifile); + return 4; + } + break; + } + + GetPrivateProfileStringW(L"MakeCommandLineTests", sOutputKey, nullptr, + sOutputVal, MAXPATHLEN, inifile); + GetPrivateProfileStringW(L"MakeCommandLineTests", sPassesKey, nullptr, + sPassesVal, MAXPATHLEN, inifile); + + rv |= verifyCmdLineCreation( + sInputVal, sOutputVal, + (_wcsicmp(sPassesVal, L"false") == 0) ? FALSE : TRUE, i); + + if (argc > 2 && _wcsicmp(argv[1], L"-check-one") == 0) { + break; + } + } + + if (rv == 0) { + wprintf(L"TEST-PASS | %s | all checks passed\n", TEST_NAME); + } else { + wprintf(L"TEST-UNEXPECTED-FAIL | %s | some checks failed\n", TEST_NAME); + } + + return rv; +} + +#ifdef __MINGW32__ + +/* MingW currently does not implement a wide version of the + startup routines. Workaround is to implement something like + it ourselves. See bug 411826 */ + +# include <shellapi.h> + +int main(int argc, char** argv) { + LPWSTR commandLine = GetCommandLineW(); + int argcw = 0; + LPWSTR* argvw = CommandLineToArgvW(commandLine, &argcw); + if (!argvw) return 127; + + int result = wmain(argcw, argvw); + LocalFree(argvw); + return result; +} +#endif /* __MINGW32__ */ diff --git a/toolkit/xre/test/win/TestXREMakeCommandLineWin.ini b/toolkit/xre/test/win/TestXREMakeCommandLineWin.ini new file mode 100644 index 0000000000..dbb529d1b1 --- /dev/null +++ b/toolkit/xre/test/win/TestXREMakeCommandLineWin.ini @@ -0,0 +1,94 @@ +; A typical MakeCommandLine test will contain an input and an output name value +; pair. The value for input_xx is the input command line and the value for +; output_xx is the expected output command line. +; +; A test that is known to fail can be added as follows. If the passes_xx name +; value pair doesn't exist it defaults to true. +; input_99=yabadaba +; output_99=doo +; passes_99=false +; +; If a value starts and ends with single or double quotation marks then it must +; be enclosed in single or double quotation marks due to GetPrivateProfileString +; discarding the outmost quotation marks. See GetPrivateProfileString on MSDN +; for more information. +; http://msdn.microsoft.com/en-us/library/ms724353.aspx + +[MakeCommandLineTests] +input_0=a:\ +output_0=a:\ + +input_1=""a:\"" +output_1=a:\" + +input_2=""a:\b c"" +output_2=""a:\b c"" + +input_3=""a:\b c\"" +output_3=""a:\b c\""" + +input_4=""a:\b c\d e"" +output_4=""a:\b c\d e"" + +input_5=""a:\b c\d e\"" +output_5=""a:\b c\d e\""" + +input_6=""a:\\"" +output_6=a:\ + +input_7="a:\" "b:\c d" +output_7=a:\" "b:\c d" + +input_8="a "b:\" "c:\d e"" +output_8="a "b:\" c:\d" e" + +input_9="abc" d e +output_9=abc d e + +input_10="a b c" d e +output_10="a b c" d e + +input_11=a\\\b d"e f"g h +output_11=a\\\b "de fg" h + +input_12=a b +output_12=a b + +input_13=""a b"" +output_13=""a b"" + +input_14=a\\\"b c d +output_14=a\\\"b c d + +input_15=a\\\"b c" +output_15=a\\\"b c + +input_16=""a\\\b c" +output_16=""a\\\b c"" + +input_17=\"a +output_17=\"a + +input_18=\\"a +output_18=\a + +input_19=\\"\\\\"a +output_19=\\\a + +input_20=\\"\\\\\"a +output_20=\\\\\\\"a + +input_21="a\\\"b c\" d e +output_21=""a\\\"b c\" d e"" + +input_22=a\\\\\"b c" d e" +output_22=a\\\\\"b "c d e" + +input_23=a:\b c\アルファ オメガ\d +output_23=a:\b c\アルファ オメガ\d + +input_24=a:\b "c\アルファ オメガ\d" +output_24=a:\b "c\アルファ オメガ\d" + +input_25=アルファ オメガ +output_25=アルファ オメガ diff --git a/toolkit/xre/test/win/mochitest/browser_env_path_long.ini b/toolkit/xre/test/win/mochitest/browser_env_path_long.ini new file mode 100644 index 0000000000..9eb5a7b6e4 --- /dev/null +++ b/toolkit/xre/test/win/mochitest/browser_env_path_long.ini @@ -0,0 +1,7 @@ +[DEFAULT] +# This path should expand to >32K characters. See bug 1753910. +environment = + PATH="C:/loremipsumdolorsitamet/adipiscingconsectetuerelit/;C:/loremipsumdolorsitamet/adipiscingconsectetuerelit/;C:/loremipsumdolorsitamet/adipiscingconsectetuerelit/;C:/loremipsumdolorsitamet/adipiscingconsectetuerelit/;C:/loremipsumdolorsitamet/adipiscingconsectetuerelit/;C:/loremipsumdolorsitamet/adipiscingconsectetuerelit/;C:/loremipsumdolorsitamet/adipiscingconsectetuerelit/;C:/loremipsumdolorsitamet/adipiscingconsectetuerelit/;C:/loremipsumdolorsitamet/adipiscingconsectetuerelit/;C:/loremipsumdolorsitamet/adipiscingconsectetuerelit/;C:/loremipsumdolorsitamet/adipiscingconsectetuerelit/;C:/loremipsumdolorsitamet/adipiscingconsectetuerelit/;C:/loremipsumdolorsitamet/adipiscingconsectetuerelit/;C:/loremipsumdolorsitamet/adipiscingconsectetuerelit/;C:/loremipsumdolorsitamet/adipiscingconsectetuerelit/;C:/loremipsumdolorsitamet/adipiscingconsectetuerelit/;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%;%PATH%" + +[browser_env_path_long.js] +skip-if = os != "win" diff --git a/toolkit/xre/test/win/mochitest/browser_env_path_long.js b/toolkit/xre/test/win/mochitest/browser_env_path_long.js new file mode 100644 index 0000000000..9bc63214b0 --- /dev/null +++ b/toolkit/xre/test/win/mochitest/browser_env_path_long.js @@ -0,0 +1,16 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +// Test that the browser starts even when PATH would expand to a detrimentally +// long value. +add_task(async function test() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + function (browser) { + ok( + true, + "Browser should start even with potentially pathologically long PATH." + ); + } + ); +}); diff --git a/toolkit/xre/test/win/moz.build b/toolkit/xre/test/win/moz.build new file mode 100644 index 0000000000..8744f70698 --- /dev/null +++ b/toolkit/xre/test/win/moz.build @@ -0,0 +1,55 @@ +# -*- 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/. + +BROWSER_CHROME_MANIFESTS += [ + "mochitest/browser_env_path_long.ini", +] + +GeckoCppUnitTests( + [ + "TestXREMakeCommandLineWin", + ], + linkage=None, +) + +# This needs to be installed alongside the above unit test. +FINAL_TARGET_FILES += [ + "TestXREMakeCommandLineWin.ini", +] + +DEFINES["NS_NO_XPCOM"] = True + +LOCAL_INCLUDES += [ + "/config", + "/toolkit/xre", +] + +DisableStlWrapping() +USE_STATIC_LIBS = True + +OS_LIBS += [ + "advapi32", + "comctl32", + "ole32", + "shell32", + "uuid", + "userenv", + "ws2_32", +] + +if CONFIG["MOZ_LAUNCHER_PROCESS"]: + LOCAL_INCLUDES += [ + "/toolkit/mozapps/update/common", + ] + GeckoCppUnitTests( + [ + "TestLauncherRegistryInfo", + ], + linkage=None, + ) + # Needed for TestLauncherRegistryInfo + for var in ("MOZ_APP_BASENAME", "MOZ_APP_VENDOR"): + DEFINES[var] = '"%s"' % CONFIG[var] diff --git a/toolkit/xre/test/xpcshell.ini b/toolkit/xre/test/xpcshell.ini new file mode 100644 index 0000000000..acf84f715e --- /dev/null +++ b/toolkit/xre/test/xpcshell.ini @@ -0,0 +1,19 @@ +# 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/. + +[DEFAULT] +tags = native + +[test_launch_without_hang.js] +run-sequentially = Has to launch application binary +skip-if = toolkit == 'android' +requesttimeoutfactor = 2 +[test_install_hash.js] +# Android doesn't ship Subprocess.sys.mjs and debug builds output garbage that the +# test cannot handle. +skip-if = + toolkit == 'android' + debug +support-files = + show_hash.js diff --git a/toolkit/xre/updaterfileutils_osx.h b/toolkit/xre/updaterfileutils_osx.h new file mode 100644 index 0000000000..1fa0db1f4e --- /dev/null +++ b/toolkit/xre/updaterfileutils_osx.h @@ -0,0 +1,13 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef updaterfileutils_osx_h_ +#define updaterfileutils_osx_h_ + +extern "C" { +bool IsRecursivelyWritable(const char* aPath); +} + +#endif diff --git a/toolkit/xre/updaterfileutils_osx.mm b/toolkit/xre/updaterfileutils_osx.mm new file mode 100644 index 0000000000..15a4b3a5ba --- /dev/null +++ b/toolkit/xre/updaterfileutils_osx.mm @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "updaterfileutils_osx.h" + +#include <Cocoa/Cocoa.h> + +bool IsRecursivelyWritable(const char* aPath) { + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + + NSString* rootPath = [NSString stringWithUTF8String:aPath]; + NSFileManager* fileManager = [NSFileManager defaultManager]; + NSError* error = nil; + NSArray* subPaths = [fileManager subpathsOfDirectoryAtPath:rootPath error:&error]; + NSMutableArray* paths = [NSMutableArray arrayWithCapacity:[subPaths count] + 1]; + [paths addObject:@""]; + [paths addObjectsFromArray:subPaths]; + + if (error) { + [pool drain]; + return false; + } + + for (NSString* currPath in paths) { + NSString* child = [rootPath stringByAppendingPathComponent:currPath]; + + NSDictionary* attributes = [fileManager attributesOfItemAtPath:child error:&error]; + if (error) { + [pool drain]; + return false; + } + + // Don't check for writability of files pointed to by symlinks, as they may + // not be descendants of the root path. + if ([attributes fileType] != NSFileTypeSymbolicLink && + [fileManager isWritableFileAtPath:child] == NO) { + [pool drain]; + return false; + } + } + + [pool drain]; + return true; +} |