diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /browser/app/winlauncher/freestanding | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/app/winlauncher/freestanding')
14 files changed, 2164 insertions, 0 deletions
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 <intrin.h> +# 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 <int N> +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<LPCWSTR>(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..105ab6d918 --- /dev/null +++ b/browser/app/winlauncher/freestanding/DllBlocklist.cpp @@ -0,0 +1,641 @@ +/* -*- 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<NativeNtBlockSetEntry>(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::BLOCK_WIN8_AND_OLDER | + DllBlockInfoFlags::BLOCK_WIN7_AND_OLDER)) { + RTL_OSVERSIONINFOW osv = {sizeof(osv)}; + NTSTATUS ntStatus = ::RtlGetVersion(&osv); + if (!NT_SUCCESS(ntStatus)) { + return BlockAction::Error; + } + + if ((aInfo->mFlags & DllBlockInfoFlags::BLOCK_WIN8_AND_OLDER) && + (osv.dwMajorVersion > 6 || + (osv.dwMajorVersion == 6 && osv.dwMinorVersion > 2))) { + return BlockAction::Allow; + } + + if ((aInfo->mFlags & DllBlockInfoFlags::BLOCK_WIN7_AND_OLDER) && + (osv.dwMajorVersion > 6 || + (osv.dwMajorVersion == 6 && osv.dwMinorVersion > 1))) { + return BlockAction::Allow; + } + } + + 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<bool> 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<LdrLoadDllPtr> 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<NtMapViewOfSectionPtr> + 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<LdrLoadDllPtr> stub_LdrLoadDll; + +using NtMapViewOfSectionPtr = decltype(&::NtMapViewOfSection); + +extern CrossProcessDllInterceptor::FuncHookType<NtMapViewOfSectionPtr> + 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 <typename T, typename... Args> +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<Args>(aArgs)...); +} + +template <typename T> +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..266f2ec6a4 --- /dev/null +++ b/browser/app/winlauncher/freestanding/LoaderPrivateAPI.cpp @@ -0,0 +1,293 @@ +/* -*- 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<GlobalInitializerFn>(0); + +#pragma section(".freestd$z", read) +__declspec(allocate(".freestd$z")) static const GlobalInitializerFn FreeStdEnd = + reinterpret_cast<GlobalInitializerFn>(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> 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; + +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<ModuleLoadInfoVec>(); + if (!mModuleLoads) { + return; + } + } + + Unused << mModuleLoads->emplaceBack( + std::forward<ModuleLoadInfo>(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) { + nt::AutoSharedLock lock(gLoaderObserverLock); + return gLoaderObserver->SubstituteForLSP(aLSPLeafName, aOutHandle); +} + +void LoaderPrivateAPIImp::NotifyEndDllLoad(void* aContext, + NTSTATUS aLoadNtStatus, + ModuleLoadInfo&& aModuleLoadInfo) { + aModuleLoadInfo.SetEndLoadTimeStamp(); + + if (NT_SUCCESS(aLoadNtStatus)) { + aModuleLoadInfo.CaptureBacktrace(); + } + + nt::AutoSharedLock lock(gLoaderObserverLock); + + // 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)); +} + +nt::AllocatedUnicodeString LoaderPrivateAPIImp::GetSectionName( + void* aSectionAddr) { + const HANDLE kCurrentProcess = reinterpret_cast<HANDLE>(-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<HANDLE>(-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); + 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); + + 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*> 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<ModuleLoadFrame*> 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 <type_traits> + +#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 <typename T> +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<T>, + "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 <typename T> +MOZ_THREAD_LOCAL(T) +SafeThreadLocal<T>::sThreadLocal; + +template <typename T> +T SafeThreadLocal<T>::sGlobal = nullptr; + +template <typename T> +bool SafeThreadLocal<T>::sIsTlsUsed = false; + +template <typename T> +DWORD SafeThreadLocal<T>::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..eab0074f60 --- /dev/null +++ b/browser/app/winlauncher/freestanding/SharedSection.cpp @@ -0,0 +1,368 @@ +/* -*- 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 <algorithm> +#include "CheckForCaller.h" +#include "mozilla/BinarySearch.h" + +namespace { + +bool AddString(mozilla::Span<wchar_t> 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<decltype(m##name)>(*rvaToFunction); \ + } while (0) + +#define RESOLVE_FUNCTION(base, name) \ + m##name = reinterpret_cast<decltype(m##name)>( \ + base + reinterpret_cast<uintptr_t>(m##name)) + +void Kernel32ExportsSolver::Init() { + interceptor::MMPolicyInProcess policy; + auto k32Exports = nt::PEExportSection<interceptor::MMPolicyInProcess>::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<uintptr_t>(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<Layout>(); + 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<Layout>(); + 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<Layout>(); + 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<uintptr_t>(view) + kSharedViewSize; + size_t bytesCopied = aBlocklist.CopyTo( + view->mFirstBlockEntry, + bufferEnd - reinterpret_cast<uintptr_t>(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<Layout>(); + 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<uintptr_t>(mFirstBlockEntry); + uint32_t numEntries = 0; + for (DllBlockInfo* entry = mFirstBlockEntry; + entry->mName.Length && numEntries < GetMaxNumBlockEntries(); + ++entry) { + entry->mName.Buffer = reinterpret_cast<wchar_t*>( + arrayBase + reinterpret_cast<uintptr_t>(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<wchar_t> SharedSection::Layout::GetDependentModules() { + if (!mDependentModulePathArrayStart) { + return nullptr; + } + return Span<wchar_t>( + reinterpret_cast<wchar_t*>(reinterpret_cast<uintptr_t>(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; +} + +Span<const wchar_t> SharedSection::GetDependentModules() { + Layout* writeCopyView = EnsureWriteCopyView(); + return writeCopyView ? writeCopyView->GetDependentModules() : nullptr; +} + +Span<const DllBlockInfo> 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..4c3d1b8ef8 --- /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<wchar_t*>(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<int>( + ::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) | <DllBlockInfo for first entry in dynamic blocklist> | +// | | <DllBlockInfo for second entry in dynamic blocklist> | +// | | ... | +// | | <DllBlockInfo for last entry in dynamic blocklist> | +// | | <DllBlockInfo with mName.Length of 0> | +// | | 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<DllBlockInfo> GetModulePathArray() { + return Span<DllBlockInfo>( + mFirstBlockEntry, + (kSharedViewSize - (reinterpret_cast<uintptr_t>(mFirstBlockEntry) - + reinterpret_cast<uintptr_t>(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<wchar_t> 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(); + Span<const wchar_t> GetDependentModules() final override; + Span<const DllBlockInfo> 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 |