From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- browser/app/winlauncher/DllBlocklistInit.cpp | 237 +++++++ browser/app/winlauncher/DllBlocklistInit.h | 32 + browser/app/winlauncher/ErrorHandler.cpp | 775 +++++++++++++++++++++ browser/app/winlauncher/ErrorHandler.h | 54 ++ browser/app/winlauncher/LaunchUnelevated.cpp | 290 ++++++++ browser/app/winlauncher/LaunchUnelevated.h | 32 + browser/app/winlauncher/LauncherProcessWin.cpp | 529 ++++++++++++++ browser/app/winlauncher/LauncherProcessWin.h | 70 ++ browser/app/winlauncher/NtLoaderAPI.cpp | 33 + browser/app/winlauncher/ProcThreadAttributes.h | 159 +++++ browser/app/winlauncher/SameBinary.h | 146 ++++ .../app/winlauncher/freestanding/CheckForCaller.h | 36 + .../app/winlauncher/freestanding/DllBlocklist.cpp | 620 +++++++++++++++++ .../app/winlauncher/freestanding/DllBlocklist.h | 39 ++ .../app/winlauncher/freestanding/Freestanding.h | 67 ++ .../winlauncher/freestanding/LoaderPrivateAPI.cpp | 314 +++++++++ .../winlauncher/freestanding/LoaderPrivateAPI.h | 62 ++ .../winlauncher/freestanding/ModuleLoadFrame.cpp | 144 ++++ .../app/winlauncher/freestanding/ModuleLoadFrame.h | 97 +++ .../app/winlauncher/freestanding/SafeThreadLocal.h | 96 +++ .../app/winlauncher/freestanding/SharedSection.cpp | 388 +++++++++++ .../app/winlauncher/freestanding/SharedSection.h | 212 ++++++ .../freestanding/gen_ntdll_freestanding_lib.py | 28 + browser/app/winlauncher/freestanding/moz.build | 56 ++ .../freestanding/ntdll_freestanding.def | 25 + browser/app/winlauncher/moz.build | 61 ++ .../app/winlauncher/test/TestCrossProcessWin.cpp | 715 +++++++++++++++++++ .../app/winlauncher/test/TestSafeThreadLocal.cpp | 84 +++ browser/app/winlauncher/test/TestSameBinary.cpp | 255 +++++++ browser/app/winlauncher/test/moz.build | 30 + 30 files changed, 5686 insertions(+) create mode 100644 browser/app/winlauncher/DllBlocklistInit.cpp create mode 100644 browser/app/winlauncher/DllBlocklistInit.h create mode 100644 browser/app/winlauncher/ErrorHandler.cpp create mode 100644 browser/app/winlauncher/ErrorHandler.h create mode 100644 browser/app/winlauncher/LaunchUnelevated.cpp create mode 100644 browser/app/winlauncher/LaunchUnelevated.h create mode 100644 browser/app/winlauncher/LauncherProcessWin.cpp create mode 100644 browser/app/winlauncher/LauncherProcessWin.h create mode 100644 browser/app/winlauncher/NtLoaderAPI.cpp create mode 100644 browser/app/winlauncher/ProcThreadAttributes.h create mode 100644 browser/app/winlauncher/SameBinary.h create mode 100644 browser/app/winlauncher/freestanding/CheckForCaller.h create mode 100644 browser/app/winlauncher/freestanding/DllBlocklist.cpp create mode 100644 browser/app/winlauncher/freestanding/DllBlocklist.h create mode 100644 browser/app/winlauncher/freestanding/Freestanding.h create mode 100644 browser/app/winlauncher/freestanding/LoaderPrivateAPI.cpp create mode 100644 browser/app/winlauncher/freestanding/LoaderPrivateAPI.h create mode 100644 browser/app/winlauncher/freestanding/ModuleLoadFrame.cpp create mode 100644 browser/app/winlauncher/freestanding/ModuleLoadFrame.h create mode 100644 browser/app/winlauncher/freestanding/SafeThreadLocal.h create mode 100644 browser/app/winlauncher/freestanding/SharedSection.cpp create mode 100644 browser/app/winlauncher/freestanding/SharedSection.h create mode 100644 browser/app/winlauncher/freestanding/gen_ntdll_freestanding_lib.py create mode 100644 browser/app/winlauncher/freestanding/moz.build create mode 100644 browser/app/winlauncher/freestanding/ntdll_freestanding.def create mode 100644 browser/app/winlauncher/moz.build create mode 100644 browser/app/winlauncher/test/TestCrossProcessWin.cpp create mode 100644 browser/app/winlauncher/test/TestSafeThreadLocal.cpp create mode 100644 browser/app/winlauncher/test/TestSameBinary.cpp create mode 100644 browser/app/winlauncher/test/moz.build (limited to 'browser/app/winlauncher') diff --git a/browser/app/winlauncher/DllBlocklistInit.cpp b/browser/app/winlauncher/DllBlocklistInit.cpp new file mode 100644 index 0000000000..fc4958339f --- /dev/null +++ b/browser/app/winlauncher/DllBlocklistInit.cpp @@ -0,0 +1,237 @@ +/* -*- 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 "nsWindowsDllInterceptor.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/BinarySearch.h" +#include "mozilla/ImportDir.h" +#include "mozilla/NativeNt.h" +#include "mozilla/PolicyChecks.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Types.h" +#include "mozilla/WindowsDllBlocklist.h" +#include "mozilla/WindowsStackCookie.h" +#include "mozilla/WinHeaderOnlyUtils.h" + +#include "DllBlocklistInit.h" +#include "freestanding/DllBlocklist.h" +#include "freestanding/SharedSection.h" + +namespace mozilla { + +#if defined(MOZ_ASAN) || defined(_M_ARM64) + +// This DLL blocking code is incompatible with ASAN because +// it is able to execute before ASAN itself has even initialized. +// Also, AArch64 has not been tested with this. +LauncherVoidResultWithLineInfo InitializeDllBlocklistOOP( + const wchar_t* aFullImagePath, HANDLE aChildProcess, + const IMAGE_THUNK_DATA*, const GeckoProcessType aProcessType) { + return mozilla::Ok(); +} + +LauncherVoidResultWithLineInfo InitializeDllBlocklistOOPFromLauncher( + const wchar_t* aFullImagePath, HANDLE aChildProcess, + const bool aDisableDynamicBlocklist, + Maybe aBlocklistFileName) { + return mozilla::Ok(); +} + +#else + +static LauncherVoidResultWithLineInfo InitializeDllBlocklistOOPInternal( + const wchar_t* aFullImagePath, nt::CrossExecTransferManager& aTransferMgr, + const IMAGE_THUNK_DATA* aCachedNtdllThunk, + const GeckoProcessType aProcessType) { + CrossProcessDllInterceptor intcpt(aTransferMgr.RemoteProcess()); + intcpt.Init(L"ntdll.dll"); + +# if defined(DEBUG) && defined(_M_X64) && !defined(__MINGW64__) + // This debug check preserves compatibility with third-parties (see bug + // 1733532). + MOZ_ASSERT(!HasStackCookieCheck( + reinterpret_cast(&freestanding::patched_NtMapViewOfSection))); +# endif // #if defined(DEBUG) && defined(_M_X64) && !defined(__MINGW64__) + + bool ok = freestanding::stub_NtMapViewOfSection.SetDetour( + aTransferMgr, intcpt, "NtMapViewOfSection", + &freestanding::patched_NtMapViewOfSection); + if (!ok) { + return LAUNCHER_ERROR_FROM_DETOUR_ERROR(intcpt.GetLastDetourError()); + } + + ok = freestanding::stub_LdrLoadDll.SetDetour( + aTransferMgr, intcpt, "LdrLoadDll", &freestanding::patched_LdrLoadDll); + if (!ok) { + return LAUNCHER_ERROR_FROM_DETOUR_ERROR(intcpt.GetLastDetourError()); + } + + // Because aChildProcess has just been created in a suspended state, its + // dynamic linker has not yet been initialized, thus its executable has + // not yet been linked with ntdll.dll. If the blocklist hook intercepts a + // library load prior to the link, the hook will be unable to invoke any + // ntdll.dll functions. + // + // We know that the executable for our *current* process's binary is already + // linked into ntdll, so we obtain the IAT from our own executable and graft + // it onto the child process's IAT, thus enabling the child process's hook to + // safely make its ntdll calls. + + const nt::PEHeaders& ourExeImage = aTransferMgr.LocalPEHeaders(); + + // As part of our mitigation of binary tampering, copy our import directory + // from the original in our executable file. + LauncherVoidResult importDirRestored = + RestoreImportDirectory(aFullImagePath, aTransferMgr); + if (importDirRestored.isErr()) { + return importDirRestored; + } + + mozilla::nt::PEHeaders ntdllImage(::GetModuleHandleW(L"ntdll.dll")); + if (!ntdllImage) { + return LAUNCHER_ERROR_FROM_WIN32(ERROR_BAD_EXE_FORMAT); + } + + // If we have a cached IAT i.e. |aCachedNtdllThunk| is non-null, we can + // safely copy it to |aChildProcess| even if the local IAT has been modified. + // If |aCachedNtdllThunk| is null, we've failed to cache the IAT or we're in + // the launcher process where there is no chance to cache the IAT. In those + // cases, we retrieve the IAT with the boundary check to avoid a modified IAT + // from being copied into |aChildProcess|. + Maybe > ntdllThunks; + if (aCachedNtdllThunk) { + ntdllThunks = ourExeImage.GetIATThunksForModule("ntdll.dll"); + } else { + Maybe > ntdllBoundaries = ntdllImage.GetBounds(); + if (!ntdllBoundaries) { + return LAUNCHER_ERROR_FROM_WIN32(ERROR_BAD_EXE_FORMAT); + } + + // We can use GetIATThunksForModule() to check whether IAT is modified + // or not because no functions exported from ntdll.dll is forwarded. + ntdllThunks = + ourExeImage.GetIATThunksForModule("ntdll.dll", ntdllBoundaries.ptr()); + } + if (!ntdllThunks) { + return LAUNCHER_ERROR_FROM_WIN32(ERROR_INVALID_DATA); + } + + { // Scope for prot + PIMAGE_THUNK_DATA firstIatThunkDst = ntdllThunks.value().data(); + const IMAGE_THUNK_DATA* firstIatThunkSrc = + aCachedNtdllThunk ? aCachedNtdllThunk : firstIatThunkDst; + SIZE_T iatLength = ntdllThunks.value().LengthBytes(); + + AutoVirtualProtect prot = + aTransferMgr.Protect(firstIatThunkDst, iatLength, PAGE_READWRITE); + if (!prot) { + return LAUNCHER_ERROR_FROM_MOZ_WINDOWS_ERROR(prot.GetError()); + } + + LauncherVoidResult writeResult = + aTransferMgr.Transfer(firstIatThunkDst, firstIatThunkSrc, iatLength); + if (writeResult.isErr()) { + return writeResult.propagateErr(); + } + } + + // Tell the mozglue blocklist that we have bootstrapped + uint32_t newFlags = eDllBlocklistInitFlagWasBootstrapped; + + if (gBlocklistInitFlags & eDllBlocklistInitFlagWasBootstrapped) { + // If we ourselves were bootstrapped, then we are starting a child process + // and need to set the appropriate flag. + newFlags |= eDllBlocklistInitFlagIsChildProcess; + } + + SetDllBlocklistProcessTypeFlags(newFlags, aProcessType); + + LauncherVoidResult writeResult = + aTransferMgr.Transfer(&gBlocklistInitFlags, &newFlags, sizeof(newFlags)); + if (writeResult.isErr()) { + return writeResult.propagateErr(); + } + + return Ok(); +} + +LauncherVoidResultWithLineInfo InitializeDllBlocklistOOP( + const wchar_t* aFullImagePath, HANDLE aChildProcess, + const IMAGE_THUNK_DATA* aCachedNtdllThunk, + const GeckoProcessType aProcessType) { + nt::CrossExecTransferManager transferMgr(aChildProcess); + if (!transferMgr) { + return LAUNCHER_ERROR_FROM_WIN32(ERROR_BAD_EXE_FORMAT); + } + + // We come here when the browser process launches a sandbox process. + // If the launcher process already failed to bootstrap the browser process, + // we should not attempt to bootstrap a child process because it's likely + // to fail again. Instead, we only restore the import directory entry. + if (!(gBlocklistInitFlags & eDllBlocklistInitFlagWasBootstrapped)) { + return RestoreImportDirectory(aFullImagePath, transferMgr); + } + + // Transfer a readonly handle to the child processes because all information + // are already written to the section by the launcher and main process. + LauncherVoidResult transferResult = + freestanding::gSharedSection.TransferHandle(transferMgr, GENERIC_READ); + if (transferResult.isErr()) { + return transferResult.propagateErr(); + } + + return InitializeDllBlocklistOOPInternal(aFullImagePath, transferMgr, + aCachedNtdllThunk, aProcessType); +} + +LauncherVoidResultWithLineInfo InitializeDllBlocklistOOPFromLauncher( + const wchar_t* aFullImagePath, HANDLE aChildProcess, + const bool aDisableDynamicBlocklist, + Maybe aBlocklistFileName) { + nt::CrossExecTransferManager transferMgr(aChildProcess); + if (!transferMgr) { + return LAUNCHER_ERROR_FROM_WIN32(ERROR_BAD_EXE_FORMAT); + } + + // The launcher process initializes a section object, whose handle is + // transferred to the browser process, and that transferred handle in + // the browser process is transferred to the sandbox processes. + LauncherVoidResultWithLineInfo result = freestanding::gSharedSection.Init(); + if (result.isErr()) { + return result; + } + + if (aBlocklistFileName.isSome() && + !PolicyCheckBoolean(L"DisableThirdPartyModuleBlocking")) { + DynamicBlockList blockList(aBlocklistFileName->c_str()); + result = freestanding::gSharedSection.SetBlocklist( + blockList, aDisableDynamicBlocklist); + if (result.isErr()) { + return result; + } + } + + // Transfer a writable handle to the main process because it needs to append + // dependent module paths to the section. + LauncherVoidResult transferResult = + freestanding::gSharedSection.TransferHandle(transferMgr, + GENERIC_READ | GENERIC_WRITE); + if (transferResult.isErr()) { + return transferResult.propagateErr(); + } + + auto clearInstance = MakeScopeExit([]() { + // After transfer, the launcher process does not need the object anymore. + freestanding::gSharedSection.Reset(nullptr); + }); + return InitializeDllBlocklistOOPInternal(aFullImagePath, transferMgr, nullptr, + GeckoProcessType_Default); +} + +#endif // defined(MOZ_ASAN) || defined(_M_ARM64) + +} // namespace mozilla diff --git a/browser/app/winlauncher/DllBlocklistInit.h b/browser/app/winlauncher/DllBlocklistInit.h new file mode 100644 index 0000000000..9e5e1a181c --- /dev/null +++ b/browser/app/winlauncher/DllBlocklistInit.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 https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_DllBlocklistInit_h +#define mozilla_DllBlocklistInit_h + +#include + +#if defined(MOZ_LAUNCHER_PROCESS) +# include "mozilla/LauncherRegistryInfo.h" +#endif +#include "mozilla/ProcessType.h" +#include "mozilla/WinHeaderOnlyUtils.h" + +namespace mozilla { + +LauncherVoidResultWithLineInfo InitializeDllBlocklistOOP( + const wchar_t* aFullImagePath, HANDLE aChildProcess, + const IMAGE_THUNK_DATA* aCachedNtdllThunk, + const GeckoProcessType aProcessType); + +LauncherVoidResultWithLineInfo InitializeDllBlocklistOOPFromLauncher( + const wchar_t* aFullImagePath, HANDLE aChildProcess, + const bool aDisableDynamicBlocklist, + Maybe aBlocklistFileName); + +} // namespace mozilla + +#endif // mozilla_DllBlocklistInit_h diff --git a/browser/app/winlauncher/ErrorHandler.cpp b/browser/app/winlauncher/ErrorHandler.cpp new file mode 100644 index 0000000000..5b2582284e --- /dev/null +++ b/browser/app/winlauncher/ErrorHandler.cpp @@ -0,0 +1,775 @@ +/* -*- 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 "ErrorHandler.h" + +#include + +#include "mozilla/ArrayUtils.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/JSONWriter.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/WinTokenUtils.h" +#include "mozilla/XREAppData.h" +#include "mozilla/glue/WindowsDllServices.h" +#include "mozilla/mscom/ProcessRuntime.h" +#include "nsWindowsHelpers.h" + +#if defined(MOZ_LAUNCHER_PROCESS) +# include "mozilla/LauncherRegistryInfo.h" +#endif // defined(MOZ_LAUNCHER_PROCESS) + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#if !defined(__MINGW32__) +# include +# include +# include +#endif // !defined(__MINGW32__) +#include + +#if !defined(RRF_SUBKEY_WOW6464KEY) +# define RRF_SUBKEY_WOW6464KEY 0x00010000 +#endif // !defined(RRF_SUBKEY_WOW6464KEY) + +#define QUOTE_ME2(x) #x +#define QUOTE_ME(x) QUOTE_ME2(x) + +#define TELEMETRY_BASE_URL L"https://incoming.telemetry.mozilla.org/submit" +#define TELEMETRY_NAMESPACE L"/firefox-launcher-process" +#define TELEMETRY_LAUNCHER_PING_DOCTYPE L"/launcher-process-failure" +#define TELEMETRY_LAUNCHER_PING_VERSION L"/1" + +static const wchar_t kUrl[] = TELEMETRY_BASE_URL TELEMETRY_NAMESPACE + TELEMETRY_LAUNCHER_PING_DOCTYPE TELEMETRY_LAUNCHER_PING_VERSION L"/"; +static const uint32_t kGuidCharLenWithNul = 39; +static const uint32_t kGuidCharLenNoBracesNoNul = 36; +static const mozilla::StaticXREAppData* gAppData; + +// Ordinarily, errors are only reported to the Windows Event Log when they are +// not reported upstream via telemetry (usually due either to telemetry being +// disabled or to network failure). +// +// If `--log-launcher-error` is given at the command line, launcher errors will +// always be reported to the Windows Event Log, regardless of whether or not +// they're sent upstream. +static bool gForceEventLog = false; + +namespace { + +constexpr wchar_t kEventSourceName[] = L"" MOZ_APP_DISPLAYNAME " Launcher"; + +struct EventSourceDeleter { + using pointer = HANDLE; + + void operator()(pointer aEvtSrc) { ::DeregisterEventSource(aEvtSrc); } +}; + +using EventLog = mozilla::UniquePtr; + +struct SerializedEventData { + HRESULT mHr; + uint32_t mLine; + char mFile[1]; +}; + +} // anonymous namespace + +static void PostErrorToLog(const mozilla::LauncherError& aError) { + // This is very bare-bones; just enough to spit out an HRESULT to the + // Application event log. + EventLog log(::RegisterEventSourceW(nullptr, kEventSourceName)); + + if (!log) { + return; + } + + size_t fileLen = strlen(aError.mFile); + size_t dataLen = sizeof(HRESULT) + sizeof(uint32_t) + fileLen; + auto evtDataBuf = mozilla::MakeUnique(dataLen); + SerializedEventData& evtData = + *reinterpret_cast(evtDataBuf.get()); + evtData.mHr = aError.mError.AsHResult(); + evtData.mLine = aError.mLine; + // Since this is binary data, we're not concerning ourselves with null + // terminators. + memcpy(evtData.mFile, aError.mFile, fileLen); + + ::ReportEventW(log.get(), EVENTLOG_ERROR_TYPE, 0, aError.mError.AsHResult(), + nullptr, 0, dataLen, nullptr, evtDataBuf.get()); +} + +#if defined(MOZ_TELEMETRY_REPORTING) + +namespace { + +// This JSONWriteFunc writes directly to a temp file. By creating this file +// with the FILE_ATTRIBUTE_TEMPORARY attribute, we hint to the OS that this +// file is short-lived. The OS will try to avoid flushing it to disk if at +// all possible. +class TempFileWriter final : public mozilla::JSONWriteFunc { + public: + TempFileWriter() : mFailed(false), mSuccessfulHandoff(false) { + wchar_t name[MAX_PATH + 1] = {}; + if (_wtmpnam_s(name)) { + mFailed = true; + return; + } + + mTempFileName = name; + + mTempFile.own(::CreateFileW(name, GENERIC_WRITE, FILE_SHARE_READ, nullptr, + CREATE_NEW, FILE_ATTRIBUTE_TEMPORARY, nullptr)); + if (mTempFile.get() == INVALID_HANDLE_VALUE) { + mFailed = true; + } + } + + ~TempFileWriter() { + if (mSuccessfulHandoff) { + // It is no longer our responsibility to delete the temp file if we have + // successfully handed it off to pingsender. + return; + } + + mTempFile.reset(); + ::DeleteFileW(mTempFileName.c_str()); + } + + explicit operator bool() const { return !mFailed; } + + void Write(const mozilla::Span& aStr) final { + if (mFailed) { + return; + } + + DWORD bytesWritten = 0; + if (!::WriteFile(mTempFile, aStr.data(), aStr.size(), &bytesWritten, + nullptr) || + bytesWritten != aStr.size()) { + mFailed = true; + } + } + + const std::wstring& GetFileName() const { return mTempFileName; } + + void SetSuccessfulHandoff() { mSuccessfulHandoff = true; } + + private: + bool mFailed; + bool mSuccessfulHandoff; + std::wstring mTempFileName; + nsAutoHandle mTempFile; +}; + +using SigMap = mozilla::Vector; + +} // anonymous namespace + +// This is the guideline for maximum string length for telemetry intake +static const size_t kMaxStrLen = 80; + +static mozilla::UniquePtr WideToUTF8(const wchar_t* aStr, + const size_t aStrLenExclNul) { + // Yes, this might not handle surrogate pairs correctly. Let's just let + // WideCharToMultiByte fail in that unlikely case. + size_t cvtLen = std::min(aStrLenExclNul, kMaxStrLen); + + int numConv = ::WideCharToMultiByte(CP_UTF8, 0, aStr, cvtLen, nullptr, 0, + nullptr, nullptr); + if (!numConv) { + return nullptr; + } + + // Include room for the null terminator by adding one + auto buf = mozilla::MakeUnique(numConv + 1); + + numConv = ::WideCharToMultiByte(CP_UTF8, 0, aStr, cvtLen, buf.get(), numConv, + nullptr, nullptr); + if (!numConv) { + return nullptr; + } + + // Add null termination. numConv does not include the terminator, so we don't + // subtract 1 when indexing into buf. + buf[numConv] = 0; + + return buf; +} + +static mozilla::UniquePtr WideToUTF8(const wchar_t* aStr) { + return WideToUTF8(aStr, wcslen(aStr)); +} + +static mozilla::UniquePtr WideToUTF8(const std::wstring& aStr) { + return WideToUTF8(aStr.c_str(), aStr.length()); +} + +// MinGW does not support the Windows Security Center APIs. +# if !defined(__MINGW32__) + +static mozilla::UniquePtr WideToUTF8(const _bstr_t& aStr) { + return WideToUTF8(static_cast(aStr), aStr.length()); +} + +namespace { + +struct ProviderKey { + WSC_SECURITY_PROVIDER mProviderType; + const char* mKey; +}; + +} // anonymous namespace + +static bool EnumWSCProductList(RefPtr& aProdList, + mozilla::JSONWriter& aJson) { + LONG count; + HRESULT hr = aProdList->get_Count(&count); + if (FAILED(hr)) { + return false; + } + + // Unlikely, but put a bound on the max length of the output array for the + // purposes of telemetry intake. + count = std::min(count, 1000L); + + // Record the name(s) of each active registered product in this category + for (LONG index = 0; index < count; ++index) { + RefPtr product; + hr = aProdList->get_Item(index, getter_AddRefs(product)); + if (FAILED(hr)) { + return false; + } + + WSC_SECURITY_PRODUCT_STATE state; + hr = product->get_ProductState(&state); + if (FAILED(hr)) { + return false; + } + + // We only care about products that are active + if (state == WSC_SECURITY_PRODUCT_STATE_OFF || + state == WSC_SECURITY_PRODUCT_STATE_SNOOZED || + state == WSC_SECURITY_PRODUCT_STATE_EXPIRED) { + continue; + } + + _bstr_t bName; + hr = product->get_ProductName(bName.GetAddress()); + if (FAILED(hr)) { + return false; + } + + auto buf = WideToUTF8(bName); + if (!buf) { + return false; + } + + aJson.StringElement(mozilla::MakeStringSpan(buf.get())); + } + + return true; +} + +static const ProviderKey gProvKeys[] = { + {WSC_SECURITY_PROVIDER_ANTIVIRUS, "av"}, + {WSC_SECURITY_PROVIDER_ANTISPYWARE, "antispyware"}, + {WSC_SECURITY_PROVIDER_FIREWALL, "firewall"}}; + +static bool AddWscInfo(mozilla::JSONWriter& aJson) { + // We need COM for this. Using ProcessRuntime so that process-global COM + // configuration is done correctly + mozilla::mscom::ProcessRuntime mscom( + mozilla::mscom::ProcessRuntime::ProcessCategory::Launcher); + if (!mscom) { + // We haven't written anything yet, so we can return true here and continue + // capturing data. + return true; + } + + aJson.StartObjectProperty("security"); + + const CLSID clsid = __uuidof(WSCProductList); + const IID iid = __uuidof(IWSCProductList); + + for (uint32_t index = 0; index < mozilla::ArrayLength(gProvKeys); ++index) { + // NB: A separate instance of IWSCProductList is needed for each distinct + // security provider type; MSDN says that we cannot reuse the same object + // and call Initialize() to pave over the previous data. + RefPtr prodList; + HRESULT hr = ::CoCreateInstance(clsid, nullptr, CLSCTX_INPROC_SERVER, iid, + getter_AddRefs(prodList)); + if (FAILED(hr)) { + return false; + } + + hr = prodList->Initialize(gProvKeys[index].mProviderType); + if (FAILED(hr)) { + return false; + } + + aJson.StartArrayProperty(mozilla::MakeStringSpan(gProvKeys[index].mKey)); + + if (!EnumWSCProductList(prodList, aJson)) { + return false; + } + + aJson.EndArray(); + } + + aJson.EndObject(); + + return true; +} +# endif // !defined(__MINGW32__) + +// Max array length for telemetry intake. +static const size_t kMaxArrayLen = 1000; + +static bool AddModuleInfo(const nsAutoHandle& aSnapshot, + mozilla::JSONWriter& aJson) { + if (aSnapshot.get() == INVALID_HANDLE_VALUE) { + // We haven't written anything yet, so we can return true here and continue + // capturing data. + return true; + } + + SigMap signatures; + size_t moduleCount = 0; + + MODULEENTRY32W module = {sizeof(module)}; + if (!::Module32FirstW(aSnapshot, &module)) { + // We haven't written anything yet, so we can return true here and continue + // capturing data. + return true; + } + + mozilla::glue::BasicDllServices dllServices; + + aJson.StartObjectProperty("modules"); + + // For each module, add its version number (or empty string if not present), + // followed by an optional index into the signatures array + do { + ++moduleCount; + + wchar_t leaf[_MAX_FNAME] = {}; + if (::_wsplitpath_s(module.szExePath, nullptr, 0, nullptr, 0, leaf, + mozilla::ArrayLength(leaf), nullptr, 0)) { + return false; + } + + if (_wcslwr_s(leaf, mozilla::ArrayLength(leaf))) { + return false; + } + + auto leafUtf8 = WideToUTF8(leaf); + if (!leafUtf8) { + return false; + } + + aJson.StartArrayProperty(mozilla::MakeStringSpan(leafUtf8.get())); + + std::string version; + DWORD verInfoSize = ::GetFileVersionInfoSizeW(module.szExePath, nullptr); + if (verInfoSize) { + auto verInfoBuf = mozilla::MakeUnique(verInfoSize); + + if (::GetFileVersionInfoW(module.szExePath, 0, verInfoSize, + verInfoBuf.get())) { + VS_FIXEDFILEINFO* fixedInfo = nullptr; + UINT fixedInfoLen = 0; + + if (::VerQueryValueW(verInfoBuf.get(), L"\\", + reinterpret_cast(&fixedInfo), + &fixedInfoLen)) { + std::ostringstream oss; + oss << HIWORD(fixedInfo->dwFileVersionMS) << '.' + << LOWORD(fixedInfo->dwFileVersionMS) << '.' + << HIWORD(fixedInfo->dwFileVersionLS) << '.' + << LOWORD(fixedInfo->dwFileVersionLS); + version = oss.str(); + } + } + } + + aJson.StringElement(version); + + mozilla::Maybe sigIndex; + auto signedBy = dllServices.GetBinaryOrgName(module.szExePath); + if (signedBy) { + std::wstring strSignedBy(signedBy.get()); + auto entry = std::find(signatures.begin(), signatures.end(), strSignedBy); + if (entry == signatures.end()) { + mozilla::Unused << signatures.append(std::move(strSignedBy)); + entry = &signatures.back(); + } + + sigIndex = mozilla::Some(entry - signatures.begin()); + } + + if (sigIndex) { + aJson.IntElement(sigIndex.value()); + } + + aJson.EndArray(); + } while (moduleCount < kMaxArrayLen && ::Module32NextW(aSnapshot, &module)); + + aJson.EndObject(); + + aJson.StartArrayProperty("signatures"); + + // Serialize each entry in the signatures array + for (auto&& itr : signatures) { + auto sigUtf8 = WideToUTF8(itr); + if (!sigUtf8) { + continue; + } + + aJson.StringElement(mozilla::MakeStringSpan(sigUtf8.get())); + } + + aJson.EndArray(); + + return true; +} + +namespace { + +struct PingThreadContext { + explicit PingThreadContext(const mozilla::LauncherError& aError, + const char* aProcessType) + : mLauncherError(aError), + mModulesSnapshot(::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, 0)), + mProcessType(aProcessType ? aProcessType : "") {} + mozilla::LauncherError mLauncherError; + nsAutoHandle mModulesSnapshot; + std::string mProcessType; +}; + +} // anonymous namespace + +static bool PrepPing(const PingThreadContext& aContext, const std::wstring& aId, + mozilla::JSONWriter& aJson) { +# if defined(DEBUG) + const mozilla::JSONWriter::CollectionStyle style = + mozilla::JSONWriter::MultiLineStyle; +# else + const mozilla::JSONWriter::CollectionStyle style = + mozilla::JSONWriter::SingleLineStyle; +# endif // defined(DEBUG) + + aJson.Start(style); + + aJson.StringProperty("type", "launcher-process-failure"); + aJson.IntProperty("version", 1); + + auto idUtf8 = WideToUTF8(aId); + if (idUtf8) { + aJson.StringProperty("id", mozilla::MakeStringSpan(idUtf8.get())); + } + + time_t now; + time(&now); + tm gmTm; + if (!gmtime_s(&gmTm, &now)) { + char isoTimeBuf[32] = {}; + if (strftime(isoTimeBuf, mozilla::ArrayLength(isoTimeBuf), "%FT%T.000Z", + &gmTm)) { + aJson.StringProperty("creationDate", isoTimeBuf); + } + } + + aJson.StringProperty("update_channel", QUOTE_ME(MOZ_UPDATE_CHANNEL)); + + if (gAppData) { + aJson.StringProperty("build_id", + mozilla::MakeStringSpan(gAppData->buildID)); + aJson.StringProperty("build_version", + mozilla::MakeStringSpan(gAppData->version)); + } + + OSVERSIONINFOEXW osv = {sizeof(osv)}; + if (::GetVersionExW(reinterpret_cast(&osv))) { + std::ostringstream oss; + oss << osv.dwMajorVersion << "." << osv.dwMinorVersion << "." + << osv.dwBuildNumber; + + if (osv.dwMajorVersion == 10 && osv.dwMinorVersion == 0) { + // Get the "Update Build Revision" (UBR) value + DWORD ubrValue; + DWORD ubrValueLen = sizeof(ubrValue); + LSTATUS ubrOk = + ::RegGetValueW(HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", + L"UBR", RRF_RT_DWORD | RRF_SUBKEY_WOW6464KEY, nullptr, + &ubrValue, &ubrValueLen); + if (ubrOk == ERROR_SUCCESS) { + oss << "." << ubrValue; + } + } + + if (oss) { + aJson.StringProperty("os_version", oss.str()); + } + + bool isServer = osv.wProductType == VER_NT_DOMAIN_CONTROLLER || + osv.wProductType == VER_NT_SERVER; + aJson.BoolProperty("server_os", isServer); + } + + WCHAR localeName[LOCALE_NAME_MAX_LENGTH] = {}; + int localeNameLen = + ::GetUserDefaultLocaleName(localeName, mozilla::ArrayLength(localeName)); + if (localeNameLen) { + auto localeNameUtf8 = WideToUTF8(localeName, localeNameLen - 1); + if (localeNameUtf8) { + aJson.StringProperty("os_locale", + mozilla::MakeStringSpan(localeNameUtf8.get())); + } + } + + SYSTEM_INFO sysInfo; + ::GetNativeSystemInfo(&sysInfo); + aJson.IntProperty("cpu_arch", sysInfo.wProcessorArchitecture); + aJson.IntProperty("num_logical_cpus", sysInfo.dwNumberOfProcessors); + + mozilla::LauncherResult isAdminWithoutUac = + mozilla::IsAdminWithoutUac(); + if (isAdminWithoutUac.isOk()) { + aJson.BoolProperty("is_admin_without_uac", isAdminWithoutUac.unwrap()); + } + + if (!aContext.mProcessType.empty()) { + aJson.StringProperty("process_type", aContext.mProcessType); + } + + MEMORYSTATUSEX memStatus = {sizeof(memStatus)}; + if (::GlobalMemoryStatusEx(&memStatus)) { + aJson.StartObjectProperty("memory"); + aJson.IntProperty("total_phys", memStatus.ullTotalPhys); + aJson.IntProperty("avail_phys", memStatus.ullAvailPhys); + aJson.IntProperty("avail_page_file", memStatus.ullAvailPageFile); + aJson.IntProperty("avail_virt", memStatus.ullAvailVirtual); + aJson.EndObject(); + } + + aJson.StringProperty("xpcom_abi", TARGET_XPCOM_ABI); + + aJson.StartObjectProperty("launcher_error", style); + + std::string srcFileLeaf(aContext.mLauncherError.mFile); + // Obtain the leaf name of the file for privacy reasons + // (In case this is somebody's local build) + auto pos = srcFileLeaf.find_last_of("/\\"); + if (pos != std::string::npos) { + srcFileLeaf = srcFileLeaf.substr(pos + 1); + } + + aJson.StringProperty("source_file", srcFileLeaf); + + aJson.IntProperty("source_line", aContext.mLauncherError.mLine); + aJson.IntProperty("hresult", aContext.mLauncherError.mError.AsHResult()); + +# if defined(NIGHTLY_BUILD) + if (aContext.mLauncherError.mDetourError.isSome()) { + static const char* kHexMap = "0123456789abcdef"; + char hexStr[sizeof(mozilla::DetourError::mOrigBytes) * 2 + 1]; + int cnt = 0; + for (uint8_t byte : aContext.mLauncherError.mDetourError->mOrigBytes) { + hexStr[cnt++] = kHexMap[(byte >> 4) & 0x0f]; + hexStr[cnt++] = kHexMap[byte & 0x0f]; + } + hexStr[cnt] = 0; + aJson.StringProperty("detour_orig_bytes", hexStr); + } +# endif // defined(NIGHTLY_BUILD) + + aJson.EndObject(); + +# if !defined(__MINGW32__) + if (!AddWscInfo(aJson)) { + return false; + } +# endif // !defined(__MINGW32__) + + if (!AddModuleInfo(aContext.mModulesSnapshot, aJson)) { + return false; + } + + aJson.End(); + + return true; +} + +static bool DoSendPing(const PingThreadContext& aContext) { + TempFileWriter tempFile; + mozilla::JSONWriter json(tempFile); + + UUID uuid; + if (::UuidCreate(&uuid) != RPC_S_OK) { + return false; + } + + wchar_t guidBuf[kGuidCharLenWithNul] = {}; + if (::StringFromGUID2(uuid, guidBuf, kGuidCharLenWithNul) != + kGuidCharLenWithNul) { + return false; + } + + // Strip the curly braces off of the guid + std::wstring guidNoBraces(guidBuf + 1, kGuidCharLenNoBracesNoNul); + + // Populate json with the ping information + if (!PrepPing(aContext, guidNoBraces, json)) { + return false; + } + + // Obtain the name of the temp file that we have written + const std::wstring& fileName = tempFile.GetFileName(); + + // Using the path to our executable binary, construct the path to + // pingsender.exe + mozilla::UniquePtr exePath(mozilla::GetFullBinaryPath()); + + wchar_t drive[_MAX_DRIVE] = {}; + wchar_t dir[_MAX_DIR] = {}; + if (_wsplitpath_s(exePath.get(), drive, mozilla::ArrayLength(drive), dir, + mozilla::ArrayLength(dir), nullptr, 0, nullptr, 0)) { + return false; + } + + wchar_t pingSenderPath[MAX_PATH + 1] = {}; + if (_wmakepath_s(pingSenderPath, mozilla::ArrayLength(pingSenderPath), drive, + dir, L"pingsender", L"exe")) { + return false; + } + + // Construct the telemetry URL + wchar_t urlBuf[mozilla::ArrayLength(kUrl) + kGuidCharLenNoBracesNoNul] = {}; + if (wcscpy_s(urlBuf, kUrl)) { + return false; + } + + if (wcscat_s(urlBuf, guidNoBraces.c_str())) { + return false; + } + + // Now build the command line arguments to pingsender + wchar_t* pingSenderArgv[] = {pingSenderPath, urlBuf, + const_cast(fileName.c_str())}; + + mozilla::UniquePtr pingSenderCmdLine(mozilla::MakeCommandLine( + mozilla::ArrayLength(pingSenderArgv), pingSenderArgv)); + + // Now start pingsender to handle the rest + PROCESS_INFORMATION pi; + + STARTUPINFOW si = {sizeof(si)}; + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + + if (!::CreateProcessW(pingSenderPath, pingSenderCmdLine.get(), nullptr, + nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) { + return false; + } + + tempFile.SetSuccessfulHandoff(); + + nsAutoHandle proc(pi.hProcess); + nsAutoHandle thread(pi.hThread); + + return true; +} + +static unsigned __stdcall SendPingThread(void* aContext) { + mozilla::UniquePtr context( + reinterpret_cast(aContext)); + + if (!DoSendPing(*context) || gForceEventLog) { + PostErrorToLog(context->mLauncherError); + } + + return 0; +} + +#endif // defined(MOZ_TELEMETRY_REPORTING) + +static bool SendPing(const mozilla::LauncherError& aError, + const char* aProcessType) { +#if defined(MOZ_TELEMETRY_REPORTING) +# if defined(MOZ_LAUNCHER_PROCESS) + mozilla::LauncherRegistryInfo regInfo; + mozilla::LauncherResult telemetryEnabled = regInfo.IsTelemetryEnabled(); + if (telemetryEnabled.isErr() || !telemetryEnabled.unwrap()) { + // Do not send anything if telemetry has been opted out + return false; + } +# endif // defined(MOZ_LAUNCHER_PROCESS) + + // We send this ping when the launcher process fails. After we start the + // SendPingThread, this thread falls back from running as the launcher process + // to running as the browser main thread. Once this happens, it will be unsafe + // to set up PoisonIOInterposer (since we have already spun up a background + // thread). + mozilla::SaveToEnv("MOZ_DISABLE_POISON_IO_INTERPOSER=1"); + + // Capture aError and our module list into context for processing on another + // thread. + auto thdParam = mozilla::MakeUnique(aError, aProcessType); + + // The ping does a lot of file I/O. Since we want this thread to continue + // executing browser startup, we should gather that information on a + // background thread. + uintptr_t thdHandle = + _beginthreadex(nullptr, 0, &SendPingThread, thdParam.get(), + STACK_SIZE_PARAM_IS_A_RESERVATION, nullptr); + if (!thdHandle) { + return false; + } + + // We have handed off thdParam to the background thread + mozilla::Unused << thdParam.release(); + + ::CloseHandle(reinterpret_cast(thdHandle)); + return true; +#else + return false; +#endif +} + +namespace mozilla { + +void HandleLauncherError(const LauncherError& aError, + const char* aProcessType) { +#if defined(MOZ_LAUNCHER_PROCESS) + LauncherRegistryInfo regInfo; + Unused << regInfo.DisableDueToFailure(); +#endif // defined(MOZ_LAUNCHER_PROCESS) + + if (!SendPing(aError, aProcessType)) { + // couldn't (or shouldn't) send telemetry; fall back to event log + PostErrorToLog(aError); + } +} + +void SetLauncherErrorAppData(const StaticXREAppData& aAppData) { + gAppData = &aAppData; +} + +void SetLauncherErrorForceEventLog() { gForceEventLog = true; } + +} // namespace mozilla diff --git a/browser/app/winlauncher/ErrorHandler.h b/browser/app/winlauncher/ErrorHandler.h new file mode 100644 index 0000000000..71fe72cf07 --- /dev/null +++ b/browser/app/winlauncher/ErrorHandler.h @@ -0,0 +1,54 @@ +/* -*- 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_ErrorHandler_h +#define mozilla_ErrorHandler_h + +#include "mozilla/Assertions.h" +#include "mozilla/WinHeaderOnlyUtils.h" + +namespace mozilla { + +/** + * All launcher process error handling should live in the implementation of + * this function. + */ +void HandleLauncherError(const LauncherError& aError, + const char* aProcessType = nullptr); + +// This function is simply a convenience overload that automatically unwraps +// the LauncherError from the provided LauncherResult and then forwards it to +// the main implementation. +template +inline void HandleLauncherError(const LauncherResult& aResult, + const char* aProcessType = nullptr) { + MOZ_ASSERT(aResult.isErr()); + if (aResult.isOk()) { + return; + } + + HandleLauncherError(aResult.inspectErr(), aProcessType); +} + +// This function is simply a convenience overload that unwraps the provided +// GenericErrorResult and forwards it to the main implementation. +inline void HandleLauncherError( + const GenericErrorResult& aResult, + const char* aProcessType = nullptr) { + LauncherVoidResult r(aResult); + HandleLauncherError(r, aProcessType); +} + +// Forward declaration +struct StaticXREAppData; + +void SetLauncherErrorAppData(const StaticXREAppData& aAppData); + +void SetLauncherErrorForceEventLog(); + +} // namespace mozilla + +#endif // mozilla_ErrorHandler_h diff --git a/browser/app/winlauncher/LaunchUnelevated.cpp b/browser/app/winlauncher/LaunchUnelevated.cpp new file mode 100644 index 0000000000..3cf6ac837c --- /dev/null +++ b/browser/app/winlauncher/LaunchUnelevated.cpp @@ -0,0 +1,290 @@ +/* -*- 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 "LaunchUnelevated.h" + +#include "mozilla/Assertions.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/mscom/ProcessRuntime.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ShellHeaderOnlyUtils.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "../BrowserDefines.h" +#include "nsWindowsHelpers.h" + +#include + +#if !defined(RRF_SUBKEY_WOW6464KEY) +# define RRF_SUBKEY_WOW6464KEY 0x00010000 +#endif // !defined(RRF_SUBKEY_WOW6464KEY) + +static mozilla::LauncherResult IsHighIntegrity( + const nsAutoHandle& aToken) { + DWORD reqdLen; + if (!::GetTokenInformation(aToken.get(), TokenIntegrityLevel, nullptr, 0, + &reqdLen)) { + DWORD err = ::GetLastError(); + if (err != ERROR_INSUFFICIENT_BUFFER) { + return LAUNCHER_ERROR_FROM_WIN32(err); + } + } + + auto buf = mozilla::MakeUnique(reqdLen); + + if (!::GetTokenInformation(aToken.get(), TokenIntegrityLevel, buf.get(), + reqdLen, &reqdLen)) { + return LAUNCHER_ERROR_FROM_LAST(); + } + + auto tokenLabel = reinterpret_cast(buf.get()); + + DWORD subAuthCount = *::GetSidSubAuthorityCount(tokenLabel->Label.Sid); + DWORD integrityLevel = + *::GetSidSubAuthority(tokenLabel->Label.Sid, subAuthCount - 1); + return integrityLevel > SECURITY_MANDATORY_MEDIUM_RID; +} + +static mozilla::LauncherResult GetMediumIntegrityToken( + const nsAutoHandle& aProcessToken) { + HANDLE rawResult; + if (!::DuplicateTokenEx(aProcessToken.get(), 0, nullptr, + SecurityImpersonation, TokenPrimary, &rawResult)) { + return LAUNCHER_ERROR_FROM_LAST(); + } + + nsAutoHandle result(rawResult); + + BYTE mediumIlSid[SECURITY_MAX_SID_SIZE]; + DWORD mediumIlSidSize = sizeof(mediumIlSid); + if (!::CreateWellKnownSid(WinMediumLabelSid, nullptr, mediumIlSid, + &mediumIlSidSize)) { + return LAUNCHER_ERROR_FROM_LAST(); + } + + TOKEN_MANDATORY_LABEL integrityLevel = {}; + integrityLevel.Label.Attributes = SE_GROUP_INTEGRITY; + integrityLevel.Label.Sid = reinterpret_cast(mediumIlSid); + + if (!::SetTokenInformation(rawResult, TokenIntegrityLevel, &integrityLevel, + sizeof(integrityLevel))) { + return LAUNCHER_ERROR_FROM_LAST(); + } + + return result.disown(); +} + +static mozilla::LauncherResult IsAdminByAppCompat( + HKEY aRootKey, const wchar_t* aExecutablePath) { + static const wchar_t kPathToLayers[] = + L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\" + L"AppCompatFlags\\Layers"; + + DWORD dataLength = 0; + LSTATUS status = ::RegGetValueW(aRootKey, kPathToLayers, aExecutablePath, + RRF_RT_REG_SZ | RRF_SUBKEY_WOW6464KEY, + nullptr, nullptr, &dataLength); + if (status == ERROR_FILE_NOT_FOUND) { + return false; + } else if (status != ERROR_SUCCESS) { + return LAUNCHER_ERROR_FROM_WIN32(status); + } + + auto valueData = mozilla::MakeUnique(dataLength); + if (!valueData) { + return LAUNCHER_ERROR_FROM_WIN32(ERROR_OUTOFMEMORY); + } + + status = ::RegGetValueW(aRootKey, kPathToLayers, aExecutablePath, + RRF_RT_REG_SZ | RRF_SUBKEY_WOW6464KEY, nullptr, + valueData.get(), &dataLength); + if (status != ERROR_SUCCESS) { + return LAUNCHER_ERROR_FROM_WIN32(status); + } + + const wchar_t kRunAsAdmin[] = L"RUNASADMIN"; + const wchar_t kDelimiters[] = L" "; + wchar_t* tokenContext = nullptr; + const wchar_t* token = wcstok_s(valueData.get(), kDelimiters, &tokenContext); + while (token) { + if (!_wcsnicmp(token, kRunAsAdmin, mozilla::ArrayLength(kRunAsAdmin))) { + return true; + } + token = wcstok_s(nullptr, kDelimiters, &tokenContext); + } + + return false; +} + +namespace mozilla { + +// If we're running at an elevated integrity level, re-run ourselves at the +// user's normal integrity level. We do this by locating the active explorer +// shell, and then asking it to do a ShellExecute on our behalf. We do it this +// way to ensure that the child process runs as the original user in the active +// session; an elevated process could be running with different credentials than +// those of the session. +// See https://devblogs.microsoft.com/oldnewthing/20131118-00/?p=2643 + +LauncherVoidResult LaunchUnelevated(int aArgc, wchar_t* aArgv[]) { + // We need COM to talk to Explorer. Using ProcessRuntime so that + // process-global COM configuration is done correctly + mozilla::mscom::ProcessRuntime mscom( + mozilla::mscom::ProcessRuntime::ProcessCategory::Launcher); + if (!mscom) { + return LAUNCHER_ERROR_FROM_HRESULT(mscom.GetHResult()); + } + + // Omit the original argv[0] because ShellExecute doesn't need it. Insert + // ATTEMPTING_DEELEVATION_FLAG so that we know not to attempt to restart + // ourselves if deelevation fails. + UniquePtr cmdLine = [&]() { + constexpr wchar_t const* kTagArg = L"--" ATTEMPTING_DEELEVATION_FLAG; + + // This should have already been checked, but just in case... + EnsureBrowserCommandlineSafe(aArgc, aArgv); + + if (mozilla::CheckArg(aArgc, aArgv, "osint", nullptr, CheckArgFlag::None)) { + // If the command line contains -osint, we have to arrange things in a + // particular order. + // + // (We can't just replace -osint with kTagArg, unfortunately: there is + // code in the browser which behaves differently in the presence of an + // `-osint` tag, but which will not have had a chance to react to this. + // See, _e.g._, bug 1243603.) + auto const aArgvCopy = MakeUnique(aArgc + 1); + aArgvCopy[0] = aArgv[1]; + aArgvCopy[1] = kTagArg; + for (int i = 2; i < aArgc; ++i) { + aArgvCopy[i] = aArgv[i]; + } + aArgvCopy[aArgc] = nullptr; // because argv[argc] is NULL + return MakeCommandLine(aArgc, aArgvCopy.get(), 0, nullptr); + } else { + // Otherwise, just tack it on at the end. + constexpr wchar_t const* const kTagArgArray[] = {kTagArg}; + return MakeCommandLine(aArgc - 1, aArgv + 1, 1, kTagArgArray); + } + }(); + if (!cmdLine) { + return LAUNCHER_ERROR_GENERIC(); + } + + _bstr_t cmd; + + UniquePtr packageFamilyName = mozilla::GetPackageFamilyName(); + if (packageFamilyName) { + int cmdLen = + // 22 for the prefix + suffix + null terminator below + 22 + wcslen(packageFamilyName.get()); + wchar_t appCmd[cmdLen]; + swprintf(appCmd, cmdLen, L"shell:appsFolder\\%s!App", + packageFamilyName.get()); + cmd = appCmd; + } else { + cmd = aArgv[0]; + } + + _variant_t args(cmdLine.get()); + _variant_t operation(L"open"); + _variant_t directory; + _variant_t showCmd(SW_SHOWNORMAL); + return ShellExecuteByExplorer(cmd, args, operation, directory, showCmd); +} + +LauncherResult GetElevationState( + const wchar_t* aExecutablePath, mozilla::LauncherFlags aFlags, + nsAutoHandle& aOutMediumIlToken) { + aOutMediumIlToken.reset(); + + const DWORD tokenFlags = TOKEN_QUERY | TOKEN_DUPLICATE | + TOKEN_ADJUST_DEFAULT | TOKEN_ASSIGN_PRIMARY; + HANDLE rawToken; + if (!::OpenProcessToken(::GetCurrentProcess(), tokenFlags, &rawToken)) { + return LAUNCHER_ERROR_FROM_LAST(); + } + + nsAutoHandle token(rawToken); + + LauncherResult elevationType = GetElevationType(token); + if (elevationType.isErr()) { + return elevationType.propagateErr(); + } + + Maybe elevationState; + switch (elevationType.unwrap()) { + case TokenElevationTypeLimited: + return ElevationState::eNormalUser; + case TokenElevationTypeFull: + elevationState = Some(ElevationState::eElevated); + break; + case TokenElevationTypeDefault: { + // In this case, UAC is disabled. We do not yet know whether or not we + // are running at high integrity. If we are at high integrity, we can't + // relaunch ourselves in a non-elevated state via Explorer, as we would + // just end up in an infinite loop of launcher processes re-launching + // themselves. + LauncherResult isHighIntegrity = IsHighIntegrity(token); + if (isHighIntegrity.isErr()) { + return isHighIntegrity.propagateErr(); + } + + if (!isHighIntegrity.unwrap()) { + return ElevationState::eNormalUser; + } + + elevationState = Some(ElevationState::eHighIntegrityNoUAC); + break; + } + default: + MOZ_ASSERT_UNREACHABLE("Was a new value added to the enumeration?"); + return LAUNCHER_ERROR_GENERIC(); + } + + MOZ_ASSERT(elevationState.isSome() && + elevationState.value() != ElevationState::eNormalUser, + "Should have returned earlier for the eNormalUser case."); + + LauncherResult isAdminByAppCompat = + IsAdminByAppCompat(HKEY_CURRENT_USER, aExecutablePath); + if (isAdminByAppCompat.isErr()) { + return isAdminByAppCompat.propagateErr(); + } + + if (isAdminByAppCompat.unwrap()) { + elevationState = Some(ElevationState::eHighIntegrityByAppCompat); + } else { + isAdminByAppCompat = + IsAdminByAppCompat(HKEY_LOCAL_MACHINE, aExecutablePath); + if (isAdminByAppCompat.isErr()) { + return isAdminByAppCompat.propagateErr(); + } + + if (isAdminByAppCompat.unwrap()) { + elevationState = Some(ElevationState::eHighIntegrityByAppCompat); + } + } + + // A medium IL token is not needed in the following cases. + // 1) We keep the process elevated (= LauncherFlags::eNoDeelevate) + // 2) The process was elevated by UAC (= ElevationState::eElevated) + // AND the launcher process doesn't wait for the browser process + if ((aFlags & mozilla::LauncherFlags::eNoDeelevate) || + (elevationState.value() == ElevationState::eElevated && + !(aFlags & mozilla::LauncherFlags::eWaitForBrowser))) { + return elevationState.value(); + } + + LauncherResult tokenResult = GetMediumIntegrityToken(token); + if (tokenResult.isOk()) { + aOutMediumIlToken.own(tokenResult.unwrap()); + } else { + return tokenResult.propagateErr(); + } + + return elevationState.value(); +} + +} // namespace mozilla diff --git a/browser/app/winlauncher/LaunchUnelevated.h b/browser/app/winlauncher/LaunchUnelevated.h new file mode 100644 index 0000000000..8c6679edf3 --- /dev/null +++ b/browser/app/winlauncher/LaunchUnelevated.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 https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_LaunchUnelevated_h +#define mozilla_LaunchUnelevated_h + +#include "LauncherProcessWin.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "mozilla/Maybe.h" +#include "nsWindowsHelpers.h" + +namespace mozilla { + +enum class ElevationState { + eNormalUser = 0, + eElevated = (1 << 0), + eHighIntegrityNoUAC = (1 << 1), + eHighIntegrityByAppCompat = (1 << 2), +}; + +LauncherResult GetElevationState( + const wchar_t* aExecutablePath, LauncherFlags aFlags, + nsAutoHandle& aOutMediumIlToken); + +LauncherVoidResult LaunchUnelevated(int aArgc, wchar_t* aArgv[]); + +} // namespace mozilla + +#endif // mozilla_LaunchUnelevated_h diff --git a/browser/app/winlauncher/LauncherProcessWin.cpp b/browser/app/winlauncher/LauncherProcessWin.cpp new file mode 100644 index 0000000000..b40e0fceb5 --- /dev/null +++ b/browser/app/winlauncher/LauncherProcessWin.cpp @@ -0,0 +1,529 @@ +/* -*- 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 "LauncherProcessWin.h" + +#include + +#include "mozilla/Attributes.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/DynamicallyLinkedFunctionPtr.h" +#include "mozilla/glue/Debug.h" +#include "mozilla/GeckoArgs.h" +#include "mozilla/Maybe.h" +#include "mozilla/SafeMode.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WindowsConsole.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "nsWindowsHelpers.h" + +#include +#include + +#include "DllBlocklistInit.h" +#include "ErrorHandler.h" +#include "LaunchUnelevated.h" +#include "ProcThreadAttributes.h" +#include "../BrowserDefines.h" + +#if defined(MOZ_LAUNCHER_PROCESS) +# include "mozilla/LauncherRegistryInfo.h" +# include "SameBinary.h" +#endif // defined(MOZ_LAUNCHER_PROCESS) + +#if defined(MOZ_SANDBOX) +# include "mozilla/sandboxing/SandboxInitialization.h" +#endif + +namespace mozilla { +// "const" because nothing in this process modifies it. +// "volatile" because something in another process may. +const volatile DeelevationStatus gDeelevationStatus = + DeelevationStatus::DefaultStaticValue; +} // namespace mozilla + +/** + * At this point the child process has been created in a suspended state. Any + * additional startup work (eg, blocklist setup) should go here. + * + * @return Ok if browser startup should proceed + */ +static mozilla::LauncherVoidResult PostCreationSetup( + const wchar_t* aFullImagePath, HANDLE aChildProcess, + HANDLE aChildMainThread, mozilla::DeelevationStatus aDStatus, + const bool aIsSafeMode, const bool aDisableDynamicBlocklist, + mozilla::Maybe aBlocklistFileName) { + /* scope for txManager */ { + mozilla::nt::CrossExecTransferManager txManager(aChildProcess); + if (!txManager) { + return LAUNCHER_ERROR_FROM_WIN32(ERROR_BAD_EXE_FORMAT); + } + + using mozilla::gDeelevationStatus; + + void* targetAddress = (LPVOID)&gDeelevationStatus; + + auto const guard = txManager.Protect( + targetAddress, sizeof(gDeelevationStatus), PAGE_READWRITE); + + mozilla::LauncherVoidResult result = + txManager.Transfer(targetAddress, &aDStatus, sizeof(aDStatus)); + if (result.isErr()) { + return result; + } + } + + return mozilla::InitializeDllBlocklistOOPFromLauncher( + aFullImagePath, aChildProcess, aDisableDynamicBlocklist, + aBlocklistFileName); +} + +/** + * Create a new Job object and assign |aProcess| to it. If something fails + * in this function, we return nullptr but continue without recording + * a launcher failure because it's not a critical problem to launch + * the browser process. + */ +static nsReturnRef CreateJobAndAssignProcess(HANDLE aProcess) { + nsAutoHandle empty; + nsAutoHandle job(::CreateJobObjectW(nullptr, nullptr)); + + // Set JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK to put only browser process + // into a job without putting children of browser process into the job. + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = {}; + jobInfo.BasicLimitInformation.LimitFlags = + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK; + if (!::SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation, + &jobInfo, sizeof(jobInfo))) { + return empty.out(); + } + + if (!::AssignProcessToJobObject(job.get(), aProcess)) { + return empty.out(); + } + + return job.out(); +} + +#if !defined( \ + PROCESS_CREATION_MITIGATION_POLICY_IMAGE_LOAD_PREFER_SYSTEM32_ALWAYS_ON) +# define PROCESS_CREATION_MITIGATION_POLICY_IMAGE_LOAD_PREFER_SYSTEM32_ALWAYS_ON \ + (0x00000001ULL << 60) +#endif // !defined(PROCESS_CREATION_MITIGATION_POLICY_IMAGE_LOAD_PREFER_SYSTEM32_ALWAYS_ON) + +#if !defined(PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF) +# define PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF \ + (0x00000002ULL << 40) +#endif // !defined(PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF) + +/** + * Any mitigation policies that should be set on the browser process should go + * here. + */ +static void SetMitigationPolicies(mozilla::ProcThreadAttributes& aAttrs, + const bool aIsSafeMode) { + if (mozilla::IsWin10AnniversaryUpdateOrLater()) { + aAttrs.AddMitigationPolicy( + PROCESS_CREATION_MITIGATION_POLICY_IMAGE_LOAD_PREFER_SYSTEM32_ALWAYS_ON); + } + +#if defined(_M_ARM64) + // Disable CFG on older versions of ARM64 Windows to avoid a crash in COM. + if (!mozilla::IsWin10Sep2018UpdateOrLater()) { + aAttrs.AddMitigationPolicy( + PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF); + } +#endif // defined(_M_ARM64) +} + +static mozilla::LauncherFlags ProcessCmdLine(int& aArgc, wchar_t* aArgv[]) { + mozilla::LauncherFlags result = mozilla::LauncherFlags::eNone; + + if (mozilla::CheckArg(aArgc, aArgv, "wait-for-browser", nullptr, + mozilla::CheckArgFlag::RemoveArg) == + mozilla::ARG_FOUND || + mozilla::CheckArg(aArgc, aArgv, "marionette", nullptr, + mozilla::CheckArgFlag::None) == mozilla::ARG_FOUND || + mozilla::CheckArg(aArgc, aArgv, "backgroundtask", nullptr, + mozilla::CheckArgFlag::None) == mozilla::ARG_FOUND || + mozilla::CheckArg(aArgc, aArgv, "headless", nullptr, + mozilla::CheckArgFlag::None) == mozilla::ARG_FOUND || + mozilla::CheckArg(aArgc, aArgv, "remote-debugging-port", nullptr, + mozilla::CheckArgFlag::None) == mozilla::ARG_FOUND || + mozilla::EnvHasValue("MOZ_AUTOMATION") || + mozilla::EnvHasValue("MOZ_HEADLESS")) { + result |= mozilla::LauncherFlags::eWaitForBrowser; + } + + if (mozilla::CheckArg(aArgc, aArgv, "no-deelevate") == mozilla::ARG_FOUND) { + result |= mozilla::LauncherFlags::eNoDeelevate; + } + + if (mozilla::CheckArg(aArgc, aArgv, ATTEMPTING_DEELEVATION_FLAG) == + mozilla::ARG_FOUND) { + result |= mozilla::LauncherFlags::eDeelevating; + } + + return result; +} + +static void MaybeBreakForBrowserDebugging() { + if (mozilla::EnvHasValue("MOZ_DEBUG_BROWSER_PROCESS")) { + ::DebugBreak(); + return; + } + + const wchar_t* pauseLenS = _wgetenv(L"MOZ_DEBUG_BROWSER_PAUSE"); + if (!pauseLenS || !(*pauseLenS)) { + return; + } + + DWORD pauseLenMs = wcstoul(pauseLenS, nullptr, 10) * 1000; + printf_stderr("\n\nBROWSERBROWSERBROWSERBROWSER\n debug me @ %lu\n\n", + ::GetCurrentProcessId()); + ::Sleep(pauseLenMs); +} + +static bool DoLauncherProcessChecks(int& argc, wchar_t** argv) { + // NB: We run all tests in this function instead of returning early in order + // to ensure that all side effects take place, such as clearing environment + // variables. + bool result = false; + +#if defined(MOZ_LAUNCHER_PROCESS) + // We still prefer to compare file ids. Comparing NT paths i.e. passing + // CompareNtPathsOnly to IsSameBinaryAsParentProcess is much faster, but + // we're not 100% sure that NT path comparison perfectly prevents the + // launching loop of the launcher process. + mozilla::LauncherResult isSame = mozilla::IsSameBinaryAsParentProcess(); + if (isSame.isOk()) { + result = !isSame.unwrap(); + } else { + HandleLauncherError(isSame.unwrapErr()); + } +#endif // defined(MOZ_LAUNCHER_PROCESS) + + if (mozilla::EnvHasValue("MOZ_LAUNCHER_PROCESS")) { + mozilla::SaveToEnv("MOZ_LAUNCHER_PROCESS="); + result = true; + } + + result |= + mozilla::CheckArg(argc, argv, "launcher", nullptr, + mozilla::CheckArgFlag::RemoveArg) == mozilla::ARG_FOUND; + + return result; +} + +#if defined(MOZ_LAUNCHER_PROCESS) +static mozilla::Maybe RunAsLauncherProcess( + mozilla::LauncherRegistryInfo& aRegInfo, int& argc, wchar_t** argv) { +#else +static mozilla::Maybe RunAsLauncherProcess(int& argc, wchar_t** argv) { +#endif // defined(MOZ_LAUNCHER_PROCESS) + bool runAsLauncher = DoLauncherProcessChecks(argc, argv); + +#if defined(MOZ_LAUNCHER_PROCESS) + bool forceLauncher = + runAsLauncher && + mozilla::CheckArg(argc, argv, "force-launcher", nullptr, + mozilla::CheckArgFlag::RemoveArg) == mozilla::ARG_FOUND; + + mozilla::LauncherRegistryInfo::ProcessType desiredType = + runAsLauncher ? mozilla::LauncherRegistryInfo::ProcessType::Launcher + : mozilla::LauncherRegistryInfo::ProcessType::Browser; + + mozilla::LauncherRegistryInfo::CheckOption checkOption = + forceLauncher ? mozilla::LauncherRegistryInfo::CheckOption::Force + : mozilla::LauncherRegistryInfo::CheckOption::Default; + + mozilla::LauncherResult + runAsType = aRegInfo.Check(desiredType, checkOption); + + if (runAsType.isErr()) { + mozilla::HandleLauncherError(runAsType); + return mozilla::Nothing(); + } + + runAsLauncher = runAsType.unwrap() == + mozilla::LauncherRegistryInfo::ProcessType::Launcher; +#endif // defined(MOZ_LAUNCHER_PROCESS) + + if (!runAsLauncher) { + // In this case, we will be proceeding to run as the browser. + // We should check MOZ_DEBUG_BROWSER_* env vars. + MaybeBreakForBrowserDebugging(); + } + + return mozilla::Some(runAsLauncher); +} + +namespace mozilla { + +Maybe LauncherMain(int& argc, wchar_t* argv[], + const StaticXREAppData& aAppData) { + EnsureBrowserCommandlineSafe(argc, argv); + + SetLauncherErrorAppData(aAppData); + + if (CheckArg(argc, argv, "log-launcher-error", nullptr, + mozilla::CheckArgFlag::RemoveArg) == ARG_FOUND) { + SetLauncherErrorForceEventLog(); + } + + // return fast when we're a child process. + // (The remainder of this function has some side effects that are + // undesirable for content processes) + if (mozilla::CheckArg(argc, argv, "contentproc", nullptr, + mozilla::CheckArgFlag::None) == mozilla::ARG_FOUND) { + // A child process should not instantiate LauncherRegistryInfo. + return Nothing(); + } + +#if defined(MOZ_LAUNCHER_PROCESS) + LauncherRegistryInfo regInfo; + Maybe runAsLauncher = RunAsLauncherProcess(regInfo, argc, argv); + LauncherResult blocklistFileNameResult = + regInfo.GetBlocklistFileName(); + Maybe blocklistFileName = + blocklistFileNameResult.isOk() ? Some(blocklistFileNameResult.unwrap()) + : Nothing(); +#else + Maybe runAsLauncher = RunAsLauncherProcess(argc, argv); + Maybe blocklistFileName = Nothing(); +#endif // defined(MOZ_LAUNCHER_PROCESS) + if (!runAsLauncher || !runAsLauncher.value()) { +#if defined(MOZ_LAUNCHER_PROCESS) + // Update the registry as Browser + LauncherVoidResult commitResult = regInfo.Commit(); + if (commitResult.isErr()) { + mozilla::HandleLauncherError(commitResult); + } +#endif // defined(MOZ_LAUNCHER_PROCESS) + return Nothing(); + } + + // Make sure that the launcher process itself has image load policies set + if (IsWin10AnniversaryUpdateOrLater()) { + static const StaticDynamicallyLinkedFunctionPtr< + decltype(&SetProcessMitigationPolicy)> + pSetProcessMitigationPolicy(L"kernel32.dll", + "SetProcessMitigationPolicy"); + if (pSetProcessMitigationPolicy) { + PROCESS_MITIGATION_IMAGE_LOAD_POLICY imgLoadPol = {}; + imgLoadPol.PreferSystem32Images = 1; + + DebugOnly setOk = pSetProcessMitigationPolicy( + ProcessImageLoadPolicy, &imgLoadPol, sizeof(imgLoadPol)); + MOZ_ASSERT(setOk); + } + } + +#if defined(MOZ_SANDBOX) + // Ensure the relevant mitigations are enforced. + mozilla::sandboxing::ApplyParentProcessMitigations(); +#endif + + mozilla::UseParentConsole(); + + if (!SetArgv0ToFullBinaryPath(argv)) { + HandleLauncherError(LAUNCHER_ERROR_GENERIC()); + return Nothing(); + } + + LauncherFlags flags = ProcessCmdLine(argc, argv); + + nsAutoHandle mediumIlToken; + LauncherResult elevationState = + GetElevationState(argv[0], flags, mediumIlToken); + if (elevationState.isErr()) { + HandleLauncherError(elevationState); + return Nothing(); + } + + // Distill deelevation status, and/or attempt to perform launcher deelevation + // via an indirect relaunch. + DeelevationStatus deelevationStatus = DeelevationStatus::Unknown; + if (mediumIlToken.get()) { + // Rather than indirectly relaunch the launcher, we'll attempt to directly + // launch the main process with a reduced-privilege security token. + deelevationStatus = DeelevationStatus::PartiallyDeelevated; + } else if (elevationState.unwrap() == ElevationState::eElevated) { + if (flags & LauncherFlags::eWaitForBrowser) { + // An indirect relaunch won't provide a process-handle to block on, + // so we have to continue onwards with this process. + deelevationStatus = DeelevationStatus::DeelevationProhibited; + } else if (flags & LauncherFlags::eNoDeelevate) { + // Our invoker (hopefully, the user) has explicitly requested that the + // launcher not deelevate itself. + deelevationStatus = DeelevationStatus::DeelevationProhibited; + } else if (flags & LauncherFlags::eDeelevating) { + // We've already tried to deelevate, to no effect. Continue onward. + deelevationStatus = DeelevationStatus::UnsuccessfullyDeelevated; + } else { + // Otherwise, attempt to relaunch the launcher process itself via the + // shell, which hopefully will not be elevated. (But see bug 1733821.) + LauncherVoidResult launchedUnelevated = LaunchUnelevated(argc, argv); + if (launchedUnelevated.isErr()) { + // On failure, don't even try for a launcher process. Continue onwards + // in this one. (TODO: why? This isn't technically fatal...) + HandleLauncherError(launchedUnelevated); + return Nothing(); + } + // Otherwise, tell our caller to exit with a success code. + return Some(0); + } + } else if (elevationState.unwrap() == ElevationState::eNormalUser) { + if (flags & LauncherFlags::eDeelevating) { + // Deelevation appears to have been successful! + deelevationStatus = DeelevationStatus::SuccessfullyDeelevated; + } else { + // We haven't done anything and we don't need to. + deelevationStatus = DeelevationStatus::StartedUnprivileged; + } + } else { + // Some other elevation state with no medium-integrity token. + // (This should probably not happen.) + deelevationStatus = DeelevationStatus::Unknown; + } + +#if defined(MOZ_LAUNCHER_PROCESS) + // Update the registry as Launcher + LauncherVoidResult commitResult = regInfo.Commit(); + if (commitResult.isErr()) { + mozilla::HandleLauncherError(commitResult); + return Nothing(); + } +#endif // defined(MOZ_LAUNCHER_PROCESS) + + // Now proceed with setting up the parameters for process creation + UniquePtr cmdLine(MakeCommandLine(argc, argv)); + if (!cmdLine) { + HandleLauncherError(LAUNCHER_ERROR_GENERIC()); + return Nothing(); + } + + const Maybe isSafeMode = + IsSafeModeRequested(argc, argv, SafeModeFlag::NoKeyPressCheck); + if (!isSafeMode) { + HandleLauncherError(LAUNCHER_ERROR_FROM_WIN32(ERROR_INVALID_PARAMETER)); + return Nothing(); + } + + ProcThreadAttributes attrs; + SetMitigationPolicies(attrs, isSafeMode.value()); + + HANDLE stdHandles[] = {::GetStdHandle(STD_INPUT_HANDLE), + ::GetStdHandle(STD_OUTPUT_HANDLE), + ::GetStdHandle(STD_ERROR_HANDLE)}; + + attrs.AddInheritableHandles(stdHandles); + + DWORD creationFlags = CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT; + + STARTUPINFOEXW siex; + LauncherResult attrsOk = attrs.AssignTo(siex); + if (attrsOk.isErr()) { + HandleLauncherError(attrsOk); + return Nothing(); + } + + BOOL inheritHandles = FALSE; + + if (attrsOk.unwrap()) { + creationFlags |= EXTENDED_STARTUPINFO_PRESENT; + + if (attrs.HasInheritableHandles()) { + siex.StartupInfo.dwFlags |= STARTF_USESTDHANDLES; + siex.StartupInfo.hStdInput = stdHandles[0]; + siex.StartupInfo.hStdOutput = stdHandles[1]; + siex.StartupInfo.hStdError = stdHandles[2]; + + // Since attrsOk == true, we have successfully set the handle inheritance + // whitelist policy, so only the handles added to attrs will be inherited. + inheritHandles = TRUE; + } + } + + // Pass on the path of the shortcut used to launch this process, if any. + STARTUPINFOW currentStartupInfo = {.cb = sizeof(STARTUPINFOW)}; + GetStartupInfoW(¤tStartupInfo); + if ((currentStartupInfo.dwFlags & STARTF_TITLEISLINKNAME) && + currentStartupInfo.lpTitle) { + siex.StartupInfo.dwFlags |= STARTF_TITLEISLINKNAME; + siex.StartupInfo.lpTitle = currentStartupInfo.lpTitle; + } + + PROCESS_INFORMATION pi = {}; + BOOL createOk; + + if (mediumIlToken.get()) { + createOk = + ::CreateProcessAsUserW(mediumIlToken.get(), argv[0], cmdLine.get(), + nullptr, nullptr, inheritHandles, creationFlags, + nullptr, nullptr, &siex.StartupInfo, &pi); + } else { + createOk = ::CreateProcessW(argv[0], cmdLine.get(), nullptr, nullptr, + inheritHandles, creationFlags, nullptr, nullptr, + &siex.StartupInfo, &pi); + } + + if (!createOk) { + HandleLauncherError(LAUNCHER_ERROR_FROM_LAST()); + return Nothing(); + } + + nsAutoHandle process(pi.hProcess); + nsAutoHandle mainThread(pi.hThread); + + nsAutoHandle job; + if (flags & LauncherFlags::eWaitForBrowser) { + job = CreateJobAndAssignProcess(process.get()); + } + + bool disableDynamicBlocklist = IsDynamicBlocklistDisabled( + isSafeMode.value(), + mozilla::CheckArg( + argc, argv, mozilla::geckoargs::sDisableDynamicDllBlocklist.sMatch, + nullptr, mozilla::CheckArgFlag::None) == mozilla::ARG_FOUND); + LauncherVoidResult setupResult = PostCreationSetup( + argv[0], process.get(), mainThread.get(), deelevationStatus, + isSafeMode.value(), disableDynamicBlocklist, blocklistFileName); + if (setupResult.isErr()) { + HandleLauncherError(setupResult); + ::TerminateProcess(process.get(), 1); + return Nothing(); + } + + if (::ResumeThread(mainThread.get()) == static_cast(-1)) { + HandleLauncherError(LAUNCHER_ERROR_FROM_LAST()); + ::TerminateProcess(process.get(), 1); + return Nothing(); + } + + if (flags & LauncherFlags::eWaitForBrowser) { + DWORD exitCode; + if (::WaitForSingleObject(process.get(), INFINITE) == WAIT_OBJECT_0 && + ::GetExitCodeProcess(process.get(), &exitCode)) { + // Propagate the browser process's exit code as our exit code. + return Some(static_cast(exitCode)); + } + } else { + const DWORD timeout = + ::IsDebuggerPresent() ? INFINITE : kWaitForInputIdleTimeoutMS; + + // Keep the current process around until the callback process has created + // its message queue, to avoid the launched process's windows being forced + // into the background. + mozilla::WaitForInputIdle(process.get(), timeout); + } + + return Some(0); +} + +} // namespace mozilla diff --git a/browser/app/winlauncher/LauncherProcessWin.h b/browser/app/winlauncher/LauncherProcessWin.h new file mode 100644 index 0000000000..8fd53d7d3f --- /dev/null +++ b/browser/app/winlauncher/LauncherProcessWin.h @@ -0,0 +1,70 @@ +/* -*- 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_LauncherProcessWin_h +#define mozilla_LauncherProcessWin_h + +#include "mozilla/Maybe.h" +#include "mozilla/TypedEnumBits.h" + +#include + +namespace mozilla { + +// Forward declaration +struct StaticXREAppData; + +/** + * Determine whether or not the current process should be run as the launcher + * process, and run if so. If we are not supposed to run as the launcher + * process, or in the event of a launcher process failure, return Nothing, thus + * indicating that we should continue on the original startup code path. + */ +Maybe LauncherMain(int& argc, wchar_t* argv[], + const StaticXREAppData& aAppData); + +enum class LauncherFlags : uint32_t { + eNone = 0, + eWaitForBrowser = (1 << 0), // Launcher should block until browser finishes + eNoDeelevate = (1 << 1), // If elevated, do not attempt to de-elevate + eDeelevating = (1 << 2), // A de-elevation attempt has been made +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(LauncherFlags); + +enum class DeelevationStatus : uint32_t { + // The deelevation status could not be determined. Should never actually be + // the value of `gDeelevationStatus`. + Unknown = 0, + + // Deelevation did not need to be performed because the process was started + // without administrative privileges. + StartedUnprivileged = 1, + // Deelevation would have been performed, but was prohibited due to a flag. + DeelevationProhibited = 2, + // The launcher process was successfully deelevated. + SuccessfullyDeelevated = 3, + // The launcher process was not successfully deelevated, but a + // medium-integrity token was used to launch the main process. + PartiallyDeelevated = 4, + // Deelevation was attempted, but failed completely. The main process is + // running with administrative privileges. + UnsuccessfullyDeelevated = 5, + + // This is the static initial value of `gDeelevationStatus`; it acts as a + // sentinel to determine whether the launcher has set it at all. (It's + // therefore the normal value of `gDeelevationStatus` when the launcher is + // disabled.) + DefaultStaticValue = 0x55AA55AA, +}; + +// The result of the deelevation attempt. Set by the launcher process in the +// main process when the two are distinct. +extern const volatile DeelevationStatus gDeelevationStatus; + +} // namespace mozilla + +#endif // mozilla_LauncherProcessWin_h diff --git a/browser/app/winlauncher/NtLoaderAPI.cpp b/browser/app/winlauncher/NtLoaderAPI.cpp new file mode 100644 index 0000000000..97f8a20186 --- /dev/null +++ b/browser/app/winlauncher/NtLoaderAPI.cpp @@ -0,0 +1,33 @@ +/* -*- 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/LoaderAPIInterfaces.h" + +#include "freestanding/CheckForCaller.h" +#include "freestanding/LoaderPrivateAPI.h" + +namespace mozilla { + +extern "C" MOZ_EXPORT nt::LoaderAPI* GetNtLoaderAPI( + nt::LoaderObserver* aNewObserver) { + // Make sure the caller is inside mozglue.dll - we don't want to allow + // external access to this function, as it contains details about + // the SharedSection which is used to sandbox future child processes. + const bool isCallerMozglue = + CheckForAddress(RETURN_ADDRESS(), L"mozglue.dll"); + MOZ_ASSERT(isCallerMozglue); + if (!isCallerMozglue) { + return nullptr; + } + + freestanding::EnsureInitialized(); + freestanding::LoaderPrivateAPI& api = freestanding::gLoaderPrivateAPI; + api.SetObserver(aNewObserver); + + return &api; +} + +} // namespace mozilla diff --git a/browser/app/winlauncher/ProcThreadAttributes.h b/browser/app/winlauncher/ProcThreadAttributes.h new file mode 100644 index 0000000000..74d5fee06c --- /dev/null +++ b/browser/app/winlauncher/ProcThreadAttributes.h @@ -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/. */ + +#ifndef mozilla_ProcThreadAttributes_h +#define mozilla_ProcThreadAttributes_h + +#include + +#include + +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Vector.h" + +namespace mozilla { + +class MOZ_RAII ProcThreadAttributes final { + struct ProcThreadAttributeListDeleter { + void operator()(LPPROC_THREAD_ATTRIBUTE_LIST aList) { + ::DeleteProcThreadAttributeList(aList); + delete[] reinterpret_cast(aList); + } + }; + + using ProcThreadAttributeListPtr = + UniquePtr<_PROC_THREAD_ATTRIBUTE_LIST, ProcThreadAttributeListDeleter>; + + public: + ProcThreadAttributes() : mMitigationPolicies(0) {} + + ~ProcThreadAttributes() = default; + + ProcThreadAttributes(const ProcThreadAttributes&) = delete; + ProcThreadAttributes(ProcThreadAttributes&&) = delete; + ProcThreadAttributes& operator=(const ProcThreadAttributes&) = delete; + ProcThreadAttributes& operator=(ProcThreadAttributes&&) = delete; + + void AddMitigationPolicy(DWORD64 aPolicy) { mMitigationPolicies |= aPolicy; } + + bool AddInheritableHandle(HANDLE aHandle) { + DWORD type = ::GetFileType(aHandle); + if (type != FILE_TYPE_DISK && type != FILE_TYPE_PIPE) { + return false; + } + + if (!::SetHandleInformation(aHandle, HANDLE_FLAG_INHERIT, + HANDLE_FLAG_INHERIT)) { + return false; + } + + return mInheritableHandles.append(aHandle); + } + + template + bool AddInheritableHandles(HANDLE (&aHandles)[N]) { + bool ok = true; + for (auto handle : aHandles) { + ok &= AddInheritableHandle(handle); + } + + return ok; + } + + bool HasMitigationPolicies() const { return !!mMitigationPolicies; } + + bool HasInheritableHandles() const { return !mInheritableHandles.empty(); } + + /** + * @return false if the STARTUPINFOEXW::lpAttributeList was set to null + * as expected based on the state of |this|; + * true if the STARTUPINFOEXW::lpAttributeList was set to + * non-null; + */ + LauncherResult AssignTo(STARTUPINFOEXW& aSiex) { + ZeroMemory(&aSiex, sizeof(STARTUPINFOEXW)); + + // We'll set the size to sizeof(STARTUPINFOW) until we determine whether the + // extended fields will be used. + aSiex.StartupInfo.cb = sizeof(STARTUPINFOW); + + DWORD numAttributes = 0; + if (HasMitigationPolicies()) { + ++numAttributes; + } + + if (HasInheritableHandles()) { + ++numAttributes; + } + + if (!numAttributes) { + return false; + } + + SIZE_T listSize = 0; + if (!::InitializeProcThreadAttributeList(nullptr, numAttributes, 0, + &listSize)) { + DWORD err = ::GetLastError(); + if (err != ERROR_INSUFFICIENT_BUFFER) { + return LAUNCHER_ERROR_FROM_WIN32(err); + } + } + + auto buf = MakeUnique(listSize); + + LPPROC_THREAD_ATTRIBUTE_LIST tmpList = + reinterpret_cast(buf.get()); + + if (!::InitializeProcThreadAttributeList(tmpList, numAttributes, 0, + &listSize)) { + return LAUNCHER_ERROR_FROM_LAST(); + } + + // Transfer buf to a ProcThreadAttributeListPtr - now that the list is + // initialized, we are no longer dealing with a plain old char array. We + // must now deinitialize the attribute list before deallocating the + // underlying buffer. + ProcThreadAttributeListPtr attrList( + reinterpret_cast(buf.release())); + + if (mMitigationPolicies) { + if (!::UpdateProcThreadAttribute( + attrList.get(), 0, PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, + &mMitigationPolicies, sizeof(mMitigationPolicies), nullptr, + nullptr)) { + return LAUNCHER_ERROR_FROM_LAST(); + } + } + + if (!mInheritableHandles.empty()) { + if (!::UpdateProcThreadAttribute( + attrList.get(), 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + mInheritableHandles.begin(), + mInheritableHandles.length() * sizeof(HANDLE), nullptr, + nullptr)) { + return LAUNCHER_ERROR_FROM_LAST(); + } + } + + mAttrList = std::move(attrList); + aSiex.lpAttributeList = mAttrList.get(); + aSiex.StartupInfo.cb = sizeof(STARTUPINFOEXW); + return true; + } + + private: + static const uint32_t kNumInline = 3; // Inline storage for the std handles + + DWORD64 mMitigationPolicies; + Vector mInheritableHandles; + ProcThreadAttributeListPtr mAttrList; +}; + +} // namespace mozilla + +#endif // mozilla_ProcThreadAttributes_h diff --git a/browser/app/winlauncher/SameBinary.h b/browser/app/winlauncher/SameBinary.h new file mode 100644 index 0000000000..e8fa78600f --- /dev/null +++ b/browser/app/winlauncher/SameBinary.h @@ -0,0 +1,146 @@ +/* -*- 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_SameBinary_h +#define mozilla_SameBinary_h + +#include "mozilla/WinHeaderOnlyUtils.h" +#include "mozilla/NativeNt.h" +#include "nsWindowsHelpers.h" + +namespace mozilla { + +class ProcessImagePath final { + PathType mType; + LauncherVoidResult mLastError; + + // Using a larger buffer because an NT path may exceed MAX_PATH. + WCHAR mPathBuffer[(MAX_PATH * 2) + 1]; + + public: + // Initialize with an NT path string of a given process handle + explicit ProcessImagePath(const nsAutoHandle& aProcess) + : mType(PathType::eNtPath), mLastError(Ok()) { + DWORD len = mozilla::ArrayLength(mPathBuffer); + if (!::QueryFullProcessImageNameW(aProcess.get(), PROCESS_NAME_NATIVE, + mPathBuffer, &len)) { + mLastError = LAUNCHER_ERROR_FROM_LAST(); + return; + } + } + + // Initizlize with a DOS path string of a given imagebase address + explicit ProcessImagePath(HMODULE aImageBase) + : mType(PathType::eDosPath), mLastError(Ok()) { + DWORD len = ::GetModuleFileNameW(aImageBase, mPathBuffer, + mozilla::ArrayLength(mPathBuffer)); + if (!len || len == mozilla::ArrayLength(mPathBuffer)) { + mLastError = LAUNCHER_ERROR_FROM_LAST(); + return; + } + } + + bool IsError() const { return mLastError.isErr(); } + + const WindowsErrorType& GetError() const { return mLastError.inspectErr(); } + + FileUniqueId GetId() const { return FileUniqueId(mPathBuffer, mType); } + + bool CompareNtPaths(const ProcessImagePath& aOther) const { + if (mLastError.isErr() || aOther.mLastError.isErr() || + mType != PathType::eNtPath || aOther.mType != PathType::eNtPath) { + return false; + } + + UNICODE_STRING path1, path2; + ::RtlInitUnicodeString(&path1, mPathBuffer); + ::RtlInitUnicodeString(&path2, aOther.mPathBuffer); + return !!::RtlEqualUnicodeString(&path1, &path2, TRUE); + } +}; + +enum class ImageFileCompareOption { + Default, + CompareNtPathsOnly, +}; + +static inline mozilla::LauncherResult IsSameBinaryAsParentProcess( + ImageFileCompareOption aOption = ImageFileCompareOption::Default) { + mozilla::LauncherResult parentPid = mozilla::nt::GetParentProcessId(); + if (parentPid.isErr()) { + return parentPid.propagateErr(); + } + + nsAutoHandle parentProcess(::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, + FALSE, parentPid.unwrap())); + if (!parentProcess.get()) { + DWORD err = ::GetLastError(); + if (err == ERROR_INVALID_PARAMETER || err == ERROR_ACCESS_DENIED) { + // In the ERROR_INVALID_PARAMETER case, the process identified by + // parentPid has already exited. This is a common case when the parent + // process is not Firefox, thus we should return false instead of erroring + // out. + // The ERROR_ACCESS_DENIED case can happen when the parent process is + // something that we don't have permission to query. For example, we may + // encounter this when Firefox is launched by the Windows Task Scheduler. + return false; + } + + return LAUNCHER_ERROR_FROM_WIN32(err); + } + + ProcessImagePath parentExe(parentProcess); + if (parentExe.IsError()) { + return ::mozilla::Err(parentExe.GetError()); + } + + if (aOption == ImageFileCompareOption::Default) { + bool skipFileIdComparison = false; + + FileUniqueId id1 = parentExe.GetId(); + if (id1.IsError()) { + // We saw a number of Win7 users failed to call NtOpenFile with + // STATUS_OBJECT_PATH_NOT_FOUND for an unknown reason. In this + // particular case, we fall back to the logic to compare NT path + // strings instead of a file id which will not fail because we don't + // need to open a file handle. +#if !defined(STATUS_OBJECT_PATH_NOT_FOUND) + constexpr NTSTATUS STATUS_OBJECT_PATH_NOT_FOUND = 0xc000003a; +#endif + const LauncherError& err = id1.GetError(); + if (err.mError != + WindowsError::FromNtStatus(STATUS_OBJECT_PATH_NOT_FOUND)) { + return ::mozilla::Err(err); + } + + skipFileIdComparison = true; + } + + if (!skipFileIdComparison) { + ProcessImagePath ourExe(nullptr); + if (ourExe.IsError()) { + return ::mozilla::Err(ourExe.GetError()); + } + + FileUniqueId id2 = ourExe.GetId(); + if (id2.IsError()) { + return ::mozilla::Err(id2.GetError()); + } + return id1 == id2; + } + } + + nsAutoHandle ourProcess(::GetCurrentProcess()); + ProcessImagePath ourExeNt(ourProcess); + if (ourExeNt.IsError()) { + return ::mozilla::Err(ourExeNt.GetError()); + } + return parentExe.CompareNtPaths(ourExeNt); +} + +} // namespace mozilla + +#endif // mozilla_SameBinary_h diff --git a/browser/app/winlauncher/freestanding/CheckForCaller.h b/browser/app/winlauncher/freestanding/CheckForCaller.h new file mode 100644 index 0000000000..799d16cd1f --- /dev/null +++ b/browser/app/winlauncher/freestanding/CheckForCaller.h @@ -0,0 +1,36 @@ +/* -*- 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_freestanding_CheckForCaller_h +#define mozilla_freestanding_CheckForCaller_h + +namespace mozilla { + +#if defined(_MSC_VER) +# include +# pragma intrinsic(_ReturnAddress) +# define RETURN_ADDRESS() _ReturnAddress() +#elif defined(__GNUC__) || defined(__clang__) +# define RETURN_ADDRESS() \ + __builtin_extract_return_addr(__builtin_return_address(0)) +#endif + +template +bool CheckForAddress(void* aReturnAddress, const wchar_t (&aName)[N]) { + HMODULE callingModule; + if (!::GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(aReturnAddress), + &callingModule)) { + return false; + } + + return callingModule && callingModule == ::GetModuleHandleW(aName); +} + +} // namespace mozilla + +#endif // mozilla_freestanding_CheckForCaller_h diff --git a/browser/app/winlauncher/freestanding/DllBlocklist.cpp b/browser/app/winlauncher/freestanding/DllBlocklist.cpp new file mode 100644 index 0000000000..bfb3d6239b --- /dev/null +++ b/browser/app/winlauncher/freestanding/DllBlocklist.cpp @@ -0,0 +1,620 @@ +/* -*- 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/ArrayUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/BinarySearch.h" +#include "mozilla/NativeNt.h" +#include "mozilla/Types.h" +#include "mozilla/WindowsDllBlocklist.h" + +#include "CrashAnnotations.h" +#include "DllBlocklist.h" +#include "LoaderPrivateAPI.h" +#include "ModuleLoadFrame.h" +#include "SharedSection.h" + +using mozilla::DllBlockInfoFlags; + +#define DLL_BLOCKLIST_ENTRY(name, ...) \ + {MOZ_LITERAL_UNICODE_STRING(L##name), __VA_ARGS__}, +#define DLL_BLOCKLIST_STRING_TYPE UNICODE_STRING + +#if defined(MOZ_LAUNCHER_PROCESS) || defined(NIGHTLY_BUILD) +# include "mozilla/WindowsDllBlocklistLauncherDefs.h" +#else +# include "mozilla/WindowsDllBlocklistCommon.h" +DLL_BLOCKLIST_DEFINITIONS_BEGIN +DLL_BLOCKLIST_DEFINITIONS_END +#endif + +using WritableBuffer = mozilla::glue::detail::WritableBuffer<1024>; + +class MOZ_STATIC_CLASS MOZ_TRIVIAL_CTOR_DTOR NativeNtBlockSet final { + struct NativeNtBlockSetEntry { + NativeNtBlockSetEntry() = default; + ~NativeNtBlockSetEntry() = default; + NativeNtBlockSetEntry(const UNICODE_STRING& aName, uint64_t aVersion, + NativeNtBlockSetEntry* aNext) + : mName(aName), mVersion(aVersion), mNext(aNext) {} + UNICODE_STRING mName; + uint64_t mVersion; + NativeNtBlockSetEntry* mNext; + }; + + public: + // Constructor and destructor MUST be trivial + constexpr NativeNtBlockSet() : mFirstEntry(nullptr) {} + ~NativeNtBlockSet() = default; + + void Add(const UNICODE_STRING& aName, uint64_t aVersion); + void Write(WritableBuffer& buffer); + + private: + static NativeNtBlockSetEntry* NewEntry(const UNICODE_STRING& aName, + uint64_t aVersion, + NativeNtBlockSetEntry* aNextEntry); + + private: + NativeNtBlockSetEntry* mFirstEntry; + mozilla::nt::SRWLock mLock; +}; + +NativeNtBlockSet::NativeNtBlockSetEntry* NativeNtBlockSet::NewEntry( + const UNICODE_STRING& aName, uint64_t aVersion, + NativeNtBlockSet::NativeNtBlockSetEntry* aNextEntry) { + return mozilla::freestanding::RtlNew(aName, aVersion, + aNextEntry); +} + +void NativeNtBlockSet::Add(const UNICODE_STRING& aName, uint64_t aVersion) { + mozilla::nt::AutoExclusiveLock lock(mLock); + + for (NativeNtBlockSetEntry* entry = mFirstEntry; entry; + entry = entry->mNext) { + if (::RtlEqualUnicodeString(&entry->mName, &aName, TRUE) && + aVersion == entry->mVersion) { + return; + } + } + + // Not present, add it + NativeNtBlockSetEntry* newEntry = NewEntry(aName, aVersion, mFirstEntry); + if (newEntry) { + mFirstEntry = newEntry; + } +} + +void NativeNtBlockSet::Write(WritableBuffer& aBuffer) { + // NB: If this function is called, it is long after kernel32 is initialized, + // so it is safe to use Win32 calls here. + char buf[MAX_PATH]; + + // It would be nicer to use RAII here. However, its destructor + // might not run if an exception occurs, in which case we would never release + // the lock (MSVC warns about this possibility). So we acquire and release + // manually. + ::AcquireSRWLockExclusive(&mLock); + + MOZ_SEH_TRY { + for (auto entry = mFirstEntry; entry; entry = entry->mNext) { + int convOk = ::WideCharToMultiByte(CP_UTF8, 0, entry->mName.Buffer, + entry->mName.Length / sizeof(wchar_t), + buf, sizeof(buf), nullptr, nullptr); + if (!convOk) { + continue; + } + + // write name[,v.v.v.v]; + aBuffer.Write(buf, convOk); + + if (entry->mVersion != DllBlockInfo::ALL_VERSIONS) { + aBuffer.Write(",", 1); + uint16_t parts[4]; + parts[0] = entry->mVersion >> 48; + parts[1] = (entry->mVersion >> 32) & 0xFFFF; + parts[2] = (entry->mVersion >> 16) & 0xFFFF; + parts[3] = entry->mVersion & 0xFFFF; + for (size_t p = 0; p < mozilla::ArrayLength(parts); ++p) { + _ltoa_s(parts[p], buf, sizeof(buf), 10); + aBuffer.Write(buf, strlen(buf)); + if (p != mozilla::ArrayLength(parts) - 1) { + aBuffer.Write(".", 1); + } + } + } + aBuffer.Write(";", 1); + } + } + MOZ_SEH_EXCEPT(EXCEPTION_EXECUTE_HANDLER) {} + + ::ReleaseSRWLockExclusive(&mLock); +} + +static NativeNtBlockSet gBlockSet; + +extern "C" void MOZ_EXPORT +NativeNtBlockSet_Write(CrashReporter::AnnotationWriter& aWriter) { + WritableBuffer buffer; + gBlockSet.Write(buffer); + aWriter.Write(CrashReporter::Annotation::BlockedDllList, buffer.Data(), + buffer.Length()); +} + +enum class BlockAction { + // Allow the DLL to be loaded. + Allow, + // Substitute in a different DLL to be loaded instead of this one? + // This is intended to be used for Layered Service Providers, which + // cannot be blocked in the normal way. Note that this doesn't seem + // to be actually implemented right now, and no entries in the blocklist + // use it. + SubstituteLSP, + // There was an error in determining whether we should block this DLL. + // It will be blocked. + Error, + // Block the DLL from loading. + Deny, + // Effectively block the DLL from loading by redirecting its DllMain + // to a stub version. This is needed for DLLs that add themselves to + // the executable's Import Table, since failing to load would mean the + // executable would fail to launch. + NoOpEntryPoint, +}; + +static BlockAction CheckBlockInfo(const DllBlockInfo* aInfo, + const mozilla::nt::PEHeaders& aHeaders, + uint64_t& aVersion) { + aVersion = DllBlockInfo::ALL_VERSIONS; + + if ((aInfo->mFlags & DllBlockInfoFlags::CHILD_PROCESSES_ONLY) && + !(gBlocklistInitFlags & eDllBlocklistInitFlagIsChildProcess)) { + return BlockAction::Allow; + } + + if ((aInfo->mFlags & DllBlockInfoFlags::UTILITY_PROCESSES_ONLY) && + !(gBlocklistInitFlags & eDllBlocklistInitFlagIsUtilityProcess)) { + return BlockAction::Allow; + } + + if ((aInfo->mFlags & DllBlockInfoFlags::SOCKET_PROCESSES_ONLY) && + !(gBlocklistInitFlags & eDllBlocklistInitFlagIsSocketProcess)) { + return BlockAction::Allow; + } + + if ((aInfo->mFlags & DllBlockInfoFlags::GPU_PROCESSES_ONLY) && + !(gBlocklistInitFlags & eDllBlocklistInitFlagIsGPUProcess)) { + return BlockAction::Allow; + } + + if ((aInfo->mFlags & DllBlockInfoFlags::BROWSER_PROCESS_ONLY) && + (gBlocklistInitFlags & eDllBlocklistInitFlagIsChildProcess)) { + return BlockAction::Allow; + } + + if ((aInfo->mFlags & DllBlockInfoFlags::GMPLUGIN_PROCESSES_ONLY) && + !(gBlocklistInitFlags & eDllBlocklistInitFlagIsGMPluginProcess)) { + return BlockAction::Allow; + } + + if (aInfo->mMaxVersion == DllBlockInfo::ALL_VERSIONS) { + return BlockAction::Deny; + } + + if (!aHeaders) { + return BlockAction::Error; + } + + if (aInfo->mFlags & DllBlockInfoFlags::USE_TIMESTAMP) { + DWORD timestamp; + if (!aHeaders.GetTimeStamp(timestamp)) { + return BlockAction::Error; + } + + if (timestamp > aInfo->mMaxVersion) { + return BlockAction::Allow; + } + + return BlockAction::Deny; + } + + // Else we try to get the file version information. Note that we don't have + // access to GetFileVersionInfo* APIs. + if (!aHeaders.GetVersionInfo(aVersion)) { + return BlockAction::Error; + } + + if (aInfo->IsVersionBlocked(aVersion)) { + return BlockAction::Deny; + } + + return BlockAction::Allow; +} + +static BOOL WINAPI NoOp_DllMain(HINSTANCE, DWORD, LPVOID) { return TRUE; } + +// This helper function checks whether a given module is included +// in the executable's Import Table. Because an injected module's +// DllMain may revert the Import Table to the original state, we parse +// the Import Table every time a module is loaded without creating a cache. +static bool IsInjectedDependentModule( + const UNICODE_STRING& aModuleLeafName, + mozilla::freestanding::Kernel32ExportsSolver& aK32Exports) { + mozilla::nt::PEHeaders exeHeaders(aK32Exports.mGetModuleHandleW(nullptr)); + if (!exeHeaders || !exeHeaders.IsImportDirectoryTampered()) { + // If no tampering is detected, no need to enumerate the Import Table. + return false; + } + + bool isDependent = false; + exeHeaders.EnumImportChunks( + [&isDependent, &aModuleLeafName, &exeHeaders](const char* aDepModule) { + // If |aDepModule| is within the PE image, it's not an injected module + // but a legitimate dependent module. + if (isDependent || exeHeaders.IsWithinImage(aDepModule)) { + return; + } + + UNICODE_STRING depModuleLeafName; + mozilla::nt::AllocatedUnicodeString depModuleName(aDepModule); + mozilla::nt::GetLeafName(&depModuleLeafName, depModuleName); + isDependent = (::RtlCompareUnicodeString( + &aModuleLeafName, &depModuleLeafName, TRUE) == 0); + }); + return isDependent; +} + +// Allowing a module to be loaded but detour the entrypoint to NoOp_DllMain +// so that the module has no chance to interact with our code. We need this +// technique to safely block a module injected by IAT tampering because +// blocking such a module makes a process fail to launch. +static bool RedirectToNoOpEntryPoint( + const mozilla::nt::PEHeaders& aModule, + mozilla::freestanding::Kernel32ExportsSolver& aK32Exports) { + mozilla::interceptor::WindowsDllEntryPointInterceptor interceptor( + aK32Exports); + if (!interceptor.Set(aModule, NoOp_DllMain)) { + return false; + } + + return true; +} + +static BlockAction DetermineBlockAction( + const UNICODE_STRING& aLeafName, void* aBaseAddress, + mozilla::freestanding::Kernel32ExportsSolver* aK32Exports) { + if (mozilla::nt::Contains12DigitHexString(aLeafName) || + mozilla::nt::IsFileNameAtLeast16HexDigits(aLeafName)) { + return BlockAction::Deny; + } + + mozilla::nt::PEHeaders headers(aBaseAddress); + DWORD checksum = 0; + DWORD timestamp = 0; + DWORD imageSize = 0; + uint64_t version = 0; + + // Block some malicious DLLs known for crashing our process (bug 1841751), + // based on matching the combination of version number + timestamp + image + // size. We further reduce the chances of collision with legit DLLs by + // checking for a checksum of 0 and the absence of debug information, both of + // which are unusual for production-ready DLLs. + if (headers.GetCheckSum(checksum) && checksum == 0 && !headers.GetPdbInfo() && + headers.GetTimeStamp(timestamp)) { + struct KnownMaliciousCombination { + uint64_t mVersion; + uint32_t mTimestamp; + uint32_t mImageSize; + }; + const KnownMaliciousCombination instances[]{ + // 1.0.0.26638 + {0x000100000000680e, 0x570B8A90, 0x62000}, + // 1.0.0.26793 + {0x00010000000068a9, 0x572B4CE4, 0x62000}, + // 1.0.0.27567 + {0x0001000000006baf, 0x57A725AC, 0x61000}, + // 1.0.0.29915 + {0x00010000000074db, 0x5A115D81, 0x5D000}, + // 1.0.0.31122 + {0x0001000000007992, 0x5CFF88B8, 0x5D000}}; + + // We iterate over timestamps, because they are unique and it is a quick + // field to fetch + for (const auto& instance : instances) { + if (instance.mTimestamp == timestamp) { + // Only fetch other fields in case we have a match. Then, we can exit + // the loop. + if (headers.GetImageSize(imageSize) && + instance.mImageSize == imageSize && + headers.GetVersionInfo(version) && instance.mVersion == version) { + return BlockAction::Deny; + } + break; + } + } + } + + DECLARE_POINTER_TO_FIRST_DLL_BLOCKLIST_ENTRY(info); + DECLARE_DLL_BLOCKLIST_NUM_ENTRIES(infoNumEntries); + + mozilla::freestanding::DllBlockInfoComparator comp(aLeafName); + + size_t match = LowerBound(info, 0, infoNumEntries, comp); + bool builtinListHasLowerBound = match != infoNumEntries; + const DllBlockInfo* entry = nullptr; + BlockAction checkResult = BlockAction::Allow; + if (builtinListHasLowerBound) { + // 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)) { + entry = &info[match]; + checkResult = CheckBlockInfo(entry, headers, version); + if (checkResult != BlockAction::Allow) { + break; + } + ++match; + } + } + mozilla::DebugOnly blockedByDynamicBlocklist = false; + // Make sure we handle a case that older versions are blocked by the static + // list, but the dynamic list blocks all versions. + if (checkResult == BlockAction::Allow) { + if (!mozilla::freestanding::gSharedSection.IsDisabled()) { + entry = mozilla::freestanding::gSharedSection.SearchBlocklist(aLeafName); + if (entry) { + checkResult = CheckBlockInfo(entry, headers, version); + blockedByDynamicBlocklist = checkResult != BlockAction::Allow; + } + } + } + if (checkResult == BlockAction::Allow) { + return BlockAction::Allow; + } + + gBlockSet.Add(entry->mName, version); + + if ((entry->mFlags & DllBlockInfoFlags::REDIRECT_TO_NOOP_ENTRYPOINT) && + aK32Exports && RedirectToNoOpEntryPoint(headers, *aK32Exports)) { + MOZ_ASSERT(!blockedByDynamicBlocklist, "dynamic blocklist has redirect?"); + return BlockAction::NoOpEntryPoint; + } + + return checkResult; +} + +namespace mozilla { +namespace freestanding { + +CrossProcessDllInterceptor::FuncHookType stub_LdrLoadDll; + +NTSTATUS NTAPI patched_LdrLoadDll(PWCHAR aDllPath, PULONG aFlags, + PUNICODE_STRING aDllName, + PHANDLE aOutHandle) { + ModuleLoadFrame frame(aDllName); + + NTSTATUS ntStatus = stub_LdrLoadDll(aDllPath, aFlags, aDllName, aOutHandle); + + return frame.SetLoadStatus(ntStatus, aOutHandle); +} + +CrossProcessDllInterceptor::FuncHookType + stub_NtMapViewOfSection; + +// All the code for patched_NtMapViewOfSection that relies on checked stack +// buffers (e.g. mbi, sectionFileName) should be put in this helper function +// (see bug 1733532). +MOZ_NEVER_INLINE NTSTATUS AfterMapViewOfExecutableSection( + HANDLE aProcess, PVOID* aBaseAddress, NTSTATUS aStubStatus) { + // We don't care about mappings that aren't MEM_IMAGE. + MEMORY_BASIC_INFORMATION mbi; + NTSTATUS ntStatus = + ::NtQueryVirtualMemory(aProcess, *aBaseAddress, MemoryBasicInformation, + &mbi, sizeof(mbi), nullptr); + if (!NT_SUCCESS(ntStatus)) { + ::NtUnmapViewOfSection(aProcess, *aBaseAddress); + return STATUS_ACCESS_DENIED; + } + if (!(mbi.Type & MEM_IMAGE)) { + return aStubStatus; + } + + // Get the section name + nt::MemorySectionNameBuf sectionFileName( + gLoaderPrivateAPI.GetSectionNameBuffer(*aBaseAddress)); + if (sectionFileName.IsEmpty()) { + ::NtUnmapViewOfSection(aProcess, *aBaseAddress); + return STATUS_ACCESS_DENIED; + } + + // Find the leaf name + UNICODE_STRING leafOnStack; + nt::GetLeafName(&leafOnStack, sectionFileName); + + bool isInjectedDependent = false; + const UNICODE_STRING k32Name = MOZ_LITERAL_UNICODE_STRING(L"kernel32.dll"); + Kernel32ExportsSolver* k32Exports = nullptr; + BlockAction blockAction; + // Trying to get the Kernel32Exports while loading kernel32.dll causes Firefox + // to crash. (but only during a profile-guided optimization run, oddly) We + // know we're never going to block kernel32.dll, so skip all this + if (::RtlCompareUnicodeString(&k32Name, &leafOnStack, TRUE) == 0) { + blockAction = BlockAction::Allow; + } else { + auto noSharedSectionReset{SharedSection::AutoNoReset()}; + k32Exports = gSharedSection.GetKernel32Exports(); + // Small optimization: Since loading a dependent module does not involve + // LdrLoadDll, we know isInjectedDependent is false if we hold a top frame. + if (k32Exports && !ModuleLoadFrame::ExistsTopFrame()) { + // Note that if a module is dependent but not injected, this means that + // the executable built against it, and it should be signed by Mozilla + // or Microsoft, so we don't need to worry about adding it to the list + // for CIG. (and users won't be able to block it) So the only special + // case here is a dependent module that has been injected. + isInjectedDependent = IsInjectedDependentModule(leafOnStack, *k32Exports); + } + + if (isInjectedDependent) { + // Add an NT dv\path to the shared section so that a sandbox process can + // use it to bypass CIG. In a sandbox process, this addition fails + // because we cannot map the section to a writable region, but it's + // ignorable because the paths have been added by the browser process. + Unused << SharedSection::AddDependentModule(sectionFileName); + + bool attemptToBlockViaRedirect; +#if defined(NIGHTLY_BUILD) + // We enable automatic DLL blocking only in Nightly for now + // because it caused a compat issue (bug 1682304 and 1704373). + attemptToBlockViaRedirect = true; + // We will set blockAction below in the if (attemptToBlockViaRedirect) + // block, but I guess the compiler isn't smart enough to figure + // that out and complains about an uninitialized variable :-( + blockAction = BlockAction::NoOpEntryPoint; +#else + // Check blocklist + blockAction = + DetermineBlockAction(leafOnStack, *aBaseAddress, k32Exports); + // If we were going to block this dependent module, try redirection + // instead of blocking it, since blocking it would cause the .exe not to + // launch. + // Note tht Deny and Error both end up blocking the module in a + // straightforward way, so those are the cases in which we need + // to redirect instead. + attemptToBlockViaRedirect = + blockAction == BlockAction::Deny || blockAction == BlockAction::Error; +#endif + if (attemptToBlockViaRedirect) { + // For a dependent module, try redirection instead of blocking it. + // If we fail, we reluctantly allow the module for free. + mozilla::nt::PEHeaders headers(*aBaseAddress); + blockAction = RedirectToNoOpEntryPoint(headers, *k32Exports) + ? BlockAction::NoOpEntryPoint + : BlockAction::Allow; + } + } else { + // Check blocklist + blockAction = + DetermineBlockAction(leafOnStack, *aBaseAddress, k32Exports); + } + } + + ModuleLoadInfo::Status loadStatus = ModuleLoadInfo::Status::Blocked; + + switch (blockAction) { + case BlockAction::Allow: + loadStatus = ModuleLoadInfo::Status::Loaded; + break; + + case BlockAction::NoOpEntryPoint: + loadStatus = ModuleLoadInfo::Status::Redirected; + break; + + case BlockAction::SubstituteLSP: + // The process heap needs to be available here because + // NotifyLSPSubstitutionRequired below copies a given string into + // the heap. We use a soft assert here, assuming LSP load always + // occurs after the heap is initialized. + MOZ_ASSERT(nt::RtlGetProcessHeap()); + + // Notify patched_LdrLoadDll that it will be necessary to perform + // a substitution before returning. + ModuleLoadFrame::NotifyLSPSubstitutionRequired(&leafOnStack); + break; + + default: + break; + } + + if (nt::RtlGetProcessHeap()) { + ModuleLoadFrame::NotifySectionMap( + nt::AllocatedUnicodeString(sectionFileName), *aBaseAddress, aStubStatus, + loadStatus, isInjectedDependent); + } + + if (loadStatus == ModuleLoadInfo::Status::Loaded || + loadStatus == ModuleLoadInfo::Status::Redirected) { + return aStubStatus; + } + + ::NtUnmapViewOfSection(aProcess, *aBaseAddress); + return STATUS_ACCESS_DENIED; +} + +// To preserve compatibility with third-parties, calling into this function +// must not use checked stack buffers when reached through Thread32Next (see +// bug 1733532). Hence this function is declared as MOZ_NO_STACK_PROTECTOR. +// Ideally, all code relying on stack buffers should be put in the dedicated +// helper function AfterMapViewOfExecutableImageSection, which does not have +// the MOZ_NO_STACK_PROTECTOR attribute. The obi variable below is an +// exception to this rule, as it is required to collect the information that +// lets us decide whether we really need to go through the helper function. +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) { + // Save off the values currently in the out-pointers for later restoration if + // we decide not to permit this mapping. + auto const rollback = + [ + // Avoid taking a reference to the stack frame, mostly out of + // paranoia. (The values of `aBaseAddress` et al. may have been + // crafted to point to our return address anyway...) + =, + // `NtMapViewOfSection` itself is mildly robust to invalid pointers; + // we can't easily do that, but we can at least check for `nullptr`. + baseAddress = aBaseAddress ? *aBaseAddress : nullptr, + sectionOffset = aSectionOffset ? *aSectionOffset : LARGE_INTEGER{}, + viewSize = aViewSize ? *aViewSize : 0]() { + if (aBaseAddress) *aBaseAddress = baseAddress; + if (aSectionOffset) *aSectionOffset = sectionOffset; + if (aViewSize) *aViewSize = viewSize; + }; + + // We always map first, then we check for additional info after. + NTSTATUS stubStatus = stub_NtMapViewOfSection( + aSection, aProcess, aBaseAddress, aZeroBits, aCommitSize, aSectionOffset, + aViewSize, aInheritDisposition, aAllocationType, aProtectionFlags); + if (!NT_SUCCESS(stubStatus)) { + return stubStatus; + } + + if (aProcess != nt::kCurrentProcess) { + // We're only interested in mapping for the current process. + return stubStatus; + } + + PUBLIC_OBJECT_BASIC_INFORMATION obi; + NTSTATUS ntStatus = ::NtQueryObject(aSection, ObjectBasicInformation, &obi, + sizeof(obi), nullptr); + if (!NT_SUCCESS(ntStatus)) { + ::NtUnmapViewOfSection(aProcess, *aBaseAddress); + rollback(); + return STATUS_ACCESS_DENIED; + } + + // We don't care about sections for which the permission to map executable + // views was not asked at creation time. This early exit path is notably + // taken for: + // - calls to LoadLibraryExW using LOAD_LIBRARY_AS_DATAFILE or + // LOAD_LIBRARY_AS_IMAGE_RESOURCE (bug 1842088), thus allowing us to load + // blocked DLLs for analysis without executing them; + // - calls to Thread32Next (bug 1733532), thus avoiding the helper function + // with stack cookie checks. + if (!(obi.GrantedAccess & SECTION_MAP_EXECUTE)) { + return stubStatus; + } + + NTSTATUS rv = + AfterMapViewOfExecutableSection(aProcess, aBaseAddress, stubStatus); + if (FAILED(rv)) { + rollback(); + } + return rv; +} + +} // namespace freestanding +} // namespace mozilla diff --git a/browser/app/winlauncher/freestanding/DllBlocklist.h b/browser/app/winlauncher/freestanding/DllBlocklist.h new file mode 100644 index 0000000000..c0ae1d255f --- /dev/null +++ b/browser/app/winlauncher/freestanding/DllBlocklist.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_freestanding_DllBlocklist_h +#define mozilla_freestanding_DllBlocklist_h + +#include "mozilla/Attributes.h" +#include "mozilla/NativeNt.h" +#include "nsWindowsDllInterceptor.h" +#include "mozilla/WinHeaderOnlyUtils.h" + +namespace mozilla { +namespace freestanding { + +NTSTATUS NTAPI patched_LdrLoadDll(PWCHAR aDllPath, PULONG aFlags, + PUNICODE_STRING aDllName, PHANDLE aOutHandle); + +MOZ_NO_STACK_PROTECTOR 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); + +using LdrLoadDllPtr = decltype(&::LdrLoadDll); + +extern CrossProcessDllInterceptor::FuncHookType stub_LdrLoadDll; + +using NtMapViewOfSectionPtr = decltype(&::NtMapViewOfSection); + +extern CrossProcessDllInterceptor::FuncHookType + stub_NtMapViewOfSection; + +} // namespace freestanding +} // namespace mozilla + +#endif // mozilla_freestanding_DllBlocklist_h diff --git a/browser/app/winlauncher/freestanding/Freestanding.h b/browser/app/winlauncher/freestanding/Freestanding.h new file mode 100644 index 0000000000..03999f231e --- /dev/null +++ b/browser/app/winlauncher/freestanding/Freestanding.h @@ -0,0 +1,67 @@ +/* -*- 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_freestanding_Freestanding_h +#define mozilla_freestanding_Freestanding_h + +/** + * This header is automatically included in all source code residing in the + * /browser/app/winlauncher/freestanding directory. + */ + +#if defined(__STDC_HOSTED__) && __STDC_HOSTED__ == 1 +# error "This header should only be included by freestanding code" +#endif // defined(__STDC_HOSTED__) && __STDC_HOSTED__ == 1 + +#define MOZ_USE_LAUNCHER_ERROR +#include "mozilla/NativeNt.h" + +namespace mozilla { +namespace freestanding { + +/** + * Since this library is the only part of firefox.exe that needs special + * treatment with respect to the heap, we implement |RtlNew| and |RtlDelete| + * to be used instead of |new| and |delete| for any heap allocations inside + * the freestanding library. + */ +template +inline static T* RtlNew(Args&&... aArgs) { + HANDLE processHeap = nt::RtlGetProcessHeap(); + if (!processHeap) { + // Handle the case where the process heap is not initialized because + // passing nullptr to RtlAllocateHeap crashes the process. + return nullptr; + } + + void* ptr = ::RtlAllocateHeap(processHeap, 0, sizeof(T)); + if (!ptr) { + return nullptr; + } + + return new (ptr) T(std::forward(aArgs)...); +} + +template +inline static void RtlDelete(T* aPtr) { + if (!aPtr) { + return; + } + + aPtr->~T(); + ::RtlFreeHeap(nt::RtlGetProcessHeap(), 0, aPtr); +} + +} // namespace freestanding +} // namespace mozilla + +// Initialization code for all statically-allocated data in freestanding is +// placed into a separate section. This allows us to initialize any +// freestanding statics without needing to initialize everything else in this +// binary. +#pragma init_seg(".freestd$g") + +#endif // mozilla_freestanding_Freestanding_h diff --git a/browser/app/winlauncher/freestanding/LoaderPrivateAPI.cpp b/browser/app/winlauncher/freestanding/LoaderPrivateAPI.cpp new file mode 100644 index 0000000000..fb65a2d405 --- /dev/null +++ b/browser/app/winlauncher/freestanding/LoaderPrivateAPI.cpp @@ -0,0 +1,314 @@ +/* -*- 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 "LoaderPrivateAPI.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Types.h" +#include "mozilla/Unused.h" +#include "../DllBlocklistInit.h" +#include "../ErrorHandler.h" +#include "SharedSection.h" + +using GlobalInitializerFn = void(__cdecl*)(void); + +// Allocation of static initialization section for the freestanding library +#pragma section(".freestd$a", read) +__declspec(allocate(".freestd$a")) static const GlobalInitializerFn + FreeStdStart = reinterpret_cast(0); + +#pragma section(".freestd$z", read) +__declspec(allocate(".freestd$z")) static const GlobalInitializerFn FreeStdEnd = + reinterpret_cast(0); + +namespace mozilla { +namespace freestanding { + +static RTL_RUN_ONCE gRunOnce = RTL_RUN_ONCE_INIT; + +// The contract for this callback is identical to the InitOnceCallback from +// Win32 land; we're just using ntdll-layer types instead. +static ULONG NTAPI DoOneTimeInit(PRTL_RUN_ONCE aRunOnce, PVOID aParameter, + PVOID* aContext) { + // Invoke every static initializer in the .freestd section + const GlobalInitializerFn* cur = &FreeStdStart + 1; + while (cur < &FreeStdEnd) { + if (*cur) { + (*cur)(); + } + + ++cur; + } + + return TRUE; +} + +/** + * This observer is only used until the mozglue observer connects itself. + * All we do here is accumulate the module loads into a vector. + * As soon as mozglue connects, we call |Forward| on mozglue's LoaderObserver + * to pass our vector on for further processing. This object then becomes + * defunct. + */ +class MOZ_ONLY_USED_TO_AVOID_STATIC_CONSTRUCTORS DefaultLoaderObserver final + : public nt::LoaderObserver { + public: + constexpr DefaultLoaderObserver() : mModuleLoads(nullptr) {} + + void OnBeginDllLoad(void** aContext, + PCUNICODE_STRING aRequestedDllName) final {} + bool SubstituteForLSP(PCUNICODE_STRING aLSPLeafName, + PHANDLE aOutHandle) final { + return false; + } + void OnEndDllLoad(void* aContext, NTSTATUS aNtStatus, + ModuleLoadInfo&& aModuleLoadInfo) final; + void Forward(nt::LoaderObserver* aNext) final; + void OnForward(ModuleLoadInfoVec&& aInfo) final { + MOZ_ASSERT_UNREACHABLE("Not valid in freestanding::DefaultLoaderObserver"); + } + + private: + mozilla::nt::SRWLock mLock; + ModuleLoadInfoVec* mModuleLoads; +}; + +class MOZ_ONLY_USED_TO_AVOID_STATIC_CONSTRUCTORS LoaderPrivateAPIImp final + : public LoaderPrivateAPI { + public: + // LoaderAPI + 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; + + // LoaderPrivateAPI + void NotifyBeginDllLoad(void** aContext, + PCUNICODE_STRING aRequestedDllName) final; + void NotifyBeginDllLoad(ModuleLoadInfo& aModuleLoadInfo, void** aContext, + PCUNICODE_STRING aRequestedDllName) final; + void SetObserver(nt::LoaderObserver* aNewObserver) final; + bool IsDefaultObserver() const final; + nt::MemorySectionNameBuf GetSectionNameBuffer(void* aSectionAddr) final; +}; + +static void Init() { + DebugOnly ntStatus = + ::RtlRunOnceExecuteOnce(&gRunOnce, &DoOneTimeInit, nullptr, nullptr); + MOZ_ASSERT(NT_SUCCESS(ntStatus)); +} + +} // namespace freestanding +} // namespace mozilla + +static mozilla::freestanding::DefaultLoaderObserver gDefaultObserver; +static mozilla::freestanding::LoaderPrivateAPIImp gPrivateAPI; + +static mozilla::nt::SRWLock gLoaderObserverLock; +static mozilla::nt::LoaderObserver* gLoaderObserver = &gDefaultObserver; +static CONDITION_VARIABLE gLoaderObserverNoOngoingLoadsCV = + CONDITION_VARIABLE_INIT; +static mozilla::Atomic gLoaderObserverOngoingLoadsCount(0); + +namespace mozilla { +namespace freestanding { + +LoaderPrivateAPI& gLoaderPrivateAPI = gPrivateAPI; + +void DefaultLoaderObserver::OnEndDllLoad(void* aContext, NTSTATUS aNtStatus, + ModuleLoadInfo&& aModuleLoadInfo) { + // If the DLL load failed, or if the DLL was loaded by a previous request + // and thus was not mapped by this request, we do not save the ModuleLoadInfo. + if (!NT_SUCCESS(aNtStatus) || !aModuleLoadInfo.WasMapped()) { + return; + } + + nt::AutoExclusiveLock lock(mLock); + if (!mModuleLoads) { + mModuleLoads = RtlNew(); + if (!mModuleLoads) { + return; + } + } + + Unused << mModuleLoads->emplaceBack( + std::forward(aModuleLoadInfo)); +} + +/** + * Pass mModuleLoads's data off to |aNext| for further processing. + */ +void DefaultLoaderObserver::Forward(nt::LoaderObserver* aNext) { + MOZ_ASSERT(aNext); + if (!aNext) { + return; + } + + ModuleLoadInfoVec* moduleLoads = nullptr; + + { // Scope for lock + nt::AutoExclusiveLock lock(mLock); + moduleLoads = mModuleLoads; + mModuleLoads = nullptr; + } + + if (!moduleLoads) { + return; + } + + aNext->OnForward(std::move(*moduleLoads)); + RtlDelete(moduleLoads); +} + +ModuleLoadInfo LoaderPrivateAPIImp::ConstructAndNotifyBeginDllLoad( + void** aContext, PCUNICODE_STRING aRequestedDllName) { + ModuleLoadInfo loadInfo(aRequestedDllName); + + NotifyBeginDllLoad(loadInfo, aContext, aRequestedDllName); + + return loadInfo; +} + +bool LoaderPrivateAPIImp::SubstituteForLSP(PCUNICODE_STRING aLSPLeafName, + PHANDLE aOutHandle) { + // This method should only be called between NotifyBeginDllLoad and + // NotifyEndDllLoad, so it is already protected against gLoaderObserver + // change, through gLoaderObserverOngoingLoadsCount. + MOZ_RELEASE_ASSERT(gLoaderObserverOngoingLoadsCount); + + return gLoaderObserver->SubstituteForLSP(aLSPLeafName, aOutHandle); +} + +void LoaderPrivateAPIImp::NotifyEndDllLoad(void* aContext, + NTSTATUS aLoadNtStatus, + ModuleLoadInfo&& aModuleLoadInfo) { + aModuleLoadInfo.SetEndLoadTimeStamp(); + + if (NT_SUCCESS(aLoadNtStatus)) { + aModuleLoadInfo.CaptureBacktrace(); + } + + // This method should only be called after a matching call to + // NotifyBeginDllLoad, so it is already protected against gLoaderObserver + // change, through gLoaderObserverOngoingLoadsCount. + + // We need to notify the observer that the DLL load has ended even when + // |aLoadNtStatus| indicates a failure. This is to ensure that any resources + // acquired by the observer during OnBeginDllLoad are cleaned up. + gLoaderObserver->OnEndDllLoad(aContext, aLoadNtStatus, + std::move(aModuleLoadInfo)); + + auto previousValue = gLoaderObserverOngoingLoadsCount--; + MOZ_RELEASE_ASSERT(previousValue); + if (previousValue == 1) { + ::RtlWakeAllConditionVariable(&gLoaderObserverNoOngoingLoadsCV); + } +} + +nt::AllocatedUnicodeString LoaderPrivateAPIImp::GetSectionName( + void* aSectionAddr) { + const HANDLE kCurrentProcess = reinterpret_cast(-1); + + nt::MemorySectionNameBuf buf; + NTSTATUS ntStatus = + ::NtQueryVirtualMemory(kCurrentProcess, aSectionAddr, MemorySectionName, + &buf, sizeof(buf), nullptr); + if (!NT_SUCCESS(ntStatus)) { + return nt::AllocatedUnicodeString(); + } + + return nt::AllocatedUnicodeString(&buf.mSectionFileName); +} + +nt::LoaderAPI::InitDllBlocklistOOPFnPtr +LoaderPrivateAPIImp::GetDllBlocklistInitFn() { + return &InitializeDllBlocklistOOP; +} + +nt::LoaderAPI::HandleLauncherErrorFnPtr +LoaderPrivateAPIImp::GetHandleLauncherErrorFn() { + return &HandleLauncherError; +} + +nt::SharedSection* LoaderPrivateAPIImp::GetSharedSection() { + return &gSharedSection; +} + +nt::MemorySectionNameBuf LoaderPrivateAPIImp::GetSectionNameBuffer( + void* aSectionAddr) { + const HANDLE kCurrentProcess = reinterpret_cast(-1); + + nt::MemorySectionNameBuf buf; + NTSTATUS ntStatus = + ::NtQueryVirtualMemory(kCurrentProcess, aSectionAddr, MemorySectionName, + &buf, sizeof(buf), nullptr); + if (!NT_SUCCESS(ntStatus)) { + return nt::MemorySectionNameBuf(); + } + + return buf; +} + +void LoaderPrivateAPIImp::NotifyBeginDllLoad( + void** aContext, PCUNICODE_STRING aRequestedDllName) { + nt::AutoSharedLock lock(gLoaderObserverLock); + ++gLoaderObserverOngoingLoadsCount; + gLoaderObserver->OnBeginDllLoad(aContext, aRequestedDllName); +} + +void LoaderPrivateAPIImp::NotifyBeginDllLoad( + ModuleLoadInfo& aModuleLoadInfo, void** aContext, + PCUNICODE_STRING aRequestedDllName) { + NotifyBeginDllLoad(aContext, aRequestedDllName); + aModuleLoadInfo.SetBeginLoadTimeStamp(); +} + +void LoaderPrivateAPIImp::SetObserver(nt::LoaderObserver* aNewObserver) { + nt::LoaderObserver* prevLoaderObserver = nullptr; + + nt::AutoExclusiveLock lock(gLoaderObserverLock); + while (gLoaderObserverOngoingLoadsCount) { + NTSTATUS status = ::RtlSleepConditionVariableSRW( + &gLoaderObserverNoOngoingLoadsCV, &gLoaderObserverLock, nullptr, 0); + MOZ_RELEASE_ASSERT(NT_SUCCESS(status) && status != STATUS_TIMEOUT); + } + + MOZ_ASSERT(aNewObserver); + if (!aNewObserver) { + // This is unlikely, but we always want a valid observer, so use the + // gDefaultObserver if necessary. + gLoaderObserver = &gDefaultObserver; + return; + } + + prevLoaderObserver = gLoaderObserver; + gLoaderObserver = aNewObserver; + + MOZ_ASSERT(prevLoaderObserver); + if (!prevLoaderObserver) { + return; + } + + // Now that we have a new observer, the previous observer must forward its + // data on to the new observer for processing. + prevLoaderObserver->Forward(aNewObserver); +} + +bool LoaderPrivateAPIImp::IsDefaultObserver() const { + nt::AutoSharedLock lock(gLoaderObserverLock); + return gLoaderObserver == &gDefaultObserver; +} + +void EnsureInitialized() { Init(); } + +} // namespace freestanding +} // namespace mozilla diff --git a/browser/app/winlauncher/freestanding/LoaderPrivateAPI.h b/browser/app/winlauncher/freestanding/LoaderPrivateAPI.h new file mode 100644 index 0000000000..f21472d689 --- /dev/null +++ b/browser/app/winlauncher/freestanding/LoaderPrivateAPI.h @@ -0,0 +1,62 @@ +/* -*- 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_freestanding_LoaderPrivateAPI_h +#define mozilla_freestanding_LoaderPrivateAPI_h + +#include "mozilla/LoaderAPIInterfaces.h" + +namespace mozilla { +namespace freestanding { + +/** + * This part of the API is available only to the launcher process. + */ +class NS_NO_VTABLE LoaderPrivateAPI : public nt::LoaderAPI { + public: + /** + * Notify the nt::LoaderObserver that a module load is beginning + */ + virtual void NotifyBeginDllLoad(void** aContext, + PCUNICODE_STRING aRequestedDllName) = 0; + /** + * Notify the nt::LoaderObserver that a module load is beginning and set the + * begin load timestamp on |aModuleLoadInfo|. + */ + virtual void NotifyBeginDllLoad(ModuleLoadInfo& aModuleLoadInfo, + void** aContext, + PCUNICODE_STRING aRequestedDllName) = 0; + + /** + * Set a new nt::LoaderObserver to be used by the launcher process. NB: This + * should only happen while the current process is still single-threaded! + */ + virtual void SetObserver(nt::LoaderObserver* aNewObserver) = 0; + + /** + * Returns true if the current nt::LoaderObserver is the launcher process's + * built-in observer. + */ + virtual bool IsDefaultObserver() const = 0; + + /** + * Returns the name of a given mapped section address as a local instance of + * nt::MemorySectionNameBuf. This does not involve heap allocation. + */ + virtual nt::MemorySectionNameBuf GetSectionNameBuffer(void* aSectionAddr) = 0; +}; + +/** + * Ensures that any statics in the freestanding library are initialized. + */ +void EnsureInitialized(); + +extern LoaderPrivateAPI& gLoaderPrivateAPI; + +} // namespace freestanding +} // namespace mozilla + +#endif // mozilla_freestanding_LoaderPrivateAPI_h diff --git a/browser/app/winlauncher/freestanding/ModuleLoadFrame.cpp b/browser/app/winlauncher/freestanding/ModuleLoadFrame.cpp new file mode 100644 index 0000000000..3aa043b8c7 --- /dev/null +++ b/browser/app/winlauncher/freestanding/ModuleLoadFrame.cpp @@ -0,0 +1,144 @@ +/* -*- 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 "ModuleLoadFrame.h" + +#include "LoaderPrivateAPI.h" + +namespace mozilla { +namespace freestanding { + +ModuleLoadFrame::ModuleLoadFrame(PCUNICODE_STRING aRequestedDllName) + : mPrev(sTopFrame.get()), + mContext(nullptr), + mLSPSubstitutionRequired(false), + mLoadNtStatus(STATUS_UNSUCCESSFUL), + mLoadInfo(aRequestedDllName) { + EnsureInitialized(); + sTopFrame.set(this); + + gLoaderPrivateAPI.NotifyBeginDllLoad(mLoadInfo, &mContext, aRequestedDllName); +} + +ModuleLoadFrame::ModuleLoadFrame(nt::AllocatedUnicodeString&& aSectionName, + const void* aMapBaseAddr, NTSTATUS aNtStatus, + ModuleLoadInfo::Status aLoadStatus, + bool aIsDependent) + : mPrev(sTopFrame.get()), + mContext(nullptr), + mLSPSubstitutionRequired(false), + mLoadNtStatus(aNtStatus), + mLoadInfo(std::move(aSectionName), aMapBaseAddr, aLoadStatus, + aIsDependent) { + sTopFrame.set(this); + + gLoaderPrivateAPI.NotifyBeginDllLoad(&mContext, mLoadInfo.mSectionName); +} + +ModuleLoadFrame::~ModuleLoadFrame() { + gLoaderPrivateAPI.NotifyEndDllLoad(mContext, mLoadNtStatus, + std::move(mLoadInfo)); + sTopFrame.set(mPrev); +} + +/* static */ +void ModuleLoadFrame::NotifyLSPSubstitutionRequired( + PCUNICODE_STRING aLeafName) { + ModuleLoadFrame* topFrame = sTopFrame.get(); + if (!topFrame) { + return; + } + + topFrame->SetLSPSubstitutionRequired(aLeafName); +} + +void ModuleLoadFrame::SetLSPSubstitutionRequired(PCUNICODE_STRING aLeafName) { + MOZ_ASSERT(!mLoadInfo.mBaseAddr); + if (mLoadInfo.mBaseAddr) { + // If mBaseAddr is not null then |this| has already seen a module load. This + // should not be the case for a LSP substitution, so we bail. + return; + } + + // Save aLeafName, as it will be used by SetLoadStatus when invoking + // SubstituteForLSP + mLoadInfo.mRequestedDllName = aLeafName; + mLSPSubstitutionRequired = true; +} + +/* static */ +void ModuleLoadFrame::NotifySectionMap( + nt::AllocatedUnicodeString&& aSectionName, const void* aMapBaseAddr, + NTSTATUS aMapNtStatus, ModuleLoadInfo::Status aLoadStatus, + bool aIsDependent) { + ModuleLoadFrame* topFrame = sTopFrame.get(); + if (!topFrame) { + // The only time that this data is useful is during initial mapping of + // the executable's dependent DLLs. If mozglue is present then + // IsDefaultObserver will return false, indicating that we are beyond + // initial process startup. + if (gLoaderPrivateAPI.IsDefaultObserver()) { + OnBareSectionMap(std::move(aSectionName), aMapBaseAddr, aMapNtStatus, + aLoadStatus, aIsDependent); + } + return; + } + + topFrame->OnSectionMap(std::move(aSectionName), aMapBaseAddr, aMapNtStatus, + aLoadStatus, aIsDependent); +} + +/* static */ +bool ModuleLoadFrame::ExistsTopFrame() { return !!sTopFrame.get(); } + +void ModuleLoadFrame::OnSectionMap(nt::AllocatedUnicodeString&& aSectionName, + const void* aMapBaseAddr, + NTSTATUS aMapNtStatus, + ModuleLoadInfo::Status aLoadStatus, + bool aIsDependent) { + if (mLoadInfo.mBaseAddr) { + // If mBaseAddr is not null then |this| has already seen a module load. This + // means that we are witnessing a bare section map. + OnBareSectionMap(std::move(aSectionName), aMapBaseAddr, aMapNtStatus, + aLoadStatus, aIsDependent); + return; + } + + mLoadInfo.mSectionName = std::move(aSectionName); + mLoadInfo.mBaseAddr = aMapBaseAddr; + mLoadInfo.mStatus = aLoadStatus; +} + +/* static */ +void ModuleLoadFrame::OnBareSectionMap( + nt::AllocatedUnicodeString&& aSectionName, const void* aMapBaseAddr, + NTSTATUS aMapNtStatus, ModuleLoadInfo::Status aLoadStatus, + bool aIsDependent) { + // We call the special constructor variant that is used for bare mappings. + ModuleLoadFrame frame(std::move(aSectionName), aMapBaseAddr, aMapNtStatus, + aLoadStatus, aIsDependent); +} + +NTSTATUS ModuleLoadFrame::SetLoadStatus(NTSTATUS aNtStatus, + PHANDLE aOutHandle) { + mLoadNtStatus = aNtStatus; + + if (!mLSPSubstitutionRequired) { + return aNtStatus; + } + + if (!gLoaderPrivateAPI.SubstituteForLSP(mLoadInfo.mRequestedDllName, + aOutHandle)) { + return aNtStatus; + } + + return STATUS_SUCCESS; +} + +SafeThreadLocal ModuleLoadFrame::sTopFrame; + +} // namespace freestanding +} // namespace mozilla diff --git a/browser/app/winlauncher/freestanding/ModuleLoadFrame.h b/browser/app/winlauncher/freestanding/ModuleLoadFrame.h new file mode 100644 index 0000000000..51a179db99 --- /dev/null +++ b/browser/app/winlauncher/freestanding/ModuleLoadFrame.h @@ -0,0 +1,97 @@ +/* -*- 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_freestanding_ModuleLoadFrame_h +#define mozilla_freestanding_ModuleLoadFrame_h + +#include "mozilla/LoaderAPIInterfaces.h" +#include "mozilla/NativeNt.h" +#include "mozilla/ThreadLocal.h" + +#include "SafeThreadLocal.h" + +namespace mozilla { +namespace freestanding { + +/** + * This class holds information about a DLL load at a particular frame in the + * current thread's stack. Each instance adds itself to a thread-local linked + * list of ModuleLoadFrames, enabling us to query information about the + * previous module load on the stack. + */ +class MOZ_RAII ModuleLoadFrame final { + public: + /** + * This constructor is for use by the LdrLoadDll hook. + */ + explicit ModuleLoadFrame(PCUNICODE_STRING aRequestedDllName); + ~ModuleLoadFrame(); + + static void NotifyLSPSubstitutionRequired(PCUNICODE_STRING aLeafName); + + /** + * This static method is called by the NtMapViewOfSection hook. + */ + static void NotifySectionMap(nt::AllocatedUnicodeString&& aSectionName, + const void* aMapBaseAddr, NTSTATUS aMapNtStatus, + ModuleLoadInfo::Status aLoadStatus, + bool aIsDependent); + static bool ExistsTopFrame(); + + /** + * Called by the LdrLoadDll hook to indicate the status of the load and for + * us to provide a substitute output handle if necessary. + */ + NTSTATUS SetLoadStatus(NTSTATUS aNtStatus, PHANDLE aOutHandle); + + ModuleLoadFrame(const ModuleLoadFrame&) = delete; + ModuleLoadFrame(ModuleLoadFrame&&) = delete; + ModuleLoadFrame& operator=(const ModuleLoadFrame&) = delete; + ModuleLoadFrame& operator=(ModuleLoadFrame&&) = delete; + + private: + /** + * Called by OnBareSectionMap to construct a frame for a bare load. + */ + ModuleLoadFrame(nt::AllocatedUnicodeString&& aSectionName, + const void* aMapBaseAddr, NTSTATUS aNtStatus, + ModuleLoadInfo::Status aLoadStatus, bool aIsDependent); + + void SetLSPSubstitutionRequired(PCUNICODE_STRING aLeafName); + void OnSectionMap(nt::AllocatedUnicodeString&& aSectionName, + const void* aMapBaseAddr, NTSTATUS aMapNtStatus, + ModuleLoadInfo::Status aLoadStatus, bool aIsDependent); + + /** + * A "bare" section mapping is one that was mapped without the code passing + * through a call to ntdll!LdrLoadDll. This method is invoked when we detect + * that condition. + */ + static void OnBareSectionMap(nt::AllocatedUnicodeString&& aSectionName, + const void* aMapBaseAddr, NTSTATUS aMapNtStatus, + ModuleLoadInfo::Status aLoadStatus, + bool aIsDependent); + + private: + // Link to the previous frame + ModuleLoadFrame* mPrev; + // Pointer to context managed by the nt::LoaderObserver implementation + void* mContext; + // Set to |true| when we need to block a WinSock LSP + bool mLSPSubstitutionRequired; + // NTSTATUS code from the |LdrLoadDll| call + NTSTATUS mLoadNtStatus; + // Telemetry information that will be forwarded to the nt::LoaderObserver + ModuleLoadInfo mLoadInfo; + + // Head of the linked list + static SafeThreadLocal sTopFrame; +}; + +} // namespace freestanding +} // namespace mozilla + +#endif // mozilla_freestanding_ModuleLoadFrame_h diff --git a/browser/app/winlauncher/freestanding/SafeThreadLocal.h b/browser/app/winlauncher/freestanding/SafeThreadLocal.h new file mode 100644 index 0000000000..e4b869f649 --- /dev/null +++ b/browser/app/winlauncher/freestanding/SafeThreadLocal.h @@ -0,0 +1,96 @@ +/* -*- 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_freestanding_SafeThreadLocal_h +#define mozilla_freestanding_SafeThreadLocal_h + +#include + +#include "mozilla/NativeNt.h" +#include "mozilla/ThreadLocal.h" + +namespace mozilla { +namespace freestanding { + +// We cannot fall back to the Tls* APIs because kernel32 might not have been +// loaded yet. +#if defined(__MINGW32__) && !defined(HAVE_THREAD_TLS_KEYWORD) +# error "This code requires the compiler to have native TLS support" +#endif // defined(__MINGW32__) && !defined(HAVE_THREAD_TLS_KEYWORD) + +/** + * This class holds data as a thread-local variable, or as a global variable + * if the thread local storage is not initialized yet. It should be safe + * because in that early stage we assume there is no more than a single thread. + */ +template +class SafeThreadLocal final { + static MOZ_THREAD_LOCAL(T) sThreadLocal; + static T sGlobal; + static bool sIsTlsUsed; + + // In normal cases, TLS is always available and the class uses sThreadLocal + // without changing sMainThreadId. So sMainThreadId is likely to be 0. + // + // If TLS is not available, we use sGlobal instead and update sMainThreadId + // so that that thread keeps using sGlobal even after TLS is initialized + // later. + static DWORD sMainThreadId; + + // Need non-inline accessors to prevent the compiler from generating code + // accessing sThreadLocal before checking a condition. + MOZ_NEVER_INLINE static void SetGlobalValue(T aValue) { sGlobal = aValue; } + MOZ_NEVER_INLINE static T GetGlobalValue() { return sGlobal; } + + public: + static void set(T aValue) { + static_assert(std::is_pointer_v, + "SafeThreadLocal must be used with a pointer"); + + if (sMainThreadId == mozilla::nt::RtlGetCurrentThreadId()) { + SetGlobalValue(aValue); + } else if (sIsTlsUsed) { + MOZ_ASSERT(mozilla::nt::RtlGetThreadLocalStoragePointer(), + "Once TLS is used, TLS should be available till the end."); + sThreadLocal.set(aValue); + } else if (mozilla::nt::RtlGetThreadLocalStoragePointer()) { + sIsTlsUsed = true; + sThreadLocal.set(aValue); + } else { + MOZ_ASSERT(sMainThreadId == 0, + "A second thread cannot be created before TLS is available."); + sMainThreadId = mozilla::nt::RtlGetCurrentThreadId(); + SetGlobalValue(aValue); + } + } + + static T get() { + if (sMainThreadId == mozilla::nt::RtlGetCurrentThreadId()) { + return GetGlobalValue(); + } else if (sIsTlsUsed) { + return sThreadLocal.get(); + } + return GetGlobalValue(); + } +}; + +template +MOZ_THREAD_LOCAL(T) +SafeThreadLocal::sThreadLocal; + +template +T SafeThreadLocal::sGlobal = nullptr; + +template +bool SafeThreadLocal::sIsTlsUsed = false; + +template +DWORD SafeThreadLocal::sMainThreadId = 0; + +} // namespace freestanding +} // namespace mozilla + +#endif // mozilla_freestanding_SafeThreadLocal_h diff --git a/browser/app/winlauncher/freestanding/SharedSection.cpp b/browser/app/winlauncher/freestanding/SharedSection.cpp new file mode 100644 index 0000000000..d6e5415e81 --- /dev/null +++ b/browser/app/winlauncher/freestanding/SharedSection.cpp @@ -0,0 +1,388 @@ +/* -*- 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 "SharedSection.h" + +#include +#include "CheckForCaller.h" +#include "mozilla/BinarySearch.h" + +namespace { + +bool AddString(mozilla::Span aBuffer, const UNICODE_STRING& aStr) { + size_t offsetElements = 0; + while (offsetElements < aBuffer.Length()) { + UNICODE_STRING uniStr; + ::RtlInitUnicodeString(&uniStr, aBuffer.data() + offsetElements); + + if (uniStr.Length == 0) { + // Reached to the array's last item. + break; + } + + if (::RtlCompareUnicodeString(&uniStr, &aStr, TRUE) == 0) { + // Already included in the array. + return true; + } + + // Go to the next string. + offsetElements += uniStr.MaximumLength / sizeof(wchar_t); + } + + // Ensure enough space including the last empty string at the end. + if (offsetElements * sizeof(wchar_t) + aStr.Length + sizeof(wchar_t) + + sizeof(wchar_t) > + aBuffer.LengthBytes()) { + return false; + } + + auto newStr = aBuffer.Subspan(offsetElements); + memcpy(newStr.data(), aStr.Buffer, aStr.Length); + memset(newStr.data() + aStr.Length / sizeof(wchar_t), 0, sizeof(wchar_t)); + return true; +} + +} // anonymous namespace + +namespace mozilla { +namespace freestanding { + +SharedSection gSharedSection; + +// Why don't we use ::GetProcAddress? +// If the export table of kernel32.dll is tampered in the current process, +// we cannot transfer an RVA because the function pointed by the RVA may not +// exist in a target process. +// We can use ::GetProcAddress with additional check to detect tampering, but +// FindExportAddressTableEntry fits perfectly here because it returns nullptr +// if the target entry is outside the image, which means it's tampered or +// forwarded to another DLL. +#define INIT_FUNCTION(exports, name) \ + do { \ + auto rvaToFunction = exports.FindExportAddressTableEntry(#name); \ + if (!rvaToFunction) { \ + return; \ + } \ + m##name = reinterpret_cast(*rvaToFunction); \ + } while (0) + +#define RESOLVE_FUNCTION(base, name) \ + m##name = reinterpret_cast( \ + base + reinterpret_cast(m##name)) + +void Kernel32ExportsSolver::Init() { + interceptor::MMPolicyInProcess policy; + auto k32Exports = nt::PEExportSection::Get( + ::GetModuleHandleW(L"kernel32.dll"), policy); + if (!k32Exports) { + return; + } + + // Please make sure these functions are not forwarded to another DLL. + INIT_FUNCTION(k32Exports, FlushInstructionCache); + INIT_FUNCTION(k32Exports, GetModuleHandleW); + INIT_FUNCTION(k32Exports, GetSystemInfo); + INIT_FUNCTION(k32Exports, VirtualProtect); +} + +bool Kernel32ExportsSolver::Resolve() { + const UNICODE_STRING k32Name = MOZ_LITERAL_UNICODE_STRING(L"kernel32.dll"); + + // We cannot use GetModuleHandleW because this code can be called + // before IAT is resolved. + auto k32Module = nt::GetModuleHandleFromLeafName(k32Name); + if (k32Module.isErr()) { + // Probably this is called before kernel32.dll is loaded. + return false; + } + + uintptr_t k32Base = + nt::PEHeaders::HModuleToBaseAddr(k32Module.unwrap()); + + RESOLVE_FUNCTION(k32Base, FlushInstructionCache); + RESOLVE_FUNCTION(k32Base, GetModuleHandleW); + RESOLVE_FUNCTION(k32Base, GetSystemInfo); + RESOLVE_FUNCTION(k32Base, VirtualProtect); + + return true; +} + +HANDLE SharedSection::sSectionHandle = nullptr; +SharedSection::Layout* SharedSection::sWriteCopyView = nullptr; +RTL_RUN_ONCE SharedSection::sEnsureOnce = RTL_RUN_ONCE_INIT; +nt::SRWLock SharedSection::sLock; + +void SharedSection::Reset(HANDLE aNewSectionObject) { + nt::AutoExclusiveLock{sLock}; + if (sWriteCopyView) { + nt::AutoMappedView view(sWriteCopyView); + sWriteCopyView = nullptr; + ::RtlRunOnceInitialize(&sEnsureOnce); + } + + if (sSectionHandle != aNewSectionObject) { + if (sSectionHandle) { + ::CloseHandle(sSectionHandle); + } + sSectionHandle = aNewSectionObject; + } +} + +void SharedSection::ConvertToReadOnly() { + if (!sSectionHandle) { + return; + } + + HANDLE readonlyHandle; + if (!::DuplicateHandle(nt::kCurrentProcess, sSectionHandle, + nt::kCurrentProcess, &readonlyHandle, GENERIC_READ, + FALSE, 0)) { + return; + } + + Reset(readonlyHandle); +} + +LauncherVoidResult SharedSection::Init() { + static_assert( + kSharedViewSize >= sizeof(Layout), + "kSharedViewSize is too small to represent SharedSection::Layout."); + + HANDLE section = + ::CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, + kSharedViewSize, nullptr); + if (!section) { + return LAUNCHER_ERROR_FROM_LAST(); + } + Reset(section); + + // The initial contents of the pages in a file mapping object backed by + // the operating system paging file are 0 (zero). No need to zero it out + // ourselves. + // https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-createfilemappingw + nt::AutoMappedView writableView(sSectionHandle, PAGE_READWRITE); + if (!writableView) { + return LAUNCHER_ERROR_FROM_LAST(); + } + + Layout* view = writableView.as(); + view->mK32Exports.Init(); + view->mState = Layout::State::kInitialized; + // Leave view->mDependentModulePathArrayStart to be zero to indicate + // we can add blocklist entries + return Ok(); +} + +LauncherVoidResult SharedSection::AddDependentModule(PCUNICODE_STRING aNtPath) { + nt::AutoMappedView writableView(sSectionHandle, PAGE_READWRITE); + if (!writableView) { + return LAUNCHER_ERROR_FROM_WIN32(::RtlGetLastWin32Error()); + } + + Layout* view = writableView.as(); + if (!view->mDependentModulePathArrayStart) { + // This is the first time AddDependentModule is called. We set the initial + // value to mDependentModulePathArrayStart, which *closes* the blocklist. + // After this, AddBlocklist is no longer allowed. + view->mDependentModulePathArrayStart = + FIELD_OFFSET(Layout, mFirstBlockEntry) + sizeof(DllBlockInfo); + } + + if (!AddString(view->GetDependentModules(), *aNtPath)) { + return LAUNCHER_ERROR_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER); + } + + return Ok(); +} + +LauncherVoidResult SharedSection::SetBlocklist( + const DynamicBlockList& aBlocklist, bool isDisabled) { + if (!aBlocklist.GetPayloadSize()) { + return Ok(); + } + + nt::AutoMappedView writableView(sSectionHandle, PAGE_READWRITE); + if (!writableView) { + return LAUNCHER_ERROR_FROM_WIN32(::RtlGetLastWin32Error()); + } + + Layout* view = writableView.as(); + if (view->mDependentModulePathArrayStart > 0) { + // If the dependent module array is already available, we must not update + // the blocklist. + return LAUNCHER_ERROR_FROM_WIN32(ERROR_INVALID_STATE); + } + + view->mBlocklistIsDisabled = isDisabled ? 1 : 0; + uintptr_t bufferEnd = reinterpret_cast(view) + kSharedViewSize; + size_t bytesCopied = aBlocklist.CopyTo( + view->mFirstBlockEntry, + bufferEnd - reinterpret_cast(view->mFirstBlockEntry)); + if (!bytesCopied) { + return LAUNCHER_ERROR_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER); + } + + // Setting mDependentModulePathArrayStart to a non-zero value means + // we no longer accept blocklist entries + // Just to be safe, make sure we don't overwrite mFirstBlockEntry even + // if there are no entries. + view->mDependentModulePathArrayStart = + FIELD_OFFSET(Layout, mFirstBlockEntry) + + std::max(bytesCopied, sizeof(DllBlockInfo)); + return Ok(); +} + +/* static */ +ULONG NTAPI SharedSection::EnsureWriteCopyViewOnce(PRTL_RUN_ONCE, PVOID, + PVOID*) { + if (!sWriteCopyView) { + nt::AutoMappedView view(sSectionHandle, PAGE_WRITECOPY); + if (!view) { + return TRUE; + } + sWriteCopyView = view.as(); + view.release(); + } + return sWriteCopyView->Resolve() ? TRUE : FALSE; +} + +SharedSection::Layout* SharedSection::EnsureWriteCopyView( + bool requireKernel32Exports /*= false */) { + ::RtlRunOnceExecuteOnce(&sEnsureOnce, &EnsureWriteCopyViewOnce, nullptr, + nullptr); + if (!sWriteCopyView) { + return nullptr; + } + auto requiredState = requireKernel32Exports + ? Layout::State::kResolved + : Layout::State::kLoadedDynamicBlocklistEntries; + return sWriteCopyView->mState >= requiredState ? sWriteCopyView : nullptr; +} + +bool SharedSection::Layout::Resolve() { + if (mState == State::kResolved) { + return true; + } + if (mState == State::kUninitialized) { + return false; + } + if (mState == State::kInitialized) { + if (!mNumBlockEntries) { + uintptr_t arrayBase = reinterpret_cast(mFirstBlockEntry); + uint32_t numEntries = 0; + for (DllBlockInfo* entry = mFirstBlockEntry; + entry->mName.Length && numEntries < GetMaxNumBlockEntries(); + ++entry) { + entry->mName.Buffer = reinterpret_cast( + arrayBase + reinterpret_cast(entry->mName.Buffer)); + ++numEntries; + } + mNumBlockEntries = numEntries; + // Sort by name so that we can binary-search + std::sort(mFirstBlockEntry, mFirstBlockEntry + mNumBlockEntries, + [](const DllBlockInfo& a, const DllBlockInfo& b) { + return ::RtlCompareUnicodeString(&a.mName, &b.mName, TRUE) < + 0; + }); + } + mState = State::kLoadedDynamicBlocklistEntries; + } + + if (!mK32Exports.Resolve()) { + return false; + } + + mState = State::kResolved; + return true; +} + +Span SharedSection::Layout::GetDependentModules() { + if (!mDependentModulePathArrayStart) { + return nullptr; + } + return Span( + reinterpret_cast(reinterpret_cast(this) + + mDependentModulePathArrayStart), + (kSharedViewSize - mDependentModulePathArrayStart) / sizeof(wchar_t)); +} + +bool SharedSection::Layout::IsDisabled() const { + return !!mBlocklistIsDisabled; +} + +const DllBlockInfo* SharedSection::Layout::SearchBlocklist( + const UNICODE_STRING& aLeafName) const { + MOZ_ASSERT(mState >= State::kLoadedDynamicBlocklistEntries); + DllBlockInfoComparator comp(aLeafName); + size_t match; + if (!BinarySearchIf(mFirstBlockEntry, 0, mNumBlockEntries, comp, &match)) { + return nullptr; + } + return &mFirstBlockEntry[match]; +} + +Kernel32ExportsSolver* SharedSection::GetKernel32Exports() { + Layout* writeCopyView = EnsureWriteCopyView(true); + return writeCopyView ? &writeCopyView->mK32Exports : nullptr; +} + +Maybe> SharedSection::GetDependentModules() { + Layout* writeCopyView = EnsureWriteCopyView(); + if (!writeCopyView) { + return Nothing(); + } + + mozilla::Span dependentModules = + writeCopyView->GetDependentModules(); + // Convert a null-delimited string set to a string vector. + Vector paths; + for (const wchar_t* p = dependentModules.data(); + (p - dependentModules.data() < + static_cast(dependentModules.size()) && + *p);) { + if (MOZ_UNLIKELY(!paths.append(p))) { + return Nothing(); + } + while (*p) { + ++p; + } + ++p; + } + return Some(std::move(paths)); +} + +Span SharedSection::GetDynamicBlocklist() { + Layout* writeCopyView = EnsureWriteCopyView(); + return writeCopyView ? writeCopyView->GetModulePathArray() : nullptr; +} + +const DllBlockInfo* SharedSection::SearchBlocklist( + const UNICODE_STRING& aLeafName) { + Layout* writeCopyView = EnsureWriteCopyView(); + return writeCopyView ? writeCopyView->SearchBlocklist(aLeafName) : nullptr; +} + +bool SharedSection::IsDisabled() { + Layout* writeCopyView = EnsureWriteCopyView(); + return writeCopyView ? writeCopyView->IsDisabled() : false; +} + +LauncherVoidResult SharedSection::TransferHandle( + nt::CrossExecTransferManager& aTransferMgr, DWORD aDesiredAccess, + HANDLE* aDestinationAddress) { + HANDLE remoteHandle; + if (!::DuplicateHandle(nt::kCurrentProcess, sSectionHandle, + aTransferMgr.RemoteProcess(), &remoteHandle, + aDesiredAccess, FALSE, 0)) { + return LAUNCHER_ERROR_FROM_LAST(); + } + + return aTransferMgr.Transfer(aDestinationAddress, &remoteHandle, + sizeof(remoteHandle)); +} + +} // namespace freestanding +} // namespace mozilla diff --git a/browser/app/winlauncher/freestanding/SharedSection.h b/browser/app/winlauncher/freestanding/SharedSection.h new file mode 100644 index 0000000000..54e89a2325 --- /dev/null +++ b/browser/app/winlauncher/freestanding/SharedSection.h @@ -0,0 +1,212 @@ +/* -*- 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_freestanding_SharedSection_h +#define mozilla_freestanding_SharedSection_h + +#include "mozilla/DynamicBlocklist.h" +#include "mozilla/glue/SharedSection.h" +#include "mozilla/NativeNt.h" +#include "mozilla/interceptor/MMPolicies.h" + +// clang-format off +#define MOZ_LITERAL_UNICODE_STRING(s) \ + { \ + /* Length of the string in bytes, less the null terminator */ \ + sizeof(s) - sizeof(wchar_t), \ + /* Length of the string in bytes, including the null terminator */ \ + sizeof(s), \ + /* Pointer to the buffer */ \ + const_cast(s) \ + } +// clang-format on + +namespace mozilla { +namespace freestanding { +class SharedSectionTestHelper; + +struct DllBlockInfoComparator { + explicit DllBlockInfoComparator(const UNICODE_STRING& aTarget) + : mTarget(&aTarget) {} + + int operator()(const DllBlockInfo& aVal) const { + return static_cast( + ::RtlCompareUnicodeString(mTarget, &aVal.mName, TRUE)); + } + + PCUNICODE_STRING mTarget; +}; + +// This class calculates RVAs of kernel32's functions and transfers them +// to a target process, where the transferred RVAs are resolved into +// function addresses so that the target process can use them after +// kernel32.dll is loaded and before IAT is resolved. +struct MOZ_TRIVIAL_CTOR_DTOR Kernel32ExportsSolver final + : interceptor::MMPolicyInProcessEarlyStage::Kernel32Exports { + void Init(); + bool Resolve(); +}; + +// This class manages a section which is created in the launcher process and +// mapped in the browser process and the sandboxed processes. The section's +// layout is represented as SharedSection::Layout. +// +// (1) Kernel32's functions required for MMPolicyInProcessEarlyStage +// Formatted as Kernel32ExportsSolver. +// +// (2) Various flags and offsets +// +// (3) Entries in the dynamic blocklist, in DllBlockInfo format. There +// are mNumBlockEntries of these, followed by one that has mName.Length +// of 0. Note that the strings that contain +// the names of the entries in the blocklist are stored concatenated +// after the last entry. The mName pointers in each DllBlockInfo point +// to these strings correctly in Resolve(), so clients don't need +// to do anything special to read these strings. +// +// (4) Array of NT paths of the executable's dependent modules +// Formatted as a null-delimited wide-character string set ending with +// an empty string. These entries start at offset +// mDependentModulePathArrayStart (in bytes) from the beginning +// of the structure +// +// +--------------------------------------------------------------+ +// | (1) | FlushInstructionCache | +// | | GetModuleHandleW | +// | | GetSystemInfo | +// | | VirtualProtect | +// | | State [kUninitialized|kInitialized|kResolved] | +// +--------------------------------------------------------------+ +// | (2) | (flags and offsets) | +// +--------------------------------------------------------------+ +// | (3) | | +// | | | +// | | ... | +// | | | +// | | | +// | | L"string1.dllstring2.dll...stringlast.dll" | +// +--------------------------------------------------------------+ +// | (4) | L"NT path 1" | +// | | L"NT path 2" | +// | | ... | +// | | L"" | +// +--------------------------------------------------------------+ +class MOZ_TRIVIAL_CTOR_DTOR SharedSection final : public nt::SharedSection { + struct Layout final { + enum class State { + kUninitialized, + kInitialized, + kLoadedDynamicBlocklistEntries, + kResolved, + } mState; + + Kernel32ExportsSolver mK32Exports; + // 1 if the blocklist is disabled, 0 otherwise. + // If the blocklist is disabled, the entries are still loaded to make it + // easy for the user to remove any they don't want, but none of the DLLs + // here are actually blocked. + // Stored as a uint32_t for alignment reasons. + uint32_t mBlocklistIsDisabled; + // The offset, in bytes, from the beginning of the Layout structure to the + // first dependent module entry. + // When the Layout object is created, this value is 0, indicating that no + // dependent modules have been added and it is safe to add DllBlockInfo + // entries. + // After this value is set to something non-0, no more DllBlockInfo entries + // can be added. + uint32_t mDependentModulePathArrayStart; + // The number of blocklist entries. + uint32_t mNumBlockEntries; + DllBlockInfo mFirstBlockEntry[1]; + + Span GetModulePathArray() { + return Span( + mFirstBlockEntry, + (kSharedViewSize - (reinterpret_cast(mFirstBlockEntry) - + reinterpret_cast(this))) / + sizeof(DllBlockInfo)); + } + // Can be used to make sure we don't step past the end of the shared memory + // section. + static constexpr uint32_t GetMaxNumBlockEntries() { + return (kSharedViewSize - (offsetof(Layout, mFirstBlockEntry))) / + sizeof(DllBlockInfo); + } + Layout() = delete; // disallow instantiation + bool Resolve(); + bool IsDisabled() const; + const DllBlockInfo* SearchBlocklist(const UNICODE_STRING& aLeafName) const; + Span GetDependentModules(); + }; + + // As we define a global variable of this class and use it in our blocklist + // which is excuted in a process's early stage. If we have a complex dtor, + // the static initializer tries to register that dtor with onexit() of + // ucrtbase.dll which is not loaded yet, resulting in crash. Thus, we have + // a raw handle and a pointer as a static variable and manually release them + // by calling Reset() where possible. + static HANDLE sSectionHandle; + static Layout* sWriteCopyView; + static RTL_RUN_ONCE sEnsureOnce; + + // The sLock lock guarantees that while it is held, sSectionHandle will not + // change nor get closed, sEnsureOnce will not get reinitialized, and + // sWriteCopyView will not change nor get unmapped once initialized. We take + // sLock on paths that could run concurrently with ConvertToReadOnly(). This + // method is only called on the main process, and very early, so the only + // real risk here should be threads started by third-party products reaching + // our patched_NtMapViewOfSection (see bug 1850969). + static nt::SRWLock sLock; + + static ULONG NTAPI EnsureWriteCopyViewOnce(PRTL_RUN_ONCE, PVOID, PVOID*); + static Layout* EnsureWriteCopyView(bool requireKernel32Exports = false); + + static constexpr size_t kSharedViewSize = 0x1000; + + // For test use only + friend class SharedSectionTestHelper; + + public: + // Replace |sSectionHandle| with a given handle. + static void Reset(HANDLE aNewSectionObject = sSectionHandle); + + static inline nt::AutoSharedLock AutoNoReset() { + return nt::AutoSharedLock{sLock}; + } + + // Replace |sSectionHandle| with a new readonly handle. + static void ConvertToReadOnly(); + + // Create a new writable section and initialize the Kernel32ExportsSolver + // part. + static LauncherVoidResult Init(); + + // Append a new string to the |sSectionHandle| + static LauncherVoidResult AddDependentModule(PCUNICODE_STRING aNtPath); + static LauncherVoidResult SetBlocklist(const DynamicBlockList& aBlocklist, + bool isDisabled); + + // Map |sSectionHandle| to a copy-on-write page and return a writable pointer + // to each structure, or null if Layout failed to resolve exports. + Kernel32ExportsSolver* GetKernel32Exports(); + Maybe> GetDependentModules() final override; + Span GetDynamicBlocklist() final override; + + static bool IsDisabled(); + static const DllBlockInfo* SearchBlocklist(const UNICODE_STRING& aLeafName); + + // Transfer |sSectionHandle| to a process associated with |aTransferMgr|. + static LauncherVoidResult TransferHandle( + nt::CrossExecTransferManager& aTransferMgr, DWORD aDesiredAccess, + HANDLE* aDestinationAddress = &sSectionHandle); +}; + +extern SharedSection gSharedSection; + +} // namespace freestanding +} // namespace mozilla + +#endif // mozilla_freestanding_SharedSection_h diff --git a/browser/app/winlauncher/freestanding/gen_ntdll_freestanding_lib.py b/browser/app/winlauncher/freestanding/gen_ntdll_freestanding_lib.py new file mode 100644 index 0000000000..d9fe86d78e --- /dev/null +++ b/browser/app/winlauncher/freestanding/gen_ntdll_freestanding_lib.py @@ -0,0 +1,28 @@ +# -*- 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 +import subprocess +import tempfile + + +def main(output_fd, def_file, llvm_dlltool, *llvm_dlltool_args): + # llvm-dlltool can't output to stdout, so we create a temp file, use that + # to write out the lib, and then copy it over to output_fd + (tmp_fd, tmp_output) = tempfile.mkstemp() + os.close(tmp_fd) + + try: + cmd = [llvm_dlltool] + cmd.extend(llvm_dlltool_args) + cmd += ["-d", def_file, "-l", tmp_output] + + subprocess.check_call(cmd) + + with open(tmp_output, "rb") as tmplib: + output_fd.write(tmplib.read()) + finally: + os.remove(tmp_output) diff --git a/browser/app/winlauncher/freestanding/moz.build b/browser/app/winlauncher/freestanding/moz.build new file mode 100644 index 0000000000..7a43d9a104 --- /dev/null +++ b/browser/app/winlauncher/freestanding/moz.build @@ -0,0 +1,56 @@ +# -*- 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("winlauncher-freestanding") + +FORCE_STATIC_LIB = True + +# Our patched NtMapViewOfSection can be called before the process's import +# table is populated. Don't let the compiler insert any instrumentation +# that might call an import. +NO_PGO = True + +UNIFIED_SOURCES += [ + "DllBlocklist.cpp", + "LoaderPrivateAPI.cpp", + "ModuleLoadFrame.cpp", + "SharedSection.cpp", +] + +# This library must be compiled in a freestanding environment, as its code must +# not assume that it has access to any runtime libraries. +if CONFIG["CC_TYPE"] == "clang-cl": + CXXFLAGS += ["-Xclang"] + +CXXFLAGS += [ + "-ffreestanding", +] + +# Forcibly include Freestanding.h into all source files in this library. +if CONFIG["CC_TYPE"] == "clang-cl": + CXXFLAGS += ["-FI"] +else: + CXXFLAGS += ["-include"] + +CXXFLAGS += [SRCDIR + "/Freestanding.h"] + +OS_LIBS += [ + "ntdll", + "ntdll_freestanding", +] + +if CONFIG["COMPILE_ENVIRONMENT"] and CONFIG["LLVM_DLLTOOL"]: + GeneratedFile( + "%sntdll_freestanding.%s" % (CONFIG["LIB_PREFIX"], CONFIG["LIB_SUFFIX"]), + script="gen_ntdll_freestanding_lib.py", + inputs=["ntdll_freestanding.def"], + flags=[CONFIG["LLVM_DLLTOOL"]] + CONFIG["LLVM_DLLTOOL_FLAGS"], + ) + +DisableStlWrapping() + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Launcher Process") diff --git a/browser/app/winlauncher/freestanding/ntdll_freestanding.def b/browser/app/winlauncher/freestanding/ntdll_freestanding.def new file mode 100644 index 0000000000..6e5e2685fe --- /dev/null +++ b/browser/app/winlauncher/freestanding/ntdll_freestanding.def @@ -0,0 +1,25 @@ +; 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 ntdll + +; When we compile with -freestanding, the compiler still requires implementation +; of the four functions listed below. +; +; We could implement our own naive versions of these functions, but that +; solution is less than ideal since the implementations must be extern and are +; thus picked up by the entire firefox.exe binary. This denies the rest of +; firefox.exe the benefit of optimized implementations. On Windows the +; sandbox is linked into firefox.exe, so we cannot just shrug and +; assume that a naive implementation will not have any effect on anything. +; +; There are, however, optimized implementations of these functions that are +; exported by ntdll.dll. OTOH, they are not included in the ntdll.lib +; import library. This .def file is used to build an import library that "fills +; in the blanks" and allows us to link into the ntdll implementations. +EXPORTS + memcmp + memcpy + memmove + memset diff --git a/browser/app/winlauncher/moz.build b/browser/app/winlauncher/moz.build new file mode 100644 index 0000000000..f2f321ae0c --- /dev/null +++ b/browser/app/winlauncher/moz.build @@ -0,0 +1,61 @@ +# -*- 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("winlauncher") + +FORCE_STATIC_LIB = True + +UNIFIED_SOURCES += [ + "/ipc/mscom/COMWrappers.cpp", + "/ipc/mscom/ProcessRuntime.cpp", + "/toolkit/xre/WinTokenUtils.cpp", + "/widget/windows/WindowsConsole.cpp", + "DllBlocklistInit.cpp", + "ErrorHandler.cpp", + "LauncherProcessWin.cpp", + "LaunchUnelevated.cpp", + "NtLoaderAPI.cpp", +] + +OS_LIBS += [ + "oleaut32", + "ole32", + "rpcrt4", + "version", +] + +DIRS += [ + "freestanding", +] + +USE_LIBS += [ + "winlauncher-freestanding", +] + +TEST_DIRS += [ + "test", +] + +if CONFIG["MOZ_LAUNCHER_PROCESS"]: + LOCAL_INCLUDES += [ + "/other-licenses/nsis/Contrib/CityHash/cityhash", + "/toolkit/mozapps/update/common", + ] + UNIFIED_SOURCES += [ + "/other-licenses/nsis/Contrib/CityHash/cityhash/city.cpp", + "/toolkit/mozapps/update/common/commonupdatedir.cpp", + "/toolkit/xre/LauncherRegistryInfo.cpp", + ] + +for var in ("MOZ_APP_BASENAME", "MOZ_APP_VENDOR", "MOZ_APP_DISPLAYNAME"): + DEFINES[var] = '"%s"' % CONFIG[var] + +DEFINES["MOZ_USE_LAUNCHER_ERROR"] = 1 + +DisableStlWrapping() + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Launcher Process") diff --git a/browser/app/winlauncher/test/TestCrossProcessWin.cpp b/browser/app/winlauncher/test/TestCrossProcessWin.cpp new file mode 100644 index 0000000000..e002edae50 --- /dev/null +++ b/browser/app/winlauncher/test/TestCrossProcessWin.cpp @@ -0,0 +1,715 @@ +/* -*- 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 +#include + +#define MOZ_USE_LAUNCHER_ERROR + +#include +#include +#include "freestanding/SharedSection.cpp" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/DynamicBlocklist.h" +#include "mozilla/NativeNt.h" +#include "mozilla/Vector.h" + +#define DLL_BLOCKLIST_ENTRY(name, ...) \ + {MOZ_LITERAL_UNICODE_STRING(L##name), __VA_ARGS__}, +#define DLL_BLOCKLIST_STRING_TYPE UNICODE_STRING + +#include "mozilla/WindowsDllBlocklistLauncherDefs.h" + +const wchar_t kChildArg[] = L"--child"; +const char* kTestDependentModulePaths[] = { + "\\Device\\HarddiskVolume4\\Windows\\system32\\A B C", + "\\Device\\HarddiskVolume4\\Windows\\system32\\a b c.dll", + "\\Device\\HarddiskVolume4\\Windows\\system32\\A B C.exe", + "\\Device\\HarddiskVolume4\\Windows\\system32\\X Y Z.dll", + "\\Device\\HarddiskVolume1\\a b C", + "\\Device\\HarddiskVolume2\\A b c.DLL", + "\\Device\\HarddiskVolume3\\A B c.exe", + "\\Device\\HarddiskVolume4\\X y Z.dll", +}; +const wchar_t* kExpectedDependentModules[] = { + L"A B C", + L"a b c.dll", + L"A B C.exe", + L"X Y Z.dll", +}; + +const UNICODE_STRING kStringNotInBlocklist = + MOZ_LITERAL_UNICODE_STRING(L"Test_NotInBlocklist.dll"); +const UNICODE_STRING kTestDependentModuleString = + MOZ_LITERAL_UNICODE_STRING(L"Test_DependentModule.dll"); + +using namespace mozilla; +using namespace mozilla::freestanding; + +// clang-format off +const DllBlockInfo kDllBlocklistShort[] = { + // The entries do not have to be sorted. + DLL_BLOCKLIST_ENTRY("X Y Z_Test", MAKE_VERSION(1, 2, 65535, 65535)) + DLL_BLOCKLIST_ENTRY("\u30E9\u30FC\u30E1\u30F3_Test") + DLL_BLOCKLIST_ENTRY("Avmvirtualsource_Test.ax", MAKE_VERSION(1, 0, 0, 3), + DllBlockInfoFlags::BROWSER_PROCESS_ONLY) + DLL_BLOCKLIST_ENTRY("1ccelerator_Test.dll", MAKE_VERSION(3, 2, 1, 6)) + DLL_BLOCKLIST_ENTRY("atkdx11disp_Test.dll", DllBlockInfo::ALL_VERSIONS) + {}, +}; +// clang-format on + +namespace mozilla::freestanding { +class SharedSectionTestHelper { + public: + static constexpr size_t GetModulePathArraySize() { + return SharedSection::kSharedViewSize - + (offsetof(SharedSection::Layout, mFirstBlockEntry) + + sizeof(DllBlockInfo)); + } +}; +} // namespace mozilla::freestanding + +class TempFile final { + wchar_t mFullPath[MAX_PATH + 1]; + + public: + TempFile() : mFullPath{0} { + wchar_t tempDir[MAX_PATH + 1]; + DWORD len = ::GetTempPathW(ArrayLength(tempDir), tempDir); + if (!len) { + return; + } + + len = ::GetTempFileNameW(tempDir, L"blocklist", 0, mFullPath); + if (!len) { + return; + } + } + + operator const wchar_t*() const { return mFullPath[0] ? mFullPath : nullptr; } +}; + +template +void PrintLauncherError(const LauncherResult& aResult, + const char (&aMsg)[N]) { + const LauncherError& err = aResult.inspectErr(); + printf("TEST-FAILED | TestCrossProcessWin | %s - %lx at %s:%d\n", aMsg, + err.mError.AsHResult(), err.mFile, err.mLine); +} + +#define VERIFY_FUNCTION_RESOLVED(mod, exports, name) \ + do { \ + if (reinterpret_cast(exports->m##name) != \ + ::GetProcAddress(mod, #name)) { \ + printf( \ + "TEST-FAILED | TestCrossProcessWin | " \ + "Kernel32ExportsSolver::" #name " did not match.\n"); \ + return false; \ + } \ + } while (0) + +static bool VerifySharedSection(SharedSection& aSharedSection) { + Kernel32ExportsSolver* k32Exports = aSharedSection.GetKernel32Exports(); + if (!k32Exports) { + printf( + "TEST-FAILED | TestCrossProcessWin | Failed to map a shared section\n"); + return false; + } + + HMODULE k32mod = ::GetModuleHandleW(L"kernel32.dll"); + VERIFY_FUNCTION_RESOLVED(k32mod, k32Exports, FlushInstructionCache); + VERIFY_FUNCTION_RESOLVED(k32mod, k32Exports, GetModuleHandleW); + VERIFY_FUNCTION_RESOLVED(k32mod, k32Exports, GetSystemInfo); + VERIFY_FUNCTION_RESOLVED(k32mod, k32Exports, VirtualProtect); + + Maybe> modulesArray = + aSharedSection.GetDependentModules(); + if (modulesArray.isNothing()) { + printf( + "TEST-FAILED | TestCrossProcessWin | GetDependentModules returned " + "Nothing"); + return false; + } + bool matched = + modulesArray->length() == + sizeof(kExpectedDependentModules) / sizeof(kExpectedDependentModules[0]); + if (matched) { + for (size_t i = 0; i < modulesArray->length(); ++i) { + if (wcscmp((*modulesArray)[i], kExpectedDependentModules[i])) { + matched = false; + break; + } + } + } + if (!matched) { + // Print actual strings on error + for (const wchar_t* p : *modulesArray) { + printf("%p: %S\n", p, p); + } + return false; + } + + for (const DllBlockInfo* info = kDllBlocklistShort; info->mName.Buffer; + ++info) { + const DllBlockInfo* matched = aSharedSection.SearchBlocklist(info->mName); + if (!matched) { + printf( + "TEST-FAILED | TestCrossProcessWin | No blocklist entry match for " + "entry in blocklist.\n"); + return false; + } + } + + if (aSharedSection.SearchBlocklist(kStringNotInBlocklist)) { + printf( + "TEST-FAILED | TestCrossProcessWin | Found blocklist entry match for " + "something not in the blocklist.\n"); + } + + if (aSharedSection.IsDisabled()) { + printf("TEST-FAILED | TestCrossProcessWin | Wrong disabled value.\n"); + } + + return true; +} + +static bool TestAddString() { + wchar_t testBuffer[3] = {0}; + UNICODE_STRING ustr; + + // This makes |testBuffer| full. + ::RtlInitUnicodeString(&ustr, L"a"); + if (!AddString(testBuffer, ustr)) { + printf( + "TEST-FAILED | TestCrossProcessWin | " + "AddString failed.\n"); + return false; + } + + // Adding a string to a full buffer should fail. + ::RtlInitUnicodeString(&ustr, L"b"); + if (AddString(testBuffer, ustr)) { + printf( + "TEST-FAILED | TestCrossProcessWin | " + "AddString caused OOB memory access.\n"); + return false; + } + + bool matched = memcmp(testBuffer, L"a\0", sizeof(testBuffer)) == 0; + if (!matched) { + printf( + "TEST-FAILED | TestCrossProcessWin | " + "AddString wrote wrong values.\n"); + return false; + } + + return true; +} + +// Convert |aBlockEntries|, which is an array ending with an empty instance +// of DllBlockInfo, to DynamicBlockList by storing it to a temp file, loading +// as DynamicBlockList, and deleting the temp file. +static DynamicBlockList ConvertStaticBlocklistToDynamic( + const DllBlockInfo aBlockEntries[]) { + size_t originalLength = 0; + CheckedUint32 totalStringLen = 0; + for (const DllBlockInfo* entry = aBlockEntries; entry->mName.Length; + ++entry) { + totalStringLen += entry->mName.Length; + MOZ_RELEASE_ASSERT(totalStringLen.isValid()); + ++originalLength; + } + + // Pack all strings in this buffer without null characters + UniquePtr stringBuffer = + MakeUnique(totalStringLen.value()); + + // The string buffer is placed immediately after the array of DllBlockInfo + const size_t stringBufferOffset = (originalLength + 1) * sizeof(DllBlockInfo); + + // Entries in the dynamic blocklist do have to be sorted, + // unlike in the static blocklist. + UniquePtr sortedBlockEntries = + MakeUnique(originalLength); + memcpy(sortedBlockEntries.get(), aBlockEntries, + sizeof(DllBlockInfo) * originalLength); + std::sort(sortedBlockEntries.get(), sortedBlockEntries.get() + originalLength, + [](const DllBlockInfo& a, const DllBlockInfo& b) { + return ::RtlCompareUnicodeString(&a.mName, &b.mName, TRUE) < 0; + }); + + Vector copied; + Unused << copied.resize(originalLength + 1); // aBlockEntries + sentinel + + size_t currentStringOffset = 0; + for (size_t i = 0; i < originalLength; ++i) { + copied[i].mMaxVersion = sortedBlockEntries[i].mMaxVersion; + copied[i].mFlags = sortedBlockEntries[i].mFlags; + + // Copy the module's name to the string buffer and store its offset + // in mName.Buffer + memcpy(stringBuffer.get() + currentStringOffset, + sortedBlockEntries[i].mName.Buffer, + sortedBlockEntries[i].mName.Length); + copied[i].mName.Buffer = + reinterpret_cast(stringBufferOffset + currentStringOffset); + // Only keep mName.Length and leave mName.MaximumLength to be zero + copied[i].mName.Length = sortedBlockEntries[i].mName.Length; + + currentStringOffset += sortedBlockEntries[i].mName.Length; + } + + TempFile blocklistFile; + nsAutoHandle file(::CreateFileW(blocklistFile, GENERIC_WRITE, FILE_SHARE_READ, + nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, + nullptr)); + MOZ_RELEASE_ASSERT(file); + + DynamicBlockListBase::FileHeader header; + header.mSignature = DynamicBlockListBase::kSignature; + header.mFileVersion = DynamicBlockListBase::kCurrentVersion; + header.mPayloadSize = + sizeof(DllBlockInfo) * copied.length() + totalStringLen.value(); + + DWORD written = 0; + MOZ_RELEASE_ASSERT( + ::WriteFile(file.get(), &header, sizeof(header), &written, nullptr)); + MOZ_RELEASE_ASSERT(::WriteFile(file.get(), copied.begin(), + sizeof(DllBlockInfo) * copied.length(), + &written, nullptr)); + MOZ_RELEASE_ASSERT(::WriteFile(file.get(), stringBuffer.get(), + totalStringLen.value(), &written, nullptr)); + + DynamicBlockList blockList(blocklistFile); + ::DeleteFileW(blocklistFile); + return blockList; +} + +const DynamicBlockList gFullList = + ConvertStaticBlocklistToDynamic(gWindowsDllBlocklist); +const DynamicBlockList gShortList = + ConvertStaticBlocklistToDynamic(kDllBlocklistShort); + +static bool TestDependentModules() { + LauncherVoidResult result = gSharedSection.Init(); + if (result.isErr()) { + PrintLauncherError(result, "SharedSection::Init failed"); + return false; + } + + constexpr size_t sizeInBytes = + SharedSectionTestHelper::GetModulePathArraySize(); + UniquePtr bufferData = MakeUnique(sizeInBytes); + Span buffer(bufferData, sizeInBytes); + memset(buffer.data(), 0x88, buffer.size()); + + // Try to add a long string that does not fit in the section, + // since there's no room for the NULL character to indicate the final string. + UNICODE_STRING ustr; + ustr.Buffer = reinterpret_cast(buffer.data()); + ustr.Length = ustr.MaximumLength = buffer.size(); + + result = gSharedSection.AddDependentModule(&ustr); + if (result.isOk() || result.inspectErr() != WindowsError::FromWin32Error( + ERROR_INSUFFICIENT_BUFFER)) { + printf( + "TEST-FAILED | TestCrossProcessWin | " + "Adding a too long string should fail.\n"); + return false; + } + + result = gSharedSection.Init(); + if (result.isErr()) { + PrintLauncherError(result, "SharedSection::Init failed"); + return false; + } + + // Keep adding a single-char string until it fails and + // make sure no crash. + // We want to make sure no strings match any earlier strings so + // we can get the expected count. This is a little tricky since + // it includes case-insensitivity, so start at the "CJK Unified Ideographs + // Extension A" block of Unicode, which has no two characters that compare + // equal under a case insensitive comparison. + *(reinterpret_cast(buffer.data())) = 0x3400; + ustr.Length = ustr.MaximumLength = sizeof(wchar_t); + wchar_t numberOfStringsAdded = 0; + while (gSharedSection.AddDependentModule(&ustr).isOk()) { + ++numberOfStringsAdded; + // Make sure the string doesn't match any earlier strings + wchar_t oldValue = *(reinterpret_cast(buffer.data())); + *(reinterpret_cast(buffer.data())) = oldValue + 1; + } + + int numberOfCharactersInBuffer = + SharedSectionTestHelper::GetModulePathArraySize() / sizeof(wchar_t); + // Each string is two characters long (one "real" character and a null), but + // the whole buffer needs an additional null at the end. + int expectedNumberOfStringsAdded = (numberOfCharactersInBuffer - 1) / 2; + if (numberOfStringsAdded != expectedNumberOfStringsAdded) { + printf( + "TEST-FAILED | TestCrossProcessWin | " + "Added %d dependent strings before failing (expected %d).\n", + static_cast(numberOfStringsAdded), expectedNumberOfStringsAdded); + return false; + } + + // SetBlocklist is not allowed after AddDependentModule + result = gSharedSection.SetBlocklist(gShortList, false); + if (result.isOk() || result.inspectErr() != + WindowsError::FromWin32Error(ERROR_INVALID_STATE)) { + printf( + "TEST-FAILED | TestCrossProcessWin | " + "SetBlocklist is not allowed after AddDependentModule\n"); + return false; + } + + gSharedSection.Reset(); + return true; +} + +static bool TestDynamicBlocklist() { + if (!gFullList.GetPayloadSize() || !gShortList.GetPayloadSize()) { + printf( + "TEST-FAILED | TestCrossProcessWin | DynamicBlockList::LoadFile " + "failed\n"); + return false; + } + + LauncherVoidResult result = gSharedSection.Init(); + if (result.isErr()) { + PrintLauncherError(result, "SharedSection::Init failed"); + return false; + } + + // Set gShortList, and gShortList + // 1. Setting gShortList succeeds + // 2. Next try to set gShortList fails + result = gSharedSection.SetBlocklist(gShortList, false); + if (result.isErr()) { + PrintLauncherError(result, "SetBlocklist(gShortList) failed"); + return false; + } + result = gSharedSection.SetBlocklist(gShortList, false); + if (result.isOk() || result.inspectErr() != + WindowsError::FromWin32Error(ERROR_INVALID_STATE)) { + printf( + "TEST-FAILED | TestCrossProcessWin | " + "SetBlocklist is allowed only once\n"); + return false; + } + + result = gSharedSection.Init(); + if (result.isErr()) { + PrintLauncherError(result, "SharedSection::Init failed"); + return false; + } + + // Add gFullList and gShortList + // 1. Adding gFullList always fails because it doesn't fit the section + // 2. Adding gShortList succeeds because no entry is added yet + MOZ_RELEASE_ASSERT( + gFullList.GetPayloadSize() > + SharedSectionTestHelper::GetModulePathArraySize(), + "Test assumes gFullList is too big to fit in shared section"); + result = gSharedSection.SetBlocklist(gFullList, false); + if (result.isOk() || result.inspectErr() != WindowsError::FromWin32Error( + ERROR_INSUFFICIENT_BUFFER)) { + printf( + "TEST-FAILED | TestCrossProcessWin | " + "SetBlocklist(gFullList) should fail\n"); + return false; + } + result = gSharedSection.SetBlocklist(gShortList, false); + if (result.isErr()) { + PrintLauncherError(result, "SetBlocklist(gShortList) failed"); + return false; + } + + // AddDependentModule is allowed after SetBlocklist + result = gSharedSection.AddDependentModule(&kTestDependentModuleString); + if (result.isErr()) { + PrintLauncherError(result, "SharedSection::AddDependentModule failed"); + return false; + } + + gSharedSection.Reset(); + return true; +} + +class ChildProcess final { + nsAutoHandle mChildProcess; + nsAutoHandle mChildMainThread; + DWORD mProcessId; + + public: + // The following variables are updated from the parent process via + // WriteProcessMemory while the process is suspended as a part of + // TestWithChildProcess(). + // + // Having both a non-const and a const is important because a constant + // is separately placed in the .rdata section which is read-only, so + // the region's attribute needs to be changed before modifying data via + // WriteProcessMemory. + // The keyword "volatile" is needed for a constant, otherwise the compiler + // evaluates a constant as a literal without fetching data from memory. + static HMODULE sExecutableImageBase; + static volatile const DWORD sReadOnlyProcessId; + + static int Main() { + SRWLOCK lock = SRWLOCK_INIT; + ::AcquireSRWLockExclusive(&lock); + + Vector threads; + std::atomic success = true; + for (int i = 0; i < 10; ++i) { + Unused << threads.emplaceBack( + [&success](SRWLOCK* aLock) { + // All threads call GetKernel32Exports(), but only the first thread + // maps a write-copy section and populates it. + ::AcquireSRWLockShared(aLock); + if (gSharedSection.GetKernel32Exports() == nullptr) { + success = false; + } + ::ReleaseSRWLockShared(aLock); + }, + &lock); + } + + // Wait a msec for all threads to be ready and release the lock + ::Sleep(1); + ::ReleaseSRWLockExclusive(&lock); + + for (auto& thread : threads) { + thread.join(); + } + + if (!success) { + printf( + "TEST-FAILED | TestCrossProcessWin | " + "GetKernel32Exports() returned null.\n"); + return 1; + } + + if (sExecutableImageBase != ::GetModuleHandle(nullptr)) { + printf( + "TEST-FAILED | TestCrossProcessWin | " + "sExecutableImageBase is expected to be %p, but actually was %p.\n", + ::GetModuleHandle(nullptr), sExecutableImageBase); + return 1; + } + + if (sReadOnlyProcessId != ::GetCurrentProcessId()) { + printf( + "TEST-FAILED | TestCrossProcessWin | " + "sReadOnlyProcessId is expected to be %lx, but actually was %lx.\n", + ::GetCurrentProcessId(), sReadOnlyProcessId); + return 1; + } + + if (!VerifySharedSection(gSharedSection)) { + return 1; + } + + // Test a scenario to transfer a transferred section as a readonly handle + gSharedSection.ConvertToReadOnly(); + + // AddDependentModule fails as the handle is readonly. + LauncherVoidResult result = + gSharedSection.AddDependentModule(&kTestDependentModuleString); + if (result.inspectErr() != + WindowsError::FromWin32Error(ERROR_ACCESS_DENIED)) { + PrintLauncherError(result, "The readonly section was writable"); + return 1; + } + + if (!VerifySharedSection(gSharedSection)) { + return 1; + } + + return 0; + } + + ChildProcess(const wchar_t* aExecutable, const wchar_t* aOption) + : mProcessId(0) { + const wchar_t* childArgv[] = {aExecutable, aOption}; + auto cmdLine( + mozilla::MakeCommandLine(mozilla::ArrayLength(childArgv), childArgv)); + + STARTUPINFOW si = {sizeof(si)}; + PROCESS_INFORMATION pi; + BOOL ok = + ::CreateProcessW(aExecutable, cmdLine.get(), nullptr, nullptr, FALSE, + CREATE_SUSPENDED, nullptr, nullptr, &si, &pi); + if (!ok) { + printf( + "TEST-FAILED | TestCrossProcessWin | " + "CreateProcessW falied - %08lx.\n", + GetLastError()); + return; + } + + mProcessId = pi.dwProcessId; + + mChildProcess.own(pi.hProcess); + mChildMainThread.own(pi.hThread); + } + + ~ChildProcess() { ::TerminateProcess(mChildProcess, 0); } + + operator HANDLE() const { return mChildProcess; } + DWORD GetProcessId() const { return mProcessId; } + + bool ResumeAndWaitUntilExit() { + if (::ResumeThread(mChildMainThread) == 0xffffffff) { + printf( + "TEST-FAILED | TestCrossProcessWin | " + "ResumeThread failed - %08lx\n", + GetLastError()); + return false; + } + + if (::WaitForSingleObject(mChildProcess, 60000) != WAIT_OBJECT_0) { + printf( + "TEST-FAILED | TestCrossProcessWin | " + "Unexpected result from WaitForSingleObject\n"); + return false; + } + + DWORD exitCode; + if (!::GetExitCodeProcess(mChildProcess, &exitCode)) { + printf( + "TEST-FAILED | TestCrossProcessWin | " + "GetExitCodeProcess failed - %08lx\n", + GetLastError()); + return false; + } + + return exitCode == 0; + } +}; + +HMODULE ChildProcess::sExecutableImageBase = 0; +volatile const DWORD ChildProcess::sReadOnlyProcessId = 0; + +int wmain(int argc, wchar_t* argv[]) { + printf("Process: %-8lx Base: %p\n", ::GetCurrentProcessId(), + ::GetModuleHandle(nullptr)); + + if (argc == 2 && wcscmp(argv[1], kChildArg) == 0) { + return ChildProcess::Main(); + } + + ChildProcess childProcess(argv[0], kChildArg); + if (!childProcess) { + return 1; + } + + if (!TestAddString()) { + return 1; + } + + if (!TestDependentModules()) { + return 1; + } + + if (!TestDynamicBlocklist()) { + return 1; + } + + LauncherResult remoteImageBase = + nt::GetProcessExeModule(childProcess); + if (remoteImageBase.isErr()) { + PrintLauncherError(remoteImageBase, "nt::GetProcessExeModule failed"); + return 1; + } + + nt::CrossExecTransferManager transferMgr(childProcess); + if (!transferMgr) { + printf( + "TEST-FAILED | TestCrossProcessWin | " + "CrossExecTransferManager instantiation failed.\n"); + return 1; + } + + LauncherVoidResult result = + transferMgr.Transfer(&ChildProcess::sExecutableImageBase, + &remoteImageBase.inspect(), sizeof(HMODULE)); + if (result.isErr()) { + PrintLauncherError(result, "ChildProcess::WriteData(Imagebase) failed"); + return 1; + } + + DWORD childPid = childProcess.GetProcessId(); + + DWORD* readOnlyData = const_cast(&ChildProcess::sReadOnlyProcessId); + result = transferMgr.Transfer(readOnlyData, &childPid, sizeof(DWORD)); + if (result.isOk()) { + printf( + "TEST-UNEXPECTED | TestCrossProcessWin | " + "A constant was located in a writable section."); + return 1; + } + + AutoVirtualProtect prot = + transferMgr.Protect(readOnlyData, sizeof(uint32_t), PAGE_READWRITE); + if (!prot) { + printf( + "TEST-FAILED | TestCrossProcessWin | " + "VirtualProtect failed - %08lx\n", + prot.GetError().AsHResult()); + return 1; + } + + result = transferMgr.Transfer(readOnlyData, &childPid, sizeof(DWORD)); + if (result.isErr()) { + PrintLauncherError(result, "ChildProcess::WriteData(PID) failed"); + return 1; + } + + result = gSharedSection.Init(); + if (result.isErr()) { + PrintLauncherError(result, "SharedSection::Init failed"); + return 1; + } + + result = gSharedSection.SetBlocklist(gShortList, false); + if (result.isErr()) { + PrintLauncherError(result, "SetBlocklist(gShortList) failed"); + return false; + } + + for (const char* testString : kTestDependentModulePaths) { + // Test AllocatedUnicodeString(const char*) that is used + // in IsDependentModule() + nt::AllocatedUnicodeString depModule(testString); + UNICODE_STRING depModuleLeafName; + nt::GetLeafName(&depModuleLeafName, depModule); + result = gSharedSection.AddDependentModule(&depModuleLeafName); + if (result.isErr()) { + PrintLauncherError(result, "SharedSection::AddDependentModule failed"); + return 1; + } + } + + result = + gSharedSection.TransferHandle(transferMgr, GENERIC_READ | GENERIC_WRITE); + if (result.isErr()) { + PrintLauncherError(result, "SharedSection::TransferHandle failed"); + return 1; + } + + // Close the section in the parent process before resuming the child process + gSharedSection.Reset(nullptr); + + if (!childProcess.ResumeAndWaitUntilExit()) { + return 1; + } + + printf("TEST-PASS | TestCrossProcessWin | All checks passed\n"); + return 0; +} diff --git a/browser/app/winlauncher/test/TestSafeThreadLocal.cpp b/browser/app/winlauncher/test/TestSafeThreadLocal.cpp new file mode 100644 index 0000000000..31af93c375 --- /dev/null +++ b/browser/app/winlauncher/test/TestSafeThreadLocal.cpp @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#define MOZ_USE_LAUNCHER_ERROR + +#include "freestanding/SafeThreadLocal.h" + +#include "mozilla/NativeNt.h" +#include "nsWindowsHelpers.h" + +#include +#include + +// Need a non-inline function to bypass compiler optimization that the thread +// local storage pointer is cached in a register before accessing a thread-local +// variable. +MOZ_NEVER_INLINE PVOID SwapThreadLocalStoragePointer(PVOID aNewValue) { + auto oldValue = mozilla::nt::RtlGetThreadLocalStoragePointer(); + mozilla::nt::RtlSetThreadLocalStoragePointerForTestingOnly(aNewValue); + return oldValue; +} + +static mozilla::freestanding::SafeThreadLocal gTheStorage; + +// Need non-inline functions to bypass compiler optimization that the thread +// local storage pointer is cached in a register before accessing a thread-local +// variable. See bug 1803322 for a motivating example. +MOZ_NEVER_INLINE int* getTheStorage() { return gTheStorage.get(); } +MOZ_NEVER_INLINE void setTheStorage(int* p) { gTheStorage.set(p); } + +static unsigned int __stdcall TestNonMainThread(void* aArg) { + for (int i = 0; i < 100; ++i) { + setTheStorage(&i); + if (getTheStorage() != &i) { + printf( + "TEST-FAILED | TestSafeThreadLocal | " + "A value is not correctly stored in the thread-local storage.\n"); + return 1; + } + } + return 0; +} + +extern "C" int wmain(int argc, wchar_t* argv[]) { + int dummy = 0x1234; + + auto origHead = SwapThreadLocalStoragePointer(nullptr); + // Setting gTheStorage when TLS is null. + setTheStorage(&dummy); + SwapThreadLocalStoragePointer(origHead); + + nsAutoHandle handles[8]; + for (auto& handle : handles) { + handle.own(reinterpret_cast( + ::_beginthreadex(nullptr, 0, TestNonMainThread, nullptr, 0, nullptr))); + } + + for (int i = 0; i < 100; ++i) { + if (getTheStorage() != &dummy) { + printf( + "TEST-FAILED | TestSafeThreadLocal | " + "A value is not correctly stored in the global scope.\n"); + return 1; + } + } + + for (auto& handle : handles) { + ::WaitForSingleObject(handle, INFINITE); + +#if !defined(MOZ_ASAN) + // ASAN builds under Windows 11 can have unexpected thread exit codes. + // See bug 1798796 + DWORD exitCode; + if (!::GetExitCodeThread(handle, &exitCode) || exitCode) { + return 1; + } +#endif // !defined(MOZ_ASAN) + } + + return 0; +} diff --git a/browser/app/winlauncher/test/TestSameBinary.cpp b/browser/app/winlauncher/test/TestSameBinary.cpp new file mode 100644 index 0000000000..2cb45f546f --- /dev/null +++ b/browser/app/winlauncher/test/TestSameBinary.cpp @@ -0,0 +1,255 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#define MOZ_USE_LAUNCHER_ERROR + +#include +#include + +#include + +#include "SameBinary.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/NativeNt.h" +#include "mozilla/Unused.h" +#include "mozilla/Vector.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "nsWindowsHelpers.h" + +#define EXPECT_SAMEBINARY_IS(expected, option, message) \ + do { \ + mozilla::LauncherResult isSame = \ + mozilla::IsSameBinaryAsParentProcess(option); \ + if (isSame.isErr()) { \ + PrintLauncherError(isSame, \ + "IsSameBinaryAsParentProcess returned error " \ + "when we were expecting success."); \ + return 1; \ + } \ + if (isSame.unwrap() != expected) { \ + PrintErrorMsg(message); \ + return 1; \ + } \ + } while (0) + +/** + * This test involves three processes: + * 1. The "Monitor" process, which is executed by |MonitorMain|. This process + * is responsible for integrating with the test harness, so it spawns the + * "Parent" process (2), and then waits for the other two processes to + * finish. + * 2. The "Parent" process, which is executed by |ParentMain|. This process + * creates the "Child" process (3) and then waits indefinitely. + * 3. The "Child" process, which is executed by |ChildMain| and carries out + * the actual test. It terminates the Parent process during its execution, + * using the Child PID as the Parent process's exit code. This serves as a + * hacky yet effective way to signal to the Monitor process which PID it + * should wait on to ensure that the Child process has exited. + */ + +static const char kMsgStart[] = "TEST-FAILED | SameBinary | "; + +inline void PrintErrorMsg(const char* aMsg) { + printf("%s%s\n", kMsgStart, aMsg); +} + +inline void PrintWinError(const char* aMsg) { + mozilla::WindowsError err(mozilla::WindowsError::FromLastError()); + printf("%s%s: %S\n", kMsgStart, aMsg, err.AsString().get()); +} + +template +inline void PrintLauncherError(const mozilla::LauncherResult& aResult, + const char* aMsg = nullptr) { + const char* const kSep = aMsg ? ": " : ""; + const char* msg = aMsg ? aMsg : ""; + const mozilla::LauncherError& err = aResult.inspectErr(); + printf("%s%s%s%S (%s:%d)\n", kMsgStart, msg, kSep, + err.mError.AsString().get(), err.mFile, err.mLine); +} + +static int ChildMain(DWORD aExpectedParentPid) { + mozilla::LauncherResult parentPid = mozilla::nt::GetParentProcessId(); + if (parentPid.isErr()) { + PrintLauncherError(parentPid); + return 1; + } + + if (parentPid.inspect() != aExpectedParentPid) { + PrintErrorMsg("Unexpected mismatch of parent PIDs"); + return 1; + } + + const DWORD kAccess = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE; + nsAutoHandle parentProcess( + ::OpenProcess(kAccess, FALSE, parentPid.inspect())); + if (!parentProcess) { + PrintWinError("Unexpectedly failed to call OpenProcess on parent"); + return 1; + } + + EXPECT_SAMEBINARY_IS( + true, mozilla::ImageFileCompareOption::Default, + "IsSameBinaryAsParentProcess returned incorrect result for identical " + "binaries"); + EXPECT_SAMEBINARY_IS( + true, mozilla::ImageFileCompareOption::CompareNtPathsOnly, + "IsSameBinaryAsParentProcess(CompareNtPathsOnly) returned incorrect " + "result for identical binaries"); + + // Total hack, but who cares? We'll set the parent's exit code as our PID + // so that the monitor process knows who to wait for! + if (!::TerminateProcess(parentProcess.get(), ::GetCurrentProcessId())) { + PrintWinError("Unexpected failure in TerminateProcess"); + return 1; + } + + // Close our handle to the parent process so that no references are held. + ::CloseHandle(parentProcess.disown()); + + // Querying a pid on a terminated process may still succeed some time after + // that process has been terminated. For the purposes of this test, we'll poll + // the OS until we cannot succesfully open the parentPid anymore. + const uint32_t kMaxAttempts = 100; + uint32_t curAttempt = 0; + while (HANDLE p = ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, + parentPid.inspect())) { + ::CloseHandle(p); + ::Sleep(100); + ++curAttempt; + if (curAttempt >= kMaxAttempts) { + PrintErrorMsg( + "Exhausted retry attempts waiting for parent pid to become invalid"); + return 1; + } + } + + EXPECT_SAMEBINARY_IS( + false, mozilla::ImageFileCompareOption::Default, + "IsSameBinaryAsParentProcess returned incorrect result for dead parent " + "process"); + EXPECT_SAMEBINARY_IS( + false, mozilla::ImageFileCompareOption::CompareNtPathsOnly, + "IsSameBinaryAsParentProcess(CompareNtPathsOnly) returned incorrect " + "result for dead parent process"); + + return 0; +} + +static nsReturnRef CreateSelfProcess(int argc, wchar_t* argv[]) { + nsAutoHandle empty; + + DWORD myPid = ::GetCurrentProcessId(); + + wchar_t strPid[11] = {}; +#if defined(__MINGW32__) + _ultow(myPid, strPid, 16); +#else + if (_ultow_s(myPid, strPid, 16)) { + PrintErrorMsg("_ultow_s failed"); + return empty.out(); + } +#endif // defined(__MINGW32__) + + wchar_t* extraArgs[] = {strPid}; + + auto cmdLine = mozilla::MakeCommandLine( + argc, argv, mozilla::ArrayLength(extraArgs), extraArgs); + if (!cmdLine) { + PrintErrorMsg("MakeCommandLine failed"); + return empty.out(); + } + + STARTUPINFOW si = {sizeof(si)}; + PROCESS_INFORMATION pi; + BOOL ok = + ::CreateProcessW(argv[0], cmdLine.get(), nullptr, nullptr, FALSE, + CREATE_UNICODE_ENVIRONMENT, nullptr, nullptr, &si, &pi); + if (!ok) { + PrintWinError("CreateProcess failed"); + return empty.out(); + } + + nsAutoHandle proc(pi.hProcess); + nsAutoHandle thd(pi.hThread); + + return proc.out(); +} + +static int ParentMain(int argc, wchar_t* argv[]) { + nsAutoHandle childProc(CreateSelfProcess(argc, argv)); + if (!childProc) { + return 1; + } + + if (::WaitForSingleObject(childProc.get(), INFINITE) != WAIT_OBJECT_0) { + PrintWinError( + "Unexpected result from WaitForSingleObject on child process"); + return 1; + } + + MOZ_ASSERT_UNREACHABLE("This process should be terminated by now"); + return 0; +} + +static int MonitorMain(int argc, wchar_t* argv[]) { + // In this process, "parent" means the process that will be running + // ParentMain, which is our child process (confusing, I know...) + nsAutoHandle parentProc(CreateSelfProcess(argc, argv)); + if (!parentProc) { + return 1; + } + + if (::WaitForSingleObject(parentProc.get(), 60000) != WAIT_OBJECT_0) { + PrintWinError("Unexpected result from WaitForSingleObject on parent"); + return 1; + } + + DWORD childPid; + if (!::GetExitCodeProcess(parentProc.get(), &childPid)) { + PrintWinError("GetExitCodeProcess failed"); + return 1; + } + + nsAutoHandle childProc(::OpenProcess(SYNCHRONIZE, FALSE, childPid)); + if (!childProc) { + // Nothing to wait on anymore, which is OK. + return 0; + } + + // We want no more references to parentProc + ::CloseHandle(parentProc.disown()); + + if (::WaitForSingleObject(childProc.get(), 60000) != WAIT_OBJECT_0) { + PrintWinError("Unexpected result from WaitForSingleObject on child"); + return 1; + } + + return 0; +} + +extern "C" int wmain(int argc, wchar_t* argv[]) { + if (argc == 3) { + return ChildMain(wcstoul(argv[2], nullptr, 16)); + } + + if (!mozilla::SetArgv0ToFullBinaryPath(argv)) { + return 1; + } + + if (argc == 1) { + return MonitorMain(argc, argv); + } + + if (argc == 2) { + return ParentMain(argc, argv); + } + + PrintErrorMsg("Unexpected argc"); + return 1; +} diff --git a/browser/app/winlauncher/test/moz.build b/browser/app/winlauncher/test/moz.build new file mode 100644 index 0000000000..a8503ddf55 --- /dev/null +++ b/browser/app/winlauncher/test/moz.build @@ -0,0 +1,30 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DisableStlWrapping() + +GeckoCppUnitTests( + [ + "TestCrossProcessWin", + "TestSafeThreadLocal", + "TestSameBinary", + ], + linkage=None, +) + +LOCAL_INCLUDES += [ + "/browser/app/winlauncher", +] + +OS_LIBS += [ + "ntdll", +] + +if CONFIG["CC_TYPE"] in ("gcc", "clang"): + # This allows us to use wmain as the entry point on mingw + LDFLAGS += [ + "-municode", + ] -- cgit v1.2.3