diff options
Diffstat (limited to 'toolkit/xre/dllservices')
79 files changed, 16999 insertions, 0 deletions
diff --git a/toolkit/xre/dllservices/DynamicBlocklist.h b/toolkit/xre/dllservices/DynamicBlocklist.h new file mode 100644 index 0000000000..9bdc7f4a51 --- /dev/null +++ b/toolkit/xre/dllservices/DynamicBlocklist.h @@ -0,0 +1,291 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_DynamicBlocklist_h +#define mozilla_DynamicBlocklist_h + +#include <winternl.h> + +#include "nsWindowsHelpers.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/NativeNt.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/Vector.h" + +#if defined(MOZILLA_INTERNAL_API) +# include "mozilla/dom/Promise.h" +# include "nsIOutputStream.h" +# include "nsTHashSet.h" +#endif // defined(MOZILLA_INTERNAL_API) + +#include "mozilla/WindowsDllBlocklistInfo.h" + +#if !defined(MOZILLA_INTERNAL_API) && defined(ENABLE_TESTS) +# include "mozilla/CmdLineAndEnvUtils.h" +# define BLOCKLIST_INSERT_TEST_ENTRY +#endif // !defined(MOZILLA_INTERNAL_API) && defined(ENABLE_TESTS) + +namespace mozilla { +using DllBlockInfo = DllBlockInfoT<UNICODE_STRING>; + +struct DynamicBlockListBase { + static constexpr uint32_t kSignature = 'FFBL'; // Firefox Blocklist + static constexpr uint32_t kCurrentVersion = 1; + + struct FileHeader { + uint32_t mSignature; + uint32_t mFileVersion; + uint32_t mPayloadSize; + }; +}; +// Define this class in a header so that TestCrossProcessWin +// can include and test it. +class DynamicBlockList final : public DynamicBlockListBase { + uint32_t mPayloadSize; + UniquePtr<uint8_t[]> mPayload; + +#ifdef ENABLE_TESTS + // These two definitions are needed for the DynamicBlocklistWriter to avoid + // writing this test entry out to the blocklist file, so compile these in + // even if MOZILLA_INTERNAL_API is defined. + public: + static constexpr wchar_t kTestDll[] = L"TestDllBlocklist_UserBlocked.dll"; + // kTestDll is null-terminated, but we don't want that for the blocklist + // file + static constexpr size_t kTestDllBytes = sizeof(kTestDll) - sizeof(wchar_t); + + private: +# ifdef BLOCKLIST_INSERT_TEST_ENTRY + // Set up a test entry in the blocklist, used for testing purposes. + void AssignTestEntry(DllBlockInfo* testEntry, uint32_t nameOffset) { + testEntry->mName.Length = kTestDllBytes; + testEntry->mName.MaximumLength = kTestDllBytes; + testEntry->mName.Buffer = reinterpret_cast<PWSTR>(nameOffset); + testEntry->mMaxVersion = DllBlockInfo::ALL_VERSIONS; + testEntry->mFlags = DllBlockInfo::FLAGS_DEFAULT; + } + + void CreateListWithTestEntry() { + mPayloadSize = sizeof(DllBlockInfo) * 2 + kTestDllBytes; + mPayload = MakeUnique<uint8_t[]>(mPayloadSize); + DllBlockInfo* entry = reinterpret_cast<DllBlockInfo*>(mPayload.get()); + AssignTestEntry(entry, sizeof(DllBlockInfo) * 2); + memcpy(mPayload.get() + sizeof(DllBlockInfo) * 2, kTestDll, kTestDllBytes); + ++entry; + entry->mName.Length = 0; + entry->mName.MaximumLength = 0; + } +# endif // BLOCKLIST_INSERT_TEST_ENTRY +#endif // ENABLE_TESTS + + bool LoadFile(const wchar_t* aPath) { +#ifdef BLOCKLIST_INSERT_TEST_ENTRY + const bool shouldInsertBlocklistTestEntry = + MOZ_UNLIKELY(mozilla::EnvHasValue("MOZ_DISABLE_NONLOCAL_CONNECTIONS") || + mozilla::EnvHasValue("MOZ_RUN_GTEST")); +#endif + + nsAutoHandle file( + ::CreateFileW(aPath, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); + if (!file) { +#ifdef BLOCKLIST_INSERT_TEST_ENTRY + if (shouldInsertBlocklistTestEntry) { + // If the blocklist file doesn't exist, we still want to include a test + // entry for testing purposes. + CreateListWithTestEntry(); + return true; + } +#endif + return false; + } + + DWORD bytesRead = 0; + FileHeader header; + BOOL ok = + ::ReadFile(file.get(), &header, sizeof(header), &bytesRead, nullptr); + if (!ok) { +#ifdef BLOCKLIST_INSERT_TEST_ENTRY + if (shouldInsertBlocklistTestEntry) { + // If we have a problem reading the blocklist file, we still want to + // include a test entry for testing purposes. + CreateListWithTestEntry(); + return true; + } +#endif + return false; + } + if (bytesRead != sizeof(header)) { + return false; + } + + if (header.mSignature != kSignature || + header.mFileVersion != kCurrentVersion) { + return false; + } + + uint32_t destinationPayloadSize = header.mPayloadSize; +#ifdef BLOCKLIST_INSERT_TEST_ENTRY + if (shouldInsertBlocklistTestEntry) { + // Write an extra entry for testing purposes + destinationPayloadSize += sizeof(DllBlockInfo) + kTestDllBytes; + } +#endif + UniquePtr<uint8_t[]> payload = + MakeUnique<uint8_t[]>(destinationPayloadSize); + ok = ::ReadFile(file.get(), payload.get(), header.mPayloadSize, &bytesRead, + nullptr); + if (!ok || bytesRead != header.mPayloadSize) { + return false; + } + + uint32_t sizeOfPayloadToIterateOver = header.mPayloadSize; +#ifdef BLOCKLIST_INSERT_TEST_ENTRY + bool haveWrittenTestEntry = false; + UNICODE_STRING testUnicodeString; + // Cast the const-ness away since we're only going to use + // this to compare against strings. + testUnicodeString.Buffer = const_cast<PWSTR>(kTestDll); + testUnicodeString.Length = kTestDllBytes; + testUnicodeString.MaximumLength = kTestDllBytes; +#endif + for (uint32_t offset = 0; offset < sizeOfPayloadToIterateOver; + offset += sizeof(DllBlockInfo)) { + DllBlockInfo* entry = + reinterpret_cast<DllBlockInfo*>(payload.get() + offset); + if (!entry->mName.Length) { +#ifdef BLOCKLIST_INSERT_TEST_ENTRY + if (shouldInsertBlocklistTestEntry && !haveWrittenTestEntry) { + // Shift everything forward + memmove(payload.get() + offset + sizeof(DllBlockInfo), + payload.get() + offset, header.mPayloadSize - offset); + AssignTestEntry(entry, destinationPayloadSize - kTestDllBytes); + haveWrittenTestEntry = true; + } +#endif + break; + } + + size_t stringOffset = reinterpret_cast<size_t>(entry->mName.Buffer); + if (stringOffset + entry->mName.Length > header.mPayloadSize) { + entry->mName.Length = 0; + break; + } + entry->mName.MaximumLength = entry->mName.Length; +#ifdef BLOCKLIST_INSERT_TEST_ENTRY + if (shouldInsertBlocklistTestEntry && !haveWrittenTestEntry) { + UNICODE_STRING currentUnicodeString; + currentUnicodeString.Buffer = + reinterpret_cast<PWSTR>(payload.get() + stringOffset); + currentUnicodeString.Length = entry->mName.Length; + currentUnicodeString.MaximumLength = entry->mName.Length; + if (RtlCompareUnicodeString(¤tUnicodeString, &testUnicodeString, + TRUE) > 0) { + // Shift everything forward + memmove(payload.get() + offset + sizeof(DllBlockInfo), + payload.get() + offset, header.mPayloadSize - offset); + AssignTestEntry(entry, destinationPayloadSize - kTestDllBytes); + haveWrittenTestEntry = true; + // Now we have expanded the area of valid memory, so there is more + // allowable space to iterate over. + sizeOfPayloadToIterateOver = destinationPayloadSize; + offset += sizeof(DllBlockInfo); + ++entry; + } + // The start of the string section will be moving ahead (or has already + // moved ahead) to make room for the test entry + entry->mName.Buffer += + sizeof(DllBlockInfo) / sizeof(entry->mName.Buffer[0]); + } +#endif + } +#ifdef BLOCKLIST_INSERT_TEST_ENTRY + if (shouldInsertBlocklistTestEntry) { + memcpy(payload.get() + destinationPayloadSize - kTestDllBytes, kTestDll, + kTestDllBytes); + } +#endif + + mPayloadSize = destinationPayloadSize; + mPayload = std::move(payload); + return true; + } + + public: + DynamicBlockList() : mPayloadSize(0) {} + explicit DynamicBlockList(const wchar_t* aPath) : mPayloadSize(0) { + LoadFile(aPath); + } + + DynamicBlockList(DynamicBlockList&&) = default; + DynamicBlockList& operator=(DynamicBlockList&&) = default; + DynamicBlockList(const DynamicBlockList&) = delete; + DynamicBlockList& operator=(const DynamicBlockList&) = delete; + + constexpr uint32_t GetPayloadSize() const { return mPayloadSize; } + + // Return the number of bytes copied + size_t CopyTo(void* aBuffer, size_t aBufferLength) const { + if (mPayloadSize > aBufferLength) { + return 0; + } + memcpy(aBuffer, mPayload.get(), mPayloadSize); + return mPayloadSize; + } +}; + +#if defined(MOZILLA_INTERNAL_API) && defined(MOZ_LAUNCHER_PROCESS) + +class DynamicBlocklistWriter final : public DynamicBlockListBase { + template <typename T> + static nsresult WriteValue(nsIOutputStream* aOutputStream, const T& aValue) { + uint32_t written; + return aOutputStream->Write(reinterpret_cast<const char*>(&aValue), + sizeof(T), &written); + } + + template <typename T> + static nsresult WriteBuffer(nsIOutputStream* aOutputStream, const T* aBuffer, + uint32_t aBufferSize) { + uint32_t written; + return aOutputStream->Write(reinterpret_cast<const char*>(aBuffer), + aBufferSize, &written); + } + + RefPtr<dom::Promise> mPromise; + Vector<DllBlockInfo> mArray; + // All strings are packed in this buffer without null characters + UniquePtr<uint8_t[]> mStringBuffer; + + size_t mArraySize; + size_t mStringBufferSize; + + nsresult WriteToFile(const nsAString& aName) const; + + public: + DynamicBlocklistWriter( + RefPtr<dom::Promise> aPromise, + const nsTHashSet<nsStringCaseInsensitiveHashKey>& aBlocklist); + ~DynamicBlocklistWriter() = default; + + DynamicBlocklistWriter(DynamicBlocklistWriter&&) = default; + DynamicBlocklistWriter& operator=(DynamicBlocklistWriter&&) = default; + DynamicBlocklistWriter(const DynamicBlocklistWriter&) = delete; + DynamicBlocklistWriter& operator=(const DynamicBlocklistWriter&) = delete; + + bool IsReady() const { return mStringBuffer.get(); } + + void Run(); + void Cancel(); +}; + +#endif // defined(MOZILLA_INTERNAL_API) && defined(MOZ_LAUNCHER_PROCESS) + +} // namespace mozilla + +#endif // mozilla_DynamicBlocklist_h diff --git a/toolkit/xre/dllservices/DynamicBlocklistWriter.cpp b/toolkit/xre/dllservices/DynamicBlocklistWriter.cpp new file mode 100644 index 0000000000..e967d073c5 --- /dev/null +++ b/toolkit/xre/dllservices/DynamicBlocklistWriter.cpp @@ -0,0 +1,139 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "mozilla/DynamicBlocklist.h" +#include "mozilla/LauncherRegistryInfo.h" + +#include "nsISafeOutputStream.h" +#include "nsNetUtil.h" + +namespace mozilla { +#if ENABLE_TESTS +nsDependentString testEntryString(DynamicBlockList::kTestDll, + DynamicBlockList::kTestDllBytes / + sizeof(DynamicBlockList::kTestDll[0])); +#endif + +bool ShouldWriteEntry(const nsAString& name) { +#if ENABLE_TESTS + return name != testEntryString; +#else + return true; +#endif +} + +DynamicBlocklistWriter::DynamicBlocklistWriter( + RefPtr<dom::Promise> aPromise, + const nsTHashSet<nsStringCaseInsensitiveHashKey>& aBlocklist) + : mPromise(aPromise), mArraySize(0), mStringBufferSize(0) { + CheckedUint32 payloadSize; + bool hasTestEntry = false; + for (const nsAString& name : aBlocklist) { + if (ShouldWriteEntry(name)) { + payloadSize += name.Length() * sizeof(char16_t); + if (!payloadSize.isValid()) { + return; + } + } else { + hasTestEntry = true; + } + } + + uint32_t entriesToWrite = aBlocklist.Count(); + if (hasTestEntry) { + entriesToWrite -= 1; + } + + mStringBufferSize = payloadSize.value(); + mArraySize = (entriesToWrite + 1) * sizeof(DllBlockInfo); + + payloadSize += mArraySize; + if (!payloadSize.isValid()) { + return; + } + + mStringBuffer = MakeUnique<uint8_t[]>(mStringBufferSize); + Unused << mArray.resize(entriesToWrite + 1); // aBlockEntries + sentinel + + size_t currentStringOffset = 0; + size_t i = 0; + for (const nsAString& name : aBlocklist) { + if (!ShouldWriteEntry(name)) { + continue; + } + const uint32_t nameSize = name.Length() * sizeof(char16_t); + + mArray[i].mMaxVersion = DllBlockInfo::ALL_VERSIONS; + mArray[i].mFlags = DllBlockInfo::Flags::FLAGS_DEFAULT; + + // Copy the module's name to the string buffer and store its offset + // in mName.Buffer + memcpy(mStringBuffer.get() + currentStringOffset, name.BeginReading(), + nameSize); + mArray[i].mName.Buffer = + reinterpret_cast<wchar_t*>(mArraySize + currentStringOffset); + // Only keep mName.Length and leave mName.MaximumLength to be zero + mArray[i].mName.Length = nameSize; + + currentStringOffset += nameSize; + ++i; + } +} + +nsresult DynamicBlocklistWriter::WriteToFile(const nsAString& aName) const { + nsCOMPtr<nsIFile> file; + MOZ_TRY(NS_NewLocalFile(aName, true, getter_AddRefs(file))); + + nsCOMPtr<nsIOutputStream> stream; + MOZ_TRY(NS_NewSafeLocalFileOutputStream(getter_AddRefs(stream), file)); + + MOZ_TRY(WriteValue(stream, kSignature)); + MOZ_TRY(WriteValue(stream, kCurrentVersion)); + MOZ_TRY(WriteValue(stream, + static_cast<uint32_t>(mArraySize + mStringBufferSize))); + MOZ_TRY(WriteBuffer(stream, mArray.begin(), mArraySize)); + MOZ_TRY(WriteBuffer(stream, mStringBuffer.get(), mStringBufferSize)); + + nsresult rv; + nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(stream, &rv); + if (NS_FAILED(rv)) { + return rv; + } + + return safeStream->Finish(); +} + +void DynamicBlocklistWriter::Run() { + MOZ_ASSERT(!NS_IsMainThread()); + + nsresult rv = NS_ERROR_ABORT; + + LauncherRegistryInfo regInfo; + LauncherResult<std::wstring> blocklistFile = regInfo.GetBlocklistFileName(); + if (blocklistFile.isOk()) { + const wchar_t* rawBuffer = blocklistFile.inspect().c_str(); + rv = WriteToFile(nsDependentString(rawBuffer)); + } + + NS_DispatchToMainThread( + // Don't capture mPromise by copy because we're not in the main thread + NS_NewRunnableFunction(__func__, [promise = std::move(mPromise), rv]() { + if (NS_SUCCEEDED(rv)) { + promise->MaybeResolve(JS::NullHandleValue); + } else { + promise->MaybeReject(rv); + } + })); +} + +void DynamicBlocklistWriter::Cancel() { + MOZ_ASSERT(NS_IsMainThread()); + if (mPromise) { + mPromise->MaybeReject(NS_ERROR_ABORT); + } +} + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/ModuleEvaluator.cpp b/toolkit/xre/dllservices/ModuleEvaluator.cpp new file mode 100644 index 0000000000..58814e34a5 --- /dev/null +++ b/toolkit/xre/dllservices/ModuleEvaluator.cpp @@ -0,0 +1,253 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "ModuleEvaluator.h" + +#include <algorithm> // For std::find() +#include <type_traits> + +#include <windows.h> +#include <shlobj.h> + +#include "mozilla/ArrayUtils.h" +#include "mozilla/ModuleVersionInfo.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/WinDllServices.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "nsReadableUtils.h" +#include "nsWindowsHelpers.h" +#include "nsXULAppAPI.h" + +namespace mozilla { + +// Fills a Vector with keyboard layout DLLs found in the registry. +// These are leaf names only, not full paths. Here we will convert them to +// lowercase before returning, to facilitate case-insensitive searches. +// On error, this may return partial results. +static Vector<nsString> GetKeyboardLayoutDlls() { + Vector<nsString> result; + + HKEY rawKey; + if (::RegOpenKeyExW(HKEY_LOCAL_MACHINE, + L"SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts", + 0, KEY_ENUMERATE_SUB_KEYS, &rawKey) != ERROR_SUCCESS) { + return result; + } + nsAutoRegKey key(rawKey); + + DWORD iKey = 0; + wchar_t strTemp[MAX_PATH] = {}; + while (true) { + DWORD strTempSize = ArrayLength(strTemp); + if (RegEnumKeyExW(rawKey, iKey, strTemp, &strTempSize, nullptr, nullptr, + nullptr, nullptr) != ERROR_SUCCESS) { + // ERROR_NO_MORE_ITEMS or a real error: bail with what we have. + return result; + } + iKey++; + + strTempSize = sizeof(strTemp); + if (::RegGetValueW(rawKey, strTemp, L"Layout File", RRF_RT_REG_SZ, nullptr, + strTemp, &strTempSize) == ERROR_SUCCESS && + strTempSize) { + nsString ws(strTemp, ((strTempSize + 1) / sizeof(wchar_t)) - 1); + ToLowerCase(ws); // To facilitate case-insensitive searches + Unused << result.emplaceBack(std::move(ws)); + } + } + + return result; +} + +/* static */ +bool ModuleEvaluator::ResolveKnownFolder(REFKNOWNFOLDERID aFolderId, + nsIFile** aOutFile) { + if (!aOutFile) { + return false; + } + + *aOutFile = nullptr; + + // Since we're running off main thread, we can't use NS_GetSpecialDirectory + PWSTR rawPath = nullptr; + HRESULT hr = + ::SHGetKnownFolderPath(aFolderId, KF_FLAG_DEFAULT, nullptr, &rawPath); + if (FAILED(hr)) { + return false; + } + + using ShellStringUniquePtr = + UniquePtr<std::remove_pointer_t<PWSTR>, CoTaskMemFreeDeleter>; + + ShellStringUniquePtr path(rawPath); + + nsresult rv = NS_NewLocalFile(nsDependentString(path.get()), false, aOutFile); + return NS_SUCCEEDED(rv); +} + +ModuleEvaluator::ModuleEvaluator() + : mKeyboardLayoutDlls(GetKeyboardLayoutDlls()) { + MOZ_ASSERT(XRE_IsParentProcess()); + +#if defined(_M_IX86) + // We want to resolve to SYSWOW64 when applicable + REFKNOWNFOLDERID systemFolderId = FOLDERID_SystemX86; +#else + REFKNOWNFOLDERID systemFolderId = FOLDERID_System; +#endif // defined(_M_IX86) + + bool resolveOk = + ResolveKnownFolder(systemFolderId, getter_AddRefs(mSysDirectory)); + MOZ_ASSERT(resolveOk); + if (!resolveOk) { + return; + } + + nsCOMPtr<nsIFile> winSxSDir; + resolveOk = ResolveKnownFolder(FOLDERID_Windows, getter_AddRefs(winSxSDir)); + MOZ_ASSERT(resolveOk); + if (!resolveOk) { + return; + } + + nsresult rv = winSxSDir->Append(u"WinSxS"_ns); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (NS_FAILED(rv)) { + return; + } + + mWinSxSDirectory = std::move(winSxSDir); + + nsCOMPtr<nsIFile> exeFile; + rv = XRE_GetBinaryPath(getter_AddRefs(exeFile)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (NS_FAILED(rv)) { + return; + } + + rv = exeFile->GetParent(getter_AddRefs(mExeDirectory)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (NS_FAILED(rv)) { + return; + } + + nsAutoString exePath; + rv = exeFile->GetPath(exePath); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (NS_FAILED(rv)) { + return; + } + + ModuleVersionInfo exeVi; + if (!exeVi.GetFromImage(exePath)) { + return; + } + + mExeVersion = Some(ModuleVersion(exeVi.mFileVersion.Version64())); +} + +ModuleEvaluator::operator bool() const { + return mExeVersion.isSome() && mExeDirectory && mSysDirectory && + mWinSxSDirectory; +} + +Maybe<ModuleTrustFlags> ModuleEvaluator::GetTrust( + const ModuleRecord& aModuleRecord) const { + MOZ_ASSERT(XRE_IsParentProcess()); + + // We start by checking authenticode signatures, as the presence of any + // signature will produce an immediate pass/fail. + if (aModuleRecord.mVendorInfo.isSome() && + aModuleRecord.mVendorInfo.ref().mSource == + VendorInfo::Source::Signature) { + const nsString& signedBy = aModuleRecord.mVendorInfo.ref().mVendor; + + if (signedBy.EqualsLiteral("Microsoft Windows")) { + return Some(ModuleTrustFlags::MicrosoftWindowsSignature); + } else if (signedBy.EqualsLiteral("Microsoft Corporation")) { + return Some(ModuleTrustFlags::MicrosoftWindowsSignature); + } else if (signedBy.EqualsLiteral("Mozilla Corporation")) { + return Some(ModuleTrustFlags::MozillaSignature); + } else { + // Being signed by somebody who is neither Microsoft nor us is an + // automatic and immediate disqualification. + return Some(ModuleTrustFlags::None); + } + } + + const nsCOMPtr<nsIFile>& dllFile = aModuleRecord.mResolvedDosName; + MOZ_ASSERT(!!dllFile); + if (!dllFile) { + return Nothing(); + } + + nsAutoString dllLeafLower; + if (NS_FAILED(dllFile->GetLeafName(dllLeafLower))) { + return Nothing(); + } + + ToLowerCase(dllLeafLower); // To facilitate case-insensitive searching + + // The JIT profiling module doesn't really have any other practical way to + // match; hard-code it as being trusted. + if (dllLeafLower.EqualsLiteral("jitpi.dll")) { + return Some(ModuleTrustFlags::JitPI); + } + + ModuleTrustFlags result = ModuleTrustFlags::None; + + nsresult rv; + bool contained; + + // Is the DLL in the system directory? + rv = mSysDirectory->Contains(dllFile, &contained); + if (NS_SUCCEEDED(rv) && contained) { + result |= ModuleTrustFlags::SystemDirectory; + } + + // Is the DLL in the WinSxS directory? Some Microsoft DLLs (e.g. comctl32) are + // loaded from here and don't have digital signatures. So while this is not a + // guarantee of trustworthiness, but is at least as valid as system32. + rv = mWinSxSDirectory->Contains(dllFile, &contained); + if (NS_SUCCEEDED(rv) && contained) { + result |= ModuleTrustFlags::WinSxSDirectory; + } + + // Is it a keyboard layout DLL? + if (std::find(mKeyboardLayoutDlls.begin(), mKeyboardLayoutDlls.end(), + dllLeafLower) != mKeyboardLayoutDlls.end()) { + result |= ModuleTrustFlags::KeyboardLayout; + // This doesn't guarantee trustworthiness by itself. Keyboard layouts also + // must be in the system directory. + } + + if (aModuleRecord.mVendorInfo.isSome() && + aModuleRecord.mVendorInfo.ref().mSource == + VendorInfo::Source::VersionInfo) { + const nsString& companyName = aModuleRecord.mVendorInfo.ref().mVendor; + + if (companyName.EqualsLiteral("Microsoft Corporation")) { + result |= ModuleTrustFlags::MicrosoftVersion; + } + } + + rv = mExeDirectory->Contains(dllFile, &contained); + if (NS_SUCCEEDED(rv) && contained) { + result |= ModuleTrustFlags::FirefoxDirectory; + + // If the DLL is in the Firefox directory, does it also share the Firefox + // version info? + if (mExeVersion.isSome() && aModuleRecord.mVersion.isSome() && + mExeVersion.value() == aModuleRecord.mVersion.value()) { + result |= ModuleTrustFlags::FirefoxDirectoryAndVersion; + } + } + + return Some(result); +} + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/ModuleEvaluator.h b/toolkit/xre/dllservices/ModuleEvaluator.h new file mode 100644 index 0000000000..565b5d4da4 --- /dev/null +++ b/toolkit/xre/dllservices/ModuleEvaluator.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_ModuleEvaluator_h +#define mozilla_ModuleEvaluator_h + +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/UntrustedModulesData.h" +#include "mozilla/Vector.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "nsCOMPtr.h" +#include "nsIFile.h" +#include "nsString.h" + +#include <shtypes.h> + +namespace mozilla { + +class ModuleRecord; + +/** + * This class performs trustworthiness evaluation for incoming DLLs. + */ +class MOZ_RAII ModuleEvaluator final { + public: + ModuleEvaluator(); + + explicit operator bool() const; + + Maybe<ModuleTrustFlags> GetTrust(const ModuleRecord& aModuleRecord) const; + + private: + static bool ResolveKnownFolder(REFKNOWNFOLDERID aFolderId, + nsIFile** aOutFile); + + private: + Maybe<ModuleVersion> mExeVersion; // Version number of the running EXE image + nsCOMPtr<nsIFile> mExeDirectory; + nsCOMPtr<nsIFile> mSysDirectory; + nsCOMPtr<nsIFile> mWinSxSDirectory; + Vector<nsString> mKeyboardLayoutDlls; +}; + +} // namespace mozilla + +#endif // mozilla_ModuleEvaluator_h diff --git a/toolkit/xre/dllservices/ModuleVersionInfo.cpp b/toolkit/xre/dllservices/ModuleVersionInfo.cpp new file mode 100644 index 0000000000..c8c2956a74 --- /dev/null +++ b/toolkit/xre/dllservices/ModuleVersionInfo.cpp @@ -0,0 +1,111 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "ModuleVersionInfo.h" + +#include "mozilla/UniquePtr.h" + +namespace mozilla { + +/** + * Gets a string value from a version info block with the specified translation + * and field name. + * + * @param aBlock [in] The binary version resource block + * @param aTranslation [in] The translation ID as obtained in the version + * translation list. + * @param aFieldName [in] Null-terminated name of the desired field + * @param aResult [out] Receives the string value, if successful. + * @return true if successful. aResult is unchanged upon failure. + */ +static bool QueryStringValue(const void* aBlock, DWORD aTranslation, + const wchar_t* aFieldName, nsAString& aResult) { + nsAutoString path; + path.AppendPrintf("\\StringFileInfo\\%02lX%02lX%02lX%02lX\\%S", + (aTranslation & 0x0000ff00) >> 8, + (aTranslation & 0x000000ff), + (aTranslation & 0xff000000) >> 24, + (aTranslation & 0x00ff0000) >> 16, aFieldName); + + wchar_t* lpBuffer = nullptr; + UINT len = 0; + if (!::VerQueryValueW(aBlock, path.get(), (PVOID*)&lpBuffer, &len)) { + return false; + } + aResult.Assign(lpBuffer, (size_t)len - 1); + return true; +} + +/** + * Searches through translations in the version resource for the requested + * field. English(US) is preferred, otherwise we take the first translation + * that succeeds. + * + * @param aBlock [in] The binary version resource block + * @param aTranslations [in] The list of translation IDs available + * @param aNumTrans [in] Number of items in aTranslations + * @param aFieldName [in] Null-terminated name of the desired field + * @param aResult [in] Receives the string value, if successful. + * @return true if successful. aResult is unchanged upon failure. + */ +static bool QueryStringValue(const void* aBlock, const DWORD* aTranslations, + size_t aNumTrans, const wchar_t* aFieldName, + nsAString& aResult) { + static const DWORD kPreferredTranslation = + 0x04b00409; // English (US), Unicode + if (QueryStringValue(aBlock, kPreferredTranslation, aFieldName, aResult)) { + return true; + } + for (size_t i = 0; i < aNumTrans; ++i) { + if (QueryStringValue(aBlock, aTranslations[i], aFieldName, aResult)) { + return true; + } + } + return false; +} + +bool ModuleVersionInfo::GetFromImage(const nsAString& aPath) { + nsString path(aPath); + DWORD infoSize = GetFileVersionInfoSizeW(path.get(), nullptr); + if (!infoSize) { + return false; + } + + auto verInfo = MakeUnique<BYTE[]>(infoSize); + if (!::GetFileVersionInfoW(path.get(), 0, infoSize, verInfo.get())) { + return false; + } + + VS_FIXEDFILEINFO* vInfo = nullptr; + UINT vInfoLen = 0; + if (::VerQueryValueW(verInfo.get(), L"\\", (LPVOID*)&vInfo, &vInfoLen)) { + mFileVersion = + VersionNumber(vInfo->dwFileVersionMS, vInfo->dwFileVersionLS); + mProductVersion = + VersionNumber(vInfo->dwProductVersionMS, vInfo->dwProductVersionLS); + } + + // Note that regardless the character set indicated, strings are always + // returned as Unicode by the Windows APIs. + DWORD* pTrans = nullptr; + UINT cbTrans = 0; + if (::VerQueryValueW(verInfo.get(), L"\\VarFileInfo\\Translation", + (PVOID*)&pTrans, &cbTrans)) { + size_t numTrans = cbTrans / sizeof(DWORD); + QueryStringValue(verInfo.get(), pTrans, numTrans, L"CompanyName", + mCompanyName); + QueryStringValue(verInfo.get(), pTrans, numTrans, L"ProductName", + mProductName); + QueryStringValue(verInfo.get(), pTrans, numTrans, L"LegalCopyright", + mLegalCopyright); + QueryStringValue(verInfo.get(), pTrans, numTrans, L"FileDescription", + mFileDescription); + } + + return true; +} + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/ModuleVersionInfo.h b/toolkit/xre/dllservices/ModuleVersionInfo.h new file mode 100644 index 0000000000..2b46817417 --- /dev/null +++ b/toolkit/xre/dllservices/ModuleVersionInfo.h @@ -0,0 +1,71 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_ModuleVersionInfo_h +#define mozilla_ModuleVersionInfo_h + +#include <windows.h> +#include "nsString.h" + +namespace mozilla { + +// Obtains basic version info from a module image's version info resource. +class ModuleVersionInfo { + public: + // We favor English(US) for these fields, otherwise we take the first + // translation provided in the version resource. + nsString mCompanyName; + nsString mProductName; + nsString mLegalCopyright; + nsString mFileDescription; + + // Represents an A.B.C.D style version number, internally stored as a uint64_t + class VersionNumber { + uint64_t mVersion64 = 0; + + public: + VersionNumber() = default; + + VersionNumber(DWORD aMostSig, DWORD aLeastSig) + : mVersion64((uint64_t)aMostSig << 32 | aLeastSig) {} + + uint16_t A() const { + return (uint16_t)((mVersion64 & 0xffff000000000000) >> 48); + } + + uint16_t B() const { + return (uint16_t)((mVersion64 & 0x0000ffff00000000) >> 32); + } + + uint16_t C() const { + return (uint16_t)((mVersion64 & 0x00000000ffff0000) >> 16); + } + + uint16_t D() const { return (uint16_t)(mVersion64 & 0x000000000000ffff); } + + uint64_t Version64() const { return mVersion64; } + + bool operator==(const VersionNumber& aOther) const { + return mVersion64 == aOther.mVersion64; + } + + nsCString ToString() const { + nsCString ret; + ret.AppendPrintf("%d.%d.%d.%d", (int)A(), (int)B(), (int)C(), (int)D()); + return ret; + } + }; + + VersionNumber mFileVersion; + VersionNumber mProductVersion; + + // Returns false if it has no version resource or has no fixed version info. + bool GetFromImage(const nsAString& aPath); +}; + +} // namespace mozilla + +#endif // mozilla_ModuleVersionInfo_h diff --git a/toolkit/xre/dllservices/UntrustedModulesData.cpp b/toolkit/xre/dllservices/UntrustedModulesData.cpp new file mode 100644 index 0000000000..8b99eb8573 --- /dev/null +++ b/toolkit/xre/dllservices/UntrustedModulesData.cpp @@ -0,0 +1,446 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "UntrustedModulesData.h" + +#include <windows.h> + +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/DynamicallyLinkedFunctionPtr.h" +#include "mozilla/FileUtilsWin.h" +#include "mozilla/Likely.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/WinDllServices.h" +#include "ModuleEvaluator.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsXULAppAPI.h" +#include "WinUtils.h" + +// Some utility functions + +static LONGLONG GetQPCFreq() { + static const LONGLONG sFreq = []() -> LONGLONG { + LARGE_INTEGER freq; + ::QueryPerformanceFrequency(&freq); + return freq.QuadPart; + }(); + + return sFreq; +} + +template <typename ReturnT> +static ReturnT QPCToTimeUnits(const LONGLONG aTimeStamp, + const LONGLONG aUnitsPerSec) { + return ReturnT(aTimeStamp * aUnitsPerSec) / ReturnT(GetQPCFreq()); +} + +template <typename ReturnT> +static ReturnT QPCToMilliseconds(const LONGLONG aTimeStamp) { + const LONGLONG kMillisecondsPerSec = 1000; + return QPCToTimeUnits<ReturnT>(aTimeStamp, kMillisecondsPerSec); +} + +template <typename ReturnT> +static ReturnT QPCToMicroseconds(const LONGLONG aTimeStamp) { + const LONGLONG kMicrosecondsPerSec = 1000000; + return QPCToTimeUnits<ReturnT>(aTimeStamp, kMicrosecondsPerSec); +} + +static LONGLONG TimeUnitsToQPC(const LONGLONG aTimeStamp, + const LONGLONG aUnitsPerSec) { + MOZ_ASSERT(aUnitsPerSec != 0); + + LONGLONG result = aTimeStamp; + result *= GetQPCFreq(); + result /= aUnitsPerSec; + return result; +} + +namespace mozilla { + +static Maybe<double> QPCLoadDurationToMilliseconds( + const ModuleLoadInfo& aNtInfo) { + if (aNtInfo.IsBare()) { + return Nothing(); + } + + return Some(QPCToMilliseconds<double>(aNtInfo.mLoadTimeInfo.QuadPart)); +} + +ModuleRecord::ModuleRecord() : mTrustFlags(ModuleTrustFlags::None) {} + +ModuleRecord::ModuleRecord(const nsAString& aResolvedNtPath) + : mResolvedNtName(aResolvedNtPath), mTrustFlags(ModuleTrustFlags::None) { + if (aResolvedNtPath.IsEmpty()) { + return; + } + + MOZ_ASSERT(XRE_IsParentProcess()); + + nsAutoString resolvedDosPath; + if (!NtPathToDosPath(aResolvedNtPath, resolvedDosPath)) { +#if defined(DEBUG) + nsAutoCString msg; + msg.AppendLiteral("NtPathToDosPath failed for path \""); + msg.Append(NS_ConvertUTF16toUTF8(aResolvedNtPath)); + msg.AppendLiteral("\""); + NS_WARNING(msg.get()); +#endif // defined(DEBUG) + return; + } + + nsresult rv = + NS_NewLocalFile(resolvedDosPath, false, getter_AddRefs(mResolvedDosName)); + if (NS_FAILED(rv) || !mResolvedDosName) { + return; + } + + GetVersionAndVendorInfo(resolvedDosPath); + + // Now sanitize the resolved DLL name. If we cannot sanitize this then this + // record must not be considered valid. + nsAutoString strSanitizedPath(resolvedDosPath); + if (!widget::WinUtils::PreparePathForTelemetry(strSanitizedPath)) { + return; + } + + mSanitizedDllName = strSanitizedPath; +} + +void ModuleRecord::GetVersionAndVendorInfo(const nsAString& aPath) { + RefPtr<DllServices> dllSvc(DllServices::Get()); + + // WinVerifyTrust is too slow and of limited utility for our purposes, so + // we pass SkipTrustVerification here to avoid it. + UniquePtr<wchar_t[]> signedBy( + dllSvc->GetBinaryOrgName(PromiseFlatString(aPath).get(), + AuthenticodeFlags::SkipTrustVerification)); + if (signedBy) { + mVendorInfo = Some(VendorInfo(VendorInfo::Source::Signature, + nsDependentString(signedBy.get()))); + } + + ModuleVersionInfo verInfo; + if (!verInfo.GetFromImage(aPath)) { + return; + } + + if (verInfo.mFileVersion.Version64()) { + mVersion = Some(ModuleVersion(verInfo.mFileVersion.Version64())); + } + + if (!mVendorInfo && !verInfo.mCompanyName.IsEmpty()) { + mVendorInfo = + Some(VendorInfo(VendorInfo::Source::VersionInfo, verInfo.mCompanyName)); + } +} + +bool ModuleRecord::IsXUL() const { + if (!mResolvedDosName) { + return false; + } + + nsAutoString leafName; + nsresult rv = mResolvedDosName->GetLeafName(leafName); + if (NS_FAILED(rv)) { + return false; + } + + return leafName.EqualsIgnoreCase("xul.dll"); +} + +int32_t ModuleRecord::GetScoreThreshold() const { +#ifdef ENABLE_TESTS + // Check whether we are running as an xpcshell test. + if (MOZ_UNLIKELY(mozilla::EnvHasValue("XPCSHELL_TEST_PROFILE_DIR"))) { + nsAutoString dllLeaf; + if (NS_SUCCEEDED(mResolvedDosName->GetLeafName(dllLeaf))) { + // During xpcshell tests, this DLL is hard-coded to pass through all + // criteria checks and still result in "untrusted" status, so it shows up + // in the untrusted modules ping for the test to examine. + // Setting the threshold very high ensures the test will cover all + // criteria. + if (dllLeaf.EqualsIgnoreCase("modules-test.dll")) { + return 99999; + } + } + } +#endif + + return 100; +} + +bool ModuleRecord::IsTrusted() const { + if (mTrustFlags == ModuleTrustFlags::None) { + return false; + } + + // These flags are immediate passes + if (mTrustFlags & + (ModuleTrustFlags::MicrosoftWindowsSignature | + ModuleTrustFlags::MozillaSignature | ModuleTrustFlags::JitPI)) { + return true; + } + + // The remaining flags, when set, each count for 50 points toward a + // trustworthiness score. + int32_t score = static_cast<int32_t>( + CountPopulation32(static_cast<uint32_t>(mTrustFlags))) * + 50; + return score >= GetScoreThreshold(); +} + +ProcessedModuleLoadEvent::ProcessedModuleLoadEvent() + : mProcessUptimeMS(0ULL), + mThreadId(0UL), + mBaseAddress(0U), + mIsDependent(false), + mLoadStatus(0) {} + +ProcessedModuleLoadEvent::ProcessedModuleLoadEvent( + glue::EnhancedModuleLoadInfo&& aModLoadInfo, + RefPtr<ModuleRecord>&& aModuleRecord) + : mProcessUptimeMS(QPCTimeStampToProcessUptimeMilliseconds( + aModLoadInfo.mNtLoadInfo.mBeginTimestamp)), + mLoadDurationMS(QPCLoadDurationToMilliseconds(aModLoadInfo.mNtLoadInfo)), + mThreadId(aModLoadInfo.mNtLoadInfo.mThreadId), + mThreadName(std::move(aModLoadInfo.mThreadName)), + mBaseAddress( + reinterpret_cast<uintptr_t>(aModLoadInfo.mNtLoadInfo.mBaseAddr)), + mModule(std::move(aModuleRecord)), + mIsDependent(aModLoadInfo.mNtLoadInfo.mIsDependent), + mLoadStatus(static_cast<uint32_t>(aModLoadInfo.mNtLoadInfo.mStatus)) { + if (!mModule || !(*mModule)) { + return; + } + + mRequestedDllName = aModLoadInfo.mNtLoadInfo.mRequestedDllName.AsString(); + + // If we're in the main process, sanitize the requested DLL name here. + // If not, we cannot use PreparePathForTelemetry because it may try to + // delayload shlwapi.dll and could fail if the process is sandboxed. + // We leave mRequestedDllName unsanitized here and sanitize it when + // transferring it to the main process. + // (See ParamTraits<mozilla::UntrustedModulesData>::ReadEvent) + if (XRE_IsParentProcess()) { + SanitizeRequestedDllName(); + } +} + +void ProcessedModuleLoadEvent::SanitizeRequestedDllName() { + if (!mRequestedDllName.IsEmpty() && + !widget::WinUtils::PreparePathForTelemetry(mRequestedDllName)) { + // If we cannot sanitize a path, we simply do not provide that field to + // Telemetry. + mRequestedDllName.Truncate(); + } +} + +/* static */ +Maybe<LONGLONG> +ProcessedModuleLoadEvent::ComputeQPCTimeStampForProcessCreation() { + // This is similar to the algorithm used by TimeStamp::ProcessCreation: + + // 1. Get the process creation timestamp as FILETIME; + FILETIME creationTime, exitTime, kernelTime, userTime; + if (!::GetProcessTimes(::GetCurrentProcess(), &creationTime, &exitTime, + &kernelTime, &userTime)) { + return Nothing(); + } + + // 2. Get current timestamps as both QPC and FILETIME; + LARGE_INTEGER nowQPC; + ::QueryPerformanceCounter(&nowQPC); + + static const StaticDynamicallyLinkedFunctionPtr<void(WINAPI*)(LPFILETIME)> + pGetSystemTimePreciseAsFileTime(L"kernel32.dll", + "GetSystemTimePreciseAsFileTime"); + + FILETIME nowFile; + if (pGetSystemTimePreciseAsFileTime) { + pGetSystemTimePreciseAsFileTime(&nowFile); + } else { + ::GetSystemTimeAsFileTime(&nowFile); + } + + // 3. Take the difference between the FILETIMEs from (1) and (2), + // respectively, yielding the elapsed process uptime in microseconds. + ULARGE_INTEGER ulCreation = { + {creationTime.dwLowDateTime, creationTime.dwHighDateTime}}; + ULARGE_INTEGER ulNow = {{nowFile.dwLowDateTime, nowFile.dwHighDateTime}}; + + ULONGLONG timeSinceCreationMicroSec = + (ulNow.QuadPart - ulCreation.QuadPart) / 10ULL; + + // 4. Convert the QPC timestamp from (1) to microseconds. + LONGLONG nowQPCMicroSec = QPCToMicroseconds<LONGLONG>(nowQPC.QuadPart); + + // 5. Convert the elapsed uptime to an absolute timestamp by subtracting + // from (4), which yields the absolute timestamp for process creation. + // We convert back to QPC units before returning. + const LONGLONG kMicrosecondsPerSec = 1000000; + return Some(TimeUnitsToQPC(nowQPCMicroSec - timeSinceCreationMicroSec, + kMicrosecondsPerSec)); +} + +/* static */ +uint64_t ProcessedModuleLoadEvent::QPCTimeStampToProcessUptimeMilliseconds( + const LARGE_INTEGER& aTimeStamp) { + static const Maybe<LONGLONG> sProcessCreationTimeStamp = + ComputeQPCTimeStampForProcessCreation(); + + if (!sProcessCreationTimeStamp) { + return 0ULL; + } + + LONGLONG diff = aTimeStamp.QuadPart - sProcessCreationTimeStamp.value(); + return QPCToMilliseconds<uint64_t>(diff); +} + +bool ProcessedModuleLoadEvent::IsXULLoad() const { + if (!mModule) { + return false; + } + + return mModule->IsXUL(); +} + +bool ProcessedModuleLoadEvent::IsTrusted() const { + if (!mModule) { + return false; + } + + return mModule->IsTrusted(); +} + +void UntrustedModulesData::AddNewLoads( + const ModulesMap& aModules, UntrustedModuleLoadingEvents&& aEvents, + Vector<Telemetry::ProcessedStack>&& aStacks) { + MOZ_ASSERT(aEvents.length() == aStacks.length()); + for (const auto& entry : aModules) { + if (entry.GetData()->IsTrusted()) { + // Filter out trusted module records + continue; + } + + Unused << mModules.LookupOrInsert(entry.GetKey(), entry.GetData()); + } + + MOZ_ASSERT(mEvents.length() <= kMaxEvents); + + mNumEvents += aStacks.length(); + mEvents.extendBack(std::move(aEvents)); + for (auto&& stack : aStacks) { + mStacks.AddStack(stack); + } +} + +void UntrustedModulesData::MergeModules(UntrustedModulesData& aNewData) { + for (auto item : aNewData.mEvents) { + mModules.WithEntryHandle(item->mEvent.mModule->mResolvedNtName, + [&](auto&& addPtr) { + if (addPtr) { + // Even though the path of a ModuleRecord + // matches, the object of ModuleRecord can be + // different. Make sure the event's mModule + // points to an object in mModules. + item->mEvent.mModule = addPtr.Data(); + } else { + addPtr.Insert(item->mEvent.mModule); + } + }); + } +} + +void UntrustedModulesData::Merge(UntrustedModulesData&& aNewData) { + // Don't merge loading events of a different process + MOZ_ASSERT((mProcessType == aNewData.mProcessType) && + (mPid == aNewData.mPid)); + + UntrustedModulesData newData(std::move(aNewData)); + + if (!mNumEvents) { + mNumEvents = newData.mNumEvents; + mModules = std::move(newData.mModules); + mEvents = std::move(newData.mEvents); + mStacks = std::move(newData.mStacks); + return; + } + + MergeModules(newData); + mNumEvents += newData.mNumEvents; + mEvents.extendBack(std::move(newData.mEvents)); + mStacks.AddStacks(newData.mStacks); +} + +void UntrustedModulesData::Truncate() { + mStacks.Clear(); + + if (mNumEvents <= kMaxEvents) { + return; + } + + UntrustedModuleLoadingEvents events; + events.splice(0, mEvents, mNumEvents - kMaxEvents, kMaxEvents); + std::swap(events, mEvents); + mNumEvents = kMaxEvents; +} + +void UntrustedModulesData::MergeWithoutStacks(UntrustedModulesData&& aNewData) { + // Don't merge loading events of a different process + MOZ_ASSERT((mProcessType == aNewData.mProcessType) && + (mPid == aNewData.mPid)); + MOZ_ASSERT(!mStacks.GetStackCount()); + + UntrustedModulesData newData(std::move(aNewData)); + + if (mNumEvents > 0) { + MergeModules(newData); + } else { + mModules = std::move(newData.mModules); + } + + mNumEvents += newData.mNumEvents; + mEvents.extendBack(std::move(newData.mEvents)); + + Truncate(); +} + +void UntrustedModulesData::Swap(UntrustedModulesData& aOther) { + GeckoProcessType tmpProcessType = mProcessType; + mProcessType = aOther.mProcessType; + aOther.mProcessType = tmpProcessType; + + DWORD tmpPid = mPid; + mPid = aOther.mPid; + aOther.mPid = tmpPid; + + TimeDuration tmpElapsed = mElapsed; + mElapsed = aOther.mElapsed; + aOther.mElapsed = tmpElapsed; + + mModules.SwapElements(aOther.mModules); + std::swap(mNumEvents, aOther.mNumEvents); + std::swap(mEvents, aOther.mEvents); + mStacks.Swap(aOther.mStacks); + + Maybe<double> tmpXULLoadDurationMS = mXULLoadDurationMS; + mXULLoadDurationMS = aOther.mXULLoadDurationMS; + aOther.mXULLoadDurationMS = tmpXULLoadDurationMS; + + uint32_t tmpSanitizationFailures = mSanitizationFailures; + mSanitizationFailures = aOther.mSanitizationFailures; + aOther.mSanitizationFailures = tmpSanitizationFailures; + + uint32_t tmpTrustTestFailures = mTrustTestFailures; + mTrustTestFailures = aOther.mTrustTestFailures; + aOther.mTrustTestFailures = tmpTrustTestFailures; +} + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/UntrustedModulesData.h b/toolkit/xre/dllservices/UntrustedModulesData.h new file mode 100644 index 0000000000..1bd6b91a36 --- /dev/null +++ b/toolkit/xre/dllservices/UntrustedModulesData.h @@ -0,0 +1,650 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_UntrustedModulesData_h +#define mozilla_UntrustedModulesData_h + +#if defined(XP_WIN) + +# include "ipc/IPCMessageUtils.h" +# include "mozilla/CombinedStacks.h" +# include "mozilla/DebugOnly.h" +# include "mozilla/LinkedList.h" +# include "mozilla/Maybe.h" +# include "mozilla/RefPtr.h" +# include "mozilla/TypedEnumBits.h" +# include "mozilla/Unused.h" +# include "mozilla/Variant.h" +# include "mozilla/Vector.h" +# include "mozilla/WinHeaderOnlyUtils.h" +# include "nsCOMPtr.h" +# include "nsHashKeys.h" +# include "nsIFile.h" +# include "nsISupportsImpl.h" +# include "nsRefPtrHashtable.h" +# include "nsString.h" +# include "nsXULAppAPI.h" + +namespace mozilla { +namespace glue { +struct EnhancedModuleLoadInfo; +} // namespace glue + +enum class ModuleTrustFlags : uint32_t { + None = 0, + MozillaSignature = 1, + MicrosoftWindowsSignature = 2, + MicrosoftVersion = 4, + FirefoxDirectory = 8, + FirefoxDirectoryAndVersion = 0x10, + SystemDirectory = 0x20, + KeyboardLayout = 0x40, + JitPI = 0x80, + WinSxSDirectory = 0x100, +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(ModuleTrustFlags); + +class VendorInfo final { + public: + enum class Source : uint32_t { + None, + Signature, + VersionInfo, + }; + + VendorInfo() : mSource(Source::None) {} + VendorInfo(const Source aSource, const nsAString& aVendor) + : mSource(aSource), mVendor(aVendor) { + MOZ_ASSERT(aSource != Source::None && !aVendor.IsEmpty()); + } + + Source mSource; + nsString mVendor; +}; + +class ModulesMap; + +class ModuleRecord final { + public: + explicit ModuleRecord(const nsAString& aResolvedNtPath); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ModuleRecord) + + nsString mResolvedNtName; + nsCOMPtr<nsIFile> mResolvedDosName; + nsString mSanitizedDllName; + Maybe<ModuleVersion> mVersion; + Maybe<VendorInfo> mVendorInfo; + ModuleTrustFlags mTrustFlags; + + explicit operator bool() const { return !mSanitizedDllName.IsEmpty(); } + bool IsXUL() const; + bool IsTrusted() const; + + ModuleRecord(const ModuleRecord&) = delete; + ModuleRecord(ModuleRecord&&) = delete; + + ModuleRecord& operator=(const ModuleRecord&) = delete; + ModuleRecord& operator=(ModuleRecord&&) = delete; + + private: + ModuleRecord(); + ~ModuleRecord() = default; + void GetVersionAndVendorInfo(const nsAString& aPath); + int32_t GetScoreThreshold() const; + + friend struct ::IPC::ParamTraits<ModulesMap>; +}; + +/** + * This type holds module path data using one of two internal representations. + * It may be created from either a nsTHashtable or a Vector, and may be + * serialized from either representation into a common format over the wire. + * Deserialization always uses the Vector representation. + */ +struct ModulePaths final { + using SetType = nsTHashtable<nsStringCaseInsensitiveHashKey>; + using VecType = Vector<nsString>; + + Variant<SetType, VecType> mModuleNtPaths; + + template <typename T> + explicit ModulePaths(T&& aPaths) + : mModuleNtPaths(AsVariant(std::forward<T>(aPaths))) {} + + ModulePaths() : mModuleNtPaths(VecType()) {} + + ModulePaths(const ModulePaths& aOther) = delete; + ModulePaths(ModulePaths&& aOther) = default; + ModulePaths& operator=(const ModulePaths&) = delete; + ModulePaths& operator=(ModulePaths&&) = default; +}; + +class ProcessedModuleLoadEvent final { + public: + ProcessedModuleLoadEvent(); + ProcessedModuleLoadEvent(glue::EnhancedModuleLoadInfo&& aModLoadInfo, + RefPtr<ModuleRecord>&& aModuleRecord); + + explicit operator bool() const { return mModule && *mModule; } + bool IsXULLoad() const; + bool IsTrusted() const; + + uint64_t mProcessUptimeMS; + Maybe<double> mLoadDurationMS; + DWORD mThreadId; + nsCString mThreadName; + nsString mRequestedDllName; + // We intentionally store mBaseAddress as part of the event and not the + // module, as relocation may cause it to change between loads. If so, we want + // to know about it. + uintptr_t mBaseAddress; + RefPtr<ModuleRecord> mModule; + bool mIsDependent; + uint32_t mLoadStatus; // corresponding to enum ModuleLoadInfo::Status + + ProcessedModuleLoadEvent(const ProcessedModuleLoadEvent&) = delete; + ProcessedModuleLoadEvent& operator=(const ProcessedModuleLoadEvent&) = delete; + + ProcessedModuleLoadEvent(ProcessedModuleLoadEvent&&) = default; + ProcessedModuleLoadEvent& operator=(ProcessedModuleLoadEvent&&) = default; + + void SanitizeRequestedDllName(); + + private: + static Maybe<LONGLONG> ComputeQPCTimeStampForProcessCreation(); + static uint64_t QPCTimeStampToProcessUptimeMilliseconds( + const LARGE_INTEGER& aTimeStamp); +}; + +// Declaring ModulesMap this way makes it much easier to forward declare than +// if we had used |using| or |typedef|. +class ModulesMap final + : public nsRefPtrHashtable<nsStringCaseInsensitiveHashKey, ModuleRecord> { + public: + ModulesMap() + : nsRefPtrHashtable<nsStringCaseInsensitiveHashKey, ModuleRecord>() {} +}; + +struct ProcessedModuleLoadEventContainer final + : public LinkedListElement<ProcessedModuleLoadEventContainer> { + ProcessedModuleLoadEvent mEvent; + ProcessedModuleLoadEventContainer() = default; + explicit ProcessedModuleLoadEventContainer(ProcessedModuleLoadEvent&& aEvent) + : mEvent(std::move(aEvent)) {} + + ProcessedModuleLoadEventContainer(ProcessedModuleLoadEventContainer&&) = + default; + ProcessedModuleLoadEventContainer& operator=( + ProcessedModuleLoadEventContainer&&) = default; + ProcessedModuleLoadEventContainer(const ProcessedModuleLoadEventContainer&) = + delete; + ProcessedModuleLoadEventContainer& operator=( + const ProcessedModuleLoadEventContainer&) = delete; +}; +using UntrustedModuleLoadingEvents = + AutoCleanLinkedList<ProcessedModuleLoadEventContainer>; + +class UntrustedModulesData final { + // Merge aNewData.mEvents into this->mModules and also + // make module entries in aNewData point to items in this->mModules. + void MergeModules(UntrustedModulesData& aNewData); + + public: + // Ensure mEvents will never retain more than kMaxEvents events. + // This constant matches the maximum in Telemetry::CombinedStacks. + static constexpr size_t kMaxEvents = 50; + + UntrustedModulesData() + : mProcessType(XRE_GetProcessType()), + mPid(::GetCurrentProcessId()), + mNumEvents(0), + mSanitizationFailures(0), + mTrustTestFailures(0) {} + + UntrustedModulesData(UntrustedModulesData&&) = default; + UntrustedModulesData& operator=(UntrustedModulesData&&) = default; + + UntrustedModulesData(const UntrustedModulesData&) = delete; + UntrustedModulesData& operator=(const UntrustedModulesData&) = delete; + + explicit operator bool() const { + return !mEvents.isEmpty() || mSanitizationFailures || mTrustTestFailures || + mXULLoadDurationMS.isSome(); + } + + void AddNewLoads(const ModulesMap& aModulesMap, + UntrustedModuleLoadingEvents&& aEvents, + Vector<Telemetry::ProcessedStack>&& aStacks); + void Merge(UntrustedModulesData&& aNewData); + void MergeWithoutStacks(UntrustedModulesData&& aNewData); + void Swap(UntrustedModulesData& aOther); + + // Drop callstack data and old loading events. + void Truncate(); + + GeckoProcessType mProcessType; + DWORD mPid; + TimeDuration mElapsed; + ModulesMap mModules; + uint32_t mNumEvents; + UntrustedModuleLoadingEvents mEvents; + Telemetry::CombinedStacks mStacks; + Maybe<double> mXULLoadDurationMS; + uint32_t mSanitizationFailures; + uint32_t mTrustTestFailures; +}; + +class ModulesMapResult final { + public: + ModulesMapResult() : mTrustTestFailures(0) {} + + ModulesMapResult(const ModulesMapResult& aOther) = delete; + ModulesMapResult(ModulesMapResult&& aOther) = default; + ModulesMapResult& operator=(const ModulesMapResult& aOther) = delete; + ModulesMapResult& operator=(ModulesMapResult&& aOther) = default; + + ModulesMap mModules; + uint32_t mTrustTestFailures; +}; + +} // namespace mozilla + +namespace IPC { + +template <> +struct ParamTraits<mozilla::ModuleVersion> { + typedef mozilla::ModuleVersion paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + aWriter->WriteUInt64(aParam.AsInteger()); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + uint64_t ver; + if (!aReader->ReadUInt64(&ver)) { + return false; + } + + *aResult = ver; + return true; + } +}; + +template <> +struct ParamTraits<mozilla::VendorInfo> { + typedef mozilla::VendorInfo paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + aWriter->WriteUInt32(static_cast<uint32_t>(aParam.mSource)); + WriteParam(aWriter, aParam.mVendor); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + uint32_t source; + if (!aReader->ReadUInt32(&source)) { + return false; + } + + aResult->mSource = static_cast<mozilla::VendorInfo::Source>(source); + + if (!ReadParam(aReader, &aResult->mVendor)) { + return false; + } + + return true; + } +}; + +template <> +struct ParamTraits<mozilla::ModuleRecord> { + typedef mozilla::ModuleRecord paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mResolvedNtName); + + nsAutoString resolvedDosName; + if (aParam.mResolvedDosName) { + mozilla::DebugOnly<nsresult> rv = + aParam.mResolvedDosName->GetPath(resolvedDosName); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + WriteParam(aWriter, resolvedDosName); + WriteParam(aWriter, aParam.mSanitizedDllName); + WriteParam(aWriter, aParam.mVersion); + WriteParam(aWriter, aParam.mVendorInfo); + aWriter->WriteUInt32(static_cast<uint32_t>(aParam.mTrustFlags)); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + if (!ReadParam(aReader, &aResult->mResolvedNtName)) { + return false; + } + + nsAutoString resolvedDosName; + if (!ReadParam(aReader, &resolvedDosName)) { + return false; + } + + if (resolvedDosName.IsEmpty()) { + aResult->mResolvedDosName = nullptr; + } else if (NS_FAILED(NS_NewLocalFile( + resolvedDosName, false, + getter_AddRefs(aResult->mResolvedDosName)))) { + return false; + } + + if (!ReadParam(aReader, &aResult->mSanitizedDllName)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mVersion)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mVendorInfo)) { + return false; + } + + uint32_t trustFlags; + if (!aReader->ReadUInt32(&trustFlags)) { + return false; + } + + aResult->mTrustFlags = static_cast<mozilla::ModuleTrustFlags>(trustFlags); + return true; + } +}; + +template <> +struct ParamTraits<mozilla::ModulesMap> { + typedef mozilla::ModulesMap paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + aWriter->WriteUInt32(aParam.Count()); + + for (const auto& entry : aParam) { + MOZ_RELEASE_ASSERT(entry.GetData()); + WriteParam(aWriter, entry.GetKey()); + WriteParam(aWriter, *(entry.GetData())); + } + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + uint32_t count; + if (!ReadParam(aReader, &count)) { + return false; + } + + for (uint32_t current = 0; current < count; ++current) { + nsAutoString key; + if (!ReadParam(aReader, &key) || key.IsEmpty()) { + return false; + } + + RefPtr<mozilla::ModuleRecord> rec(new mozilla::ModuleRecord()); + if (!ReadParam(aReader, rec.get())) { + return false; + } + + aResult->InsertOrUpdate(key, std::move(rec)); + } + + return true; + } +}; + +template <> +struct ParamTraits<mozilla::ModulePaths> { + typedef mozilla::ModulePaths paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + aParam.mModuleNtPaths.match( + [aWriter](const paramType::SetType& aSet) { WriteSet(aWriter, aSet); }, + [aWriter](const paramType::VecType& aVec) { + WriteVector(aWriter, aVec); + }); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + uint32_t len; + if (!aReader->ReadUInt32(&len)) { + return false; + } + + // As noted in the comments for ModulePaths, we only deserialize using the + // Vector representation. + auto& vec = aResult->mModuleNtPaths.as<paramType::VecType>(); + if (!vec.reserve(len)) { + return false; + } + + for (uint32_t idx = 0; idx < len; ++idx) { + nsString str; + if (!ReadParam(aReader, &str)) { + return false; + } + + if (!vec.emplaceBack(std::move(str))) { + return false; + } + } + + return true; + } + + private: + // NB: This function must write out the set in the same format as WriteVector + static void WriteSet(MessageWriter* aWriter, const paramType::SetType& aSet) { + aWriter->WriteUInt32(aSet.Count()); + for (const auto& key : aSet.Keys()) { + WriteParam(aWriter, key); + } + } + + // NB: This function must write out the vector in the same format as WriteSet + static void WriteVector(MessageWriter* aWriter, + const paramType::VecType& aVec) { + aWriter->WriteUInt32(aVec.length()); + for (auto const& item : aVec) { + WriteParam(aWriter, item); + } + } +}; + +template <> +struct ParamTraits<mozilla::UntrustedModulesData> { + typedef mozilla::UntrustedModulesData paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + aWriter->WriteUInt32(aParam.mProcessType); + aWriter->WriteULong(aParam.mPid); + WriteParam(aWriter, aParam.mElapsed); + WriteParam(aWriter, aParam.mModules); + + aWriter->WriteUInt32(aParam.mNumEvents); + for (auto event : aParam.mEvents) { + WriteEvent(aWriter, event->mEvent); + } + + WriteParam(aWriter, aParam.mStacks); + WriteParam(aWriter, aParam.mXULLoadDurationMS); + aWriter->WriteUInt32(aParam.mSanitizationFailures); + aWriter->WriteUInt32(aParam.mTrustTestFailures); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + uint32_t processType; + if (!aReader->ReadUInt32(&processType)) { + return false; + } + + aResult->mProcessType = static_cast<GeckoProcessType>(processType); + + if (!aReader->ReadULong(&aResult->mPid)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mElapsed)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mModules)) { + return false; + } + + // We read mEvents manually so that we can use ReadEvent defined below. + if (!ReadParam(aReader, &aResult->mNumEvents)) { + return false; + } + + for (uint32_t curEventIdx = 0; curEventIdx < aResult->mNumEvents; + ++curEventIdx) { + auto newEvent = + mozilla::MakeUnique<mozilla::ProcessedModuleLoadEventContainer>(); + if (!ReadEvent(aReader, &newEvent->mEvent, aResult->mModules)) { + return false; + } + aResult->mEvents.insertBack(newEvent.release()); + } + + if (!ReadParam(aReader, &aResult->mStacks)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mXULLoadDurationMS)) { + return false; + } + + if (!aReader->ReadUInt32(&aResult->mSanitizationFailures)) { + return false; + } + + if (!aReader->ReadUInt32(&aResult->mTrustTestFailures)) { + return false; + } + + return true; + } + + private: + // Because ProcessedModuleLoadEvent depends on a hash table from + // UntrustedModulesData, we do its serialization as part of this + // specialization. + static void WriteEvent(MessageWriter* aWriter, + const mozilla::ProcessedModuleLoadEvent& aParam) { + aWriter->WriteUInt64(aParam.mProcessUptimeMS); + WriteParam(aWriter, aParam.mLoadDurationMS); + aWriter->WriteULong(aParam.mThreadId); + WriteParam(aWriter, aParam.mThreadName); + WriteParam(aWriter, aParam.mRequestedDllName); + WriteParam(aWriter, aParam.mBaseAddress); + WriteParam(aWriter, aParam.mIsDependent); + WriteParam(aWriter, aParam.mLoadStatus); + + // We don't write the ModuleRecord directly; we write its key into the + // UntrustedModulesData::mModules hash table. + MOZ_ASSERT(aParam.mModule && !aParam.mModule->mResolvedNtName.IsEmpty()); + WriteParam(aWriter, aParam.mModule->mResolvedNtName); + } + + // Because ProcessedModuleLoadEvent depends on a hash table from + // UntrustedModulesData, we do its deserialization as part of this + // specialization. + static bool ReadEvent(MessageReader* aReader, + mozilla::ProcessedModuleLoadEvent* aResult, + const mozilla::ModulesMap& aModulesMap) { + if (!aReader->ReadUInt64(&aResult->mProcessUptimeMS)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mLoadDurationMS)) { + return false; + } + + if (!aReader->ReadULong(&aResult->mThreadId)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mThreadName)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mRequestedDllName)) { + return false; + } + + // When ProcessedModuleLoadEvent was constructed in a child process, we left + // mRequestedDllName unsanitized, so now is a good time to sanitize it. + aResult->SanitizeRequestedDllName(); + + if (!ReadParam(aReader, &aResult->mBaseAddress)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mIsDependent)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mLoadStatus)) { + return false; + } + + nsAutoString resolvedNtName; + if (!ReadParam(aReader, &resolvedNtName)) { + return false; + } + + // NB: While bad data integrity might for some reason result in a null + // mModule, we do not fail the deserialization; this is a data error, + // rather than an IPC error. The error is detected and dealt with in + // telemetry. + aResult->mModule = aModulesMap.Get(resolvedNtName); + + return true; + } +}; + +template <> +struct ParamTraits<mozilla::ModulesMapResult> { + typedef mozilla::ModulesMapResult paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mModules); + aWriter->WriteUInt32(aParam.mTrustTestFailures); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + if (!ReadParam(aReader, &aResult->mModules)) { + return false; + } + + if (!aReader->ReadUInt32(&aResult->mTrustTestFailures)) { + return false; + } + + return true; + } +}; + +} // namespace IPC + +#else // defined(XP_WIN) + +namespace mozilla { + +// For compiling IPDL on non-Windows platforms +using UntrustedModulesData = uint32_t; +using ModulePaths = uint32_t; +using ModulesMapResult = uint32_t; + +} // namespace mozilla + +#endif // defined(XP_WIN) + +#endif // mozilla_UntrustedModulesData_h diff --git a/toolkit/xre/dllservices/UntrustedModulesProcessor.cpp b/toolkit/xre/dllservices/UntrustedModulesProcessor.cpp new file mode 100644 index 0000000000..0bab977e41 --- /dev/null +++ b/toolkit/xre/dllservices/UntrustedModulesProcessor.cpp @@ -0,0 +1,1040 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "UntrustedModulesProcessor.h" + +#include <windows.h> + +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/Likely.h" +#include "mozilla/net/SocketProcessChild.h" +#include "mozilla/net/SocketProcessParent.h" +#include "mozilla/RDDChild.h" +#include "mozilla/RDDParent.h" +#include "mozilla/RDDProcessManager.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Services.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Unused.h" +#include "ModuleEvaluator.h" +#include "nsCOMPtr.h" +#include "nsHashKeys.h" +#include "nsIObserverService.h" +#include "nsTHashtable.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" +#include "private/prpriv.h" // For PR_GetThreadID + +static DWORD ToWin32ThreadId(nsIThread* aThread) { + if (!aThread) { + return 0UL; + } + + PRThread* prThread; + nsresult rv = aThread->GetPRThread(&prThread); + if (NS_FAILED(rv)) { + // Possible when a LazyInitThread's underlying nsThread is not present + return 0UL; + } + + return DWORD(::PR_GetThreadID(prThread)); +} + +namespace mozilla { + +class MOZ_RAII BackgroundPriorityRegion final { + public: + BackgroundPriorityRegion() + : mIsBackground( + ::SetThreadPriority(::GetCurrentThread(), THREAD_PRIORITY_IDLE)) {} + + ~BackgroundPriorityRegion() { + if (!mIsBackground) { + return; + } + + Clear(::GetCurrentThread()); + } + + static void Clear(nsIThread* aThread) { + DWORD tid = ToWin32ThreadId(aThread); + if (!tid) { + return; + } + + nsAutoHandle thread( + ::OpenThread(THREAD_SET_LIMITED_INFORMATION, FALSE, tid)); + if (!thread) { + return; + } + + Clear(thread); + } + + BackgroundPriorityRegion(const BackgroundPriorityRegion&) = delete; + BackgroundPriorityRegion(BackgroundPriorityRegion&&) = delete; + BackgroundPriorityRegion& operator=(const BackgroundPriorityRegion&) = delete; + BackgroundPriorityRegion& operator=(BackgroundPriorityRegion&&) = delete; + + private: + static void Clear(HANDLE aThread) { + DebugOnly<BOOL> ok = ::SetThreadPriority(aThread, THREAD_PRIORITY_NORMAL); + MOZ_ASSERT(ok); + } + + private: + const BOOL mIsBackground; +}; + +/* static */ +bool UntrustedModulesProcessor::IsSupportedProcessType() { + switch (XRE_GetProcessType()) { + case GeckoProcessType_Default: + case GeckoProcessType_Content: + case GeckoProcessType_Socket: + return Telemetry::CanRecordReleaseData(); + case GeckoProcessType_RDD: + // For RDD process, we check the telemetry settings in RDDChild::Init() + // running in the browser process because CanRecordReleaseData() always + // returns false here. + return true; + default: + return false; + } +} + +/* static */ +RefPtr<UntrustedModulesProcessor> UntrustedModulesProcessor::Create( + bool aIsReadyForBackgroundProcessing) { + if (!IsSupportedProcessType()) { + return nullptr; + } + + RefPtr<UntrustedModulesProcessor> result( + new UntrustedModulesProcessor(aIsReadyForBackgroundProcessing)); + return result.forget(); +} + +NS_IMPL_ISUPPORTS(UntrustedModulesProcessor, nsIObserver) + +static const uint32_t kThreadTimeoutMS = 120000; // 2 minutes + +UntrustedModulesProcessor::UntrustedModulesProcessor( + bool aIsReadyForBackgroundProcessing) + : mThread(new LazyIdleThread(kThreadTimeoutMS, "Untrusted Modules"_ns, + LazyIdleThread::ManualShutdown)), + mUnprocessedMutex( + "mozilla::UntrustedModulesProcessor::mUnprocessedMutex"), + mModuleCacheMutex( + "mozilla::UntrustedModulesProcessor::mModuleCacheMutex"), + mStatus(aIsReadyForBackgroundProcessing ? Status::Allowed + : Status::StartingUp) { + AddObservers(); +} + +void UntrustedModulesProcessor::AddObservers() { + nsCOMPtr<nsIObserverService> obsServ(services::GetObserverService()); + obsServ->AddObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID, false); + obsServ->AddObserver(this, "xpcom-shutdown-threads", false); + obsServ->AddObserver(this, "unblock-untrusted-modules-thread", false); + if (XRE_IsContentProcess()) { + obsServ->AddObserver(this, "content-child-will-shutdown", false); + } +} + +bool UntrustedModulesProcessor::IsReadyForBackgroundProcessing() const { + return mStatus == Status::Allowed; +} + +void UntrustedModulesProcessor::Disable() { + // Ensure that mThread cannot run at low priority anymore + BackgroundPriorityRegion::Clear(mThread); + + // No more background processing allowed beyond this point + if (mStatus.exchange(Status::ShuttingDown) != Status::Allowed) { + return; + } + + MutexAutoLock lock(mUnprocessedMutex); + CancelScheduledProcessing(lock); +} + +NS_IMETHODIMP UntrustedModulesProcessor::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID) || + !strcmp(aTopic, "content-child-will-shutdown")) { + Disable(); + return NS_OK; + } + + if (!strcmp(aTopic, "xpcom-shutdown-threads")) { + Disable(); + mThread->Shutdown(); + + RemoveObservers(); + + mThread = nullptr; + return NS_OK; + } + + if (!strcmp(aTopic, "unblock-untrusted-modules-thread")) { + nsCOMPtr<nsIObserverService> obs(services::GetObserverService()); + obs->RemoveObserver(this, "unblock-untrusted-modules-thread"); + + mStatus.compareExchange(Status::StartingUp, Status::Allowed); + + if (!IsReadyForBackgroundProcessing()) { + // If we're shutting down, stop here. + return NS_OK; + } + + if (XRE_IsParentProcess()) { + // Propagate notification to child processes + nsTArray<dom::ContentParent*> contentProcesses; + dom::ContentParent::GetAll(contentProcesses); + for (auto* proc : contentProcesses) { + Unused << proc->SendUnblockUntrustedModulesThread(); + } + if (auto* proc = net::SocketProcessParent::GetSingleton()) { + Unused << proc->SendUnblockUntrustedModulesThread(); + } + if (auto* rddMgr = RDDProcessManager::Get()) { + if (auto* proc = rddMgr->GetRDDChild()) { + Unused << proc->SendUnblockUntrustedModulesThread(); + } + } + } + + return NS_OK; + } + + MOZ_ASSERT_UNREACHABLE("Not reachable"); + + return NS_OK; +} + +void UntrustedModulesProcessor::RemoveObservers() { + nsCOMPtr<nsIObserverService> obsServ(services::GetObserverService()); + obsServ->RemoveObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID); + obsServ->RemoveObserver(this, "xpcom-shutdown-threads"); + obsServ->RemoveObserver(this, "unblock-untrusted-modules-thread"); + if (XRE_IsContentProcess()) { + obsServ->RemoveObserver(this, "content-child-will-shutdown"); + } +} + +void UntrustedModulesProcessor::ScheduleNonEmptyQueueProcessing( + const MutexAutoLock& aProofOfLock) { + // In case something tried to load a DLL during shutdown + if (!mThread) { + return; + } + +#if defined(ENABLE_TESTS) + // Don't bother scheduling background processing in short-lived xpcshell + // processes; it makes the test suites take too long. + if (MOZ_UNLIKELY(mozilla::EnvHasValue("XPCSHELL_TEST_PROFILE_DIR"))) { + return; + } +#endif // defined(ENABLE_TESTS) + + if (mIdleRunnable) { + return; + } + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + // Schedule a runnable to trigger background processing once the main thread + // has gone idle. We do it this way to ensure that we don't start doing a + // bunch of processing during periods of heavy main thread activity. + nsCOMPtr<nsIRunnable> idleRunnable(NewCancelableRunnableMethod( + "UntrustedModulesProcessor::DispatchBackgroundProcessing", this, + &UntrustedModulesProcessor::DispatchBackgroundProcessing)); + + if (NS_FAILED(NS_DispatchToMainThreadQueue(do_AddRef(idleRunnable), + EventQueuePriority::Idle))) { + return; + } + + mIdleRunnable = std::move(idleRunnable); +} + +void UntrustedModulesProcessor::CancelScheduledProcessing( + const MutexAutoLock& aProofOfLock) { + if (!mIdleRunnable) { + return; + } + + nsCOMPtr<nsICancelableRunnable> cancelable(do_QueryInterface(mIdleRunnable)); + if (cancelable) { + // Stop the pending idle runnable from doing anything + cancelable->Cancel(); + } + + mIdleRunnable = nullptr; +} + +void UntrustedModulesProcessor::DispatchBackgroundProcessing() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + nsCOMPtr<nsIRunnable> runnable(NewRunnableMethod( + "UntrustedModulesProcessor::BackgroundProcessModuleLoadQueue", this, + &UntrustedModulesProcessor::BackgroundProcessModuleLoadQueue)); + + mThread->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); +} + +void UntrustedModulesProcessor::Enqueue( + glue::EnhancedModuleLoadInfo&& aModLoadInfo) { + if (mStatus == Status::ShuttingDown) { + return; + } + + DWORD bgThreadId = ToWin32ThreadId(mThread); + if (aModLoadInfo.mNtLoadInfo.mThreadId == bgThreadId) { + // Exclude loads that were caused by our own background thread + return; + } + + MutexAutoLock lock(mUnprocessedMutex); + + mUnprocessedModuleLoads.insertBack( + new UnprocessedModuleLoadInfoContainer(std::move(aModLoadInfo))); + + ScheduleNonEmptyQueueProcessing(lock); +} + +void UntrustedModulesProcessor::Enqueue(ModuleLoadInfoVec&& aEvents) { + if (mStatus == Status::ShuttingDown) { + return; + } + + // We do not need to attempt to exclude our background thread in this case + // because |aEvents| was accumulated prior to |mThread|'s existence. + + MutexAutoLock lock(mUnprocessedMutex); + + for (auto& event : aEvents) { + mUnprocessedModuleLoads.insertBack( + new UnprocessedModuleLoadInfoContainer(std::move(event))); + } + + ScheduleNonEmptyQueueProcessing(lock); +} + +void UntrustedModulesProcessor::AssertRunningOnLazyIdleThread() { +#if defined(DEBUG) + PRThread* curThread; + PRThread* lazyIdleThread; + + MOZ_ASSERT(NS_SUCCEEDED(NS_GetCurrentThread()->GetPRThread(&curThread)) && + NS_SUCCEEDED(mThread->GetPRThread(&lazyIdleThread)) && + curThread == lazyIdleThread); +#endif // defined(DEBUG) +} + +RefPtr<UntrustedModulesPromise> UntrustedModulesProcessor::GetProcessedData() { + MOZ_ASSERT(NS_IsMainThread()); + + // Clear any background priority in case background processing is running. + BackgroundPriorityRegion::Clear(mThread); + + RefPtr<UntrustedModulesProcessor> self(this); + return InvokeAsync( + mThread->SerialEventTarget(), __func__, + [self = std::move(self)]() { return self->GetProcessedDataInternal(); }); +} + +RefPtr<ModulesTrustPromise> UntrustedModulesProcessor::GetModulesTrust( + ModulePaths&& aModPaths, bool aRunAtNormalPriority) { + MOZ_ASSERT(XRE_IsParentProcess() && NS_IsMainThread()); + + if (!IsReadyForBackgroundProcessing()) { + return ModulesTrustPromise::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + + RefPtr<UntrustedModulesProcessor> self(this); + auto run = [self = std::move(self), modPaths = std::move(aModPaths), + runNormal = aRunAtNormalPriority]() mutable { + return self->GetModulesTrustInternal(std::move(modPaths), runNormal); + }; + + if (aRunAtNormalPriority) { + // Clear any background priority in case background processing is running. + BackgroundPriorityRegion::Clear(mThread); + + return InvokeAsync(mThread->SerialEventTarget(), __func__, std::move(run)); + } + + RefPtr<ModulesTrustPromise::Private> p( + new ModulesTrustPromise::Private(__func__)); + nsCOMPtr<nsISerialEventTarget> evtTarget(mThread->SerialEventTarget()); + const char* source = __func__; + + auto runWrap = [evtTarget = std::move(evtTarget), p, source, + run = std::move(run)]() mutable -> void { + InvokeAsync(evtTarget, source, std::move(run))->ChainTo(p.forget(), source); + }; + + nsCOMPtr<nsIRunnable> idleRunnable( + NS_NewRunnableFunction(source, std::move(runWrap))); + + nsresult rv = NS_DispatchToMainThreadQueue(idleRunnable.forget(), + EventQueuePriority::Idle); + if (NS_FAILED(rv)) { + p->Reject(rv, source); + } + + return p; +} + +RefPtr<UntrustedModulesPromise> +UntrustedModulesProcessor::GetProcessedDataInternal() { + AssertRunningOnLazyIdleThread(); + if (!XRE_IsParentProcess()) { + return GetProcessedDataInternalChildProcess(); + } + + ProcessModuleLoadQueue(); + + return GetAllProcessedData(__func__); +} + +RefPtr<UntrustedModulesPromise> UntrustedModulesProcessor::GetAllProcessedData( + const char* aSource) { + AssertRunningOnLazyIdleThread(); + + UntrustedModulesData result; + + if (!mProcessedModuleLoads) { + return UntrustedModulesPromise::CreateAndResolve(Nothing(), aSource); + } + + result.Swap(mProcessedModuleLoads); + + result.mElapsed = TimeStamp::Now() - TimeStamp::ProcessCreation(); + + return UntrustedModulesPromise::CreateAndResolve( + Some(UntrustedModulesData(std::move(result))), aSource); +} + +RefPtr<UntrustedModulesPromise> +UntrustedModulesProcessor::GetProcessedDataInternalChildProcess() { + AssertRunningOnLazyIdleThread(); + MOZ_ASSERT(!XRE_IsParentProcess()); + + RefPtr<GetModulesTrustPromise> whenProcessed( + ProcessModuleLoadQueueChildProcess(Priority::Default)); + + RefPtr<UntrustedModulesProcessor> self(this); + RefPtr<UntrustedModulesPromise::Private> p( + new UntrustedModulesPromise::Private(__func__)); + nsCOMPtr<nsISerialEventTarget> evtTarget(mThread->SerialEventTarget()); + + const char* source = __func__; + auto completionRoutine = [evtTarget = std::move(evtTarget), p, + self = std::move(self), source, + whenProcessed = std::move(whenProcessed)]() { + MOZ_ASSERT(NS_IsMainThread()); + if (!self->IsReadyForBackgroundProcessing()) { + // We can't do any more work, just reject all the things + whenProcessed->Then( + GetMainThreadSerialEventTarget(), source, + [p, source](Maybe<ModulesMapResultWithLoads>&& aResult) { + p->Reject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN, source); + }, + [p, source](nsresult aRv) { p->Reject(aRv, source); }); + return; + } + + whenProcessed->Then( + evtTarget, source, + [p, self = std::move(self), + source](Maybe<ModulesMapResultWithLoads>&& aResult) mutable { + if (aResult.isSome()) { + self->CompleteProcessing(std::move(aResult.ref())); + } + self->GetAllProcessedData(source)->ChainTo(p.forget(), source); + }, + [p, source](nsresult aRv) { p->Reject(aRv, source); }); + }; + + // We always send |completionRoutine| on a trip through the main thread + // due to some subtlety with |mThread| being a LazyIdleThread: we can only + // Dispatch or Then to |mThread| from its creating thread, which is the + // main thread. Hopefully we can get rid of this in the future and just + // invoke whenProcessed->Then() directly. + nsresult rv = NS_DispatchToMainThread( + NS_NewRunnableFunction(__func__, std::move(completionRoutine))); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (NS_FAILED(rv)) { + p->Reject(rv, __func__); + } + + return p; +} + +void UntrustedModulesProcessor::BackgroundProcessModuleLoadQueue() { + if (!IsReadyForBackgroundProcessing()) { + return; + } + + BackgroundPriorityRegion bgRgn; + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + ProcessModuleLoadQueue(); +} + +RefPtr<ModuleRecord> UntrustedModulesProcessor::GetOrAddModuleRecord( + const ModuleEvaluator& aModEval, const nsAString& aResolvedNtPath) { + MOZ_ASSERT(XRE_IsParentProcess()); + + MutexAutoLock lock(mModuleCacheMutex); + return mGlobalModuleCache.WithEntryHandle( + aResolvedNtPath, [&](auto&& addPtr) -> RefPtr<ModuleRecord> { + if (addPtr) { + return addPtr.Data(); + } + + RefPtr<ModuleRecord> newMod(new ModuleRecord(aResolvedNtPath)); + if (!(*newMod)) { + return nullptr; + } + + Maybe<ModuleTrustFlags> maybeTrust = aModEval.GetTrust(*newMod); + if (maybeTrust.isNothing()) { + return nullptr; + } + + newMod->mTrustFlags = maybeTrust.value(); + + return addPtr.Insert(std::move(newMod)); + }); +} + +RefPtr<ModuleRecord> UntrustedModulesProcessor::GetModuleRecord( + const ModulesMap& aModules, + const glue::EnhancedModuleLoadInfo& aModuleLoadInfo) { + MOZ_ASSERT(!XRE_IsParentProcess()); + + return aModules.Get(aModuleLoadInfo.mNtLoadInfo.mSectionName.AsString()); +} + +void UntrustedModulesProcessor::BackgroundProcessModuleLoadQueueChildProcess() { + RefPtr<GetModulesTrustPromise> whenProcessed( + ProcessModuleLoadQueueChildProcess(Priority::Background)); + + RefPtr<UntrustedModulesProcessor> self(this); + nsCOMPtr<nsISerialEventTarget> evtTarget(mThread->SerialEventTarget()); + + const char* source = __func__; + auto completionRoutine = [evtTarget = std::move(evtTarget), + self = std::move(self), source, + whenProcessed = std::move(whenProcessed)]() { + MOZ_ASSERT(NS_IsMainThread()); + if (!self->IsReadyForBackgroundProcessing()) { + // We can't do any more work, just no-op + whenProcessed->Then( + GetMainThreadSerialEventTarget(), source, + [](Maybe<ModulesMapResultWithLoads>&& aResult) {}, + [](nsresult aRv) {}); + return; + } + + whenProcessed->Then( + evtTarget, source, + [self = std::move(self)](Maybe<ModulesMapResultWithLoads>&& aResult) { + if (aResult.isNothing() || !self->IsReadyForBackgroundProcessing()) { + // Nothing to do + return; + } + + BackgroundPriorityRegion bgRgn; + self->CompleteProcessing(std::move(aResult.ref())); + }, + [](nsresult aRv) {}); + }; + + // We always send |completionRoutine| on a trip through the main thread + // due to some subtlety with |mThread| being a LazyIdleThread: we can only + // Dispatch or Then to |mThread| from its creating thread, which is the + // main thread. Hopefully we can get rid of this in the future and just + // invoke whenProcessed->Then() directly. + DebugOnly<nsresult> rv = NS_DispatchToMainThread( + NS_NewRunnableFunction(__func__, std::move(completionRoutine))); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +UnprocessedModuleLoads UntrustedModulesProcessor::ExtractLoadingEventsToProcess( + size_t aMaxLength) { + UnprocessedModuleLoads loadsToProcess; + + MutexAutoLock lock(mUnprocessedMutex); + CancelScheduledProcessing(lock); + + loadsToProcess.splice(0, mUnprocessedModuleLoads, 0, aMaxLength); + return loadsToProcess; +} + +// This function contains multiple IsReadyForBackgroundProcessing() checks so +// that we can quickly bail out at the first sign of shutdown. This may be +// important when the current thread is running under background priority. +void UntrustedModulesProcessor::ProcessModuleLoadQueue() { + AssertRunningOnLazyIdleThread(); + if (!XRE_IsParentProcess()) { + BackgroundProcessModuleLoadQueueChildProcess(); + return; + } + + UnprocessedModuleLoads loadsToProcess = + ExtractLoadingEventsToProcess(UntrustedModulesData::kMaxEvents); + if (!IsReadyForBackgroundProcessing() || loadsToProcess.isEmpty()) { + return; + } + + ModuleEvaluator modEval; + MOZ_ASSERT(!!modEval); + if (!modEval) { + return; + } + + Telemetry::BatchProcessedStackGenerator stackProcessor; + Maybe<double> maybeXulLoadDuration; + Vector<Telemetry::ProcessedStack> processedStacks; + UntrustedModuleLoadingEvents processedEvents; + uint32_t sanitizationFailures = 0; + uint32_t trustTestFailures = 0; + + for (UnprocessedModuleLoadInfoContainer* container : loadsToProcess) { + glue::EnhancedModuleLoadInfo& entry = container->mInfo; + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + RefPtr<ModuleRecord> module(GetOrAddModuleRecord( + modEval, entry.mNtLoadInfo.mSectionName.AsString())); + if (!module) { + // We failed to obtain trust information about the module. + // Don't include test failures in the ping to avoid flooding it. + ++trustTestFailures; + continue; + } + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + glue::EnhancedModuleLoadInfo::BacktraceType backtrace = + std::move(entry.mNtLoadInfo.mBacktrace); + ProcessedModuleLoadEvent event(std::move(entry), std::move(module)); + + if (!event) { + // We don't have a sanitized DLL path, so we cannot include this event + // for privacy reasons. + ++sanitizationFailures; + continue; + } + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + if (event.IsTrusted()) { + if (event.IsXULLoad()) { + maybeXulLoadDuration = event.mLoadDurationMS; + } + + // Trusted modules are not included in the ping + continue; + } + + mProcessedModuleLoads.mModules.LookupOrInsert( + event.mModule->mResolvedNtName, event.mModule); + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + Telemetry::ProcessedStack processedStack = + stackProcessor.GetStackAndModules(backtrace); + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + Unused << processedStacks.emplaceBack(std::move(processedStack)); + processedEvents.insertBack( + new ProcessedModuleLoadEventContainer(std::move(event))); + } + + if (processedStacks.empty() && processedEvents.isEmpty() && + !sanitizationFailures && !trustTestFailures) { + // Nothing to save + return; + } + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + // Modules have been added to mProcessedModuleLoads.mModules + // in the loop above. Passing an empty ModulesMap to AddNewLoads. + mProcessedModuleLoads.AddNewLoads(ModulesMap{}, std::move(processedEvents), + std::move(processedStacks)); + if (maybeXulLoadDuration) { + MOZ_ASSERT(!mProcessedModuleLoads.mXULLoadDurationMS); + mProcessedModuleLoads.mXULLoadDurationMS = maybeXulLoadDuration; + } + + mProcessedModuleLoads.mSanitizationFailures += sanitizationFailures; + mProcessedModuleLoads.mTrustTestFailures += trustTestFailures; +} + +template <typename ActorT> +static RefPtr<GetModulesTrustIpcPromise> SendGetModulesTrust( + ActorT* aActor, ModulePaths&& aModPaths, bool aRunAtNormalPriority) { + MOZ_ASSERT(NS_IsMainThread()); + return aActor->SendGetModulesTrust(std::move(aModPaths), + aRunAtNormalPriority); +} + +RefPtr<GetModulesTrustIpcPromise> +UntrustedModulesProcessor::SendGetModulesTrust(ModulePaths&& aModules, + Priority aPriority) { + MOZ_ASSERT(NS_IsMainThread()); + bool runNormal = aPriority == Priority::Default; + + switch (XRE_GetProcessType()) { + case GeckoProcessType_Content: { + return ::mozilla::SendGetModulesTrust(dom::ContentChild::GetSingleton(), + std::move(aModules), runNormal); + } + case GeckoProcessType_RDD: { + return ::mozilla::SendGetModulesTrust(RDDParent::GetSingleton(), + std::move(aModules), runNormal); + } + case GeckoProcessType_Socket: { + return ::mozilla::SendGetModulesTrust( + net::SocketProcessChild::GetSingleton(), std::move(aModules), + runNormal); + } + default: { + MOZ_ASSERT_UNREACHABLE("Unsupported process type"); + return GetModulesTrustIpcPromise::CreateAndReject( + ipc::ResponseRejectReason::SendError, __func__); + } + } +} + +/** + * This method works very similarly to ProcessModuleLoadQueue, with the + * exception that a sandboxed child process does not have sufficient rights to + * be able to evaluate a module's trustworthiness. Instead, we accumulate the + * resolved paths for all of the modules in this batch and send them to the + * parent to determine trustworthiness. + * + * The parent process returns a list of untrusted modules and invokes + * CompleteProcessing to handle the remainder of the process. + * + * By doing it this way, we minimize the amount of data that needs to be sent + * over IPC and avoid the need to process every load's metadata only + * to throw most of it away (since most modules will be trusted). + */ +RefPtr<UntrustedModulesProcessor::GetModulesTrustPromise> +UntrustedModulesProcessor::ProcessModuleLoadQueueChildProcess( + UntrustedModulesProcessor::Priority aPriority) { + AssertRunningOnLazyIdleThread(); + MOZ_ASSERT(!XRE_IsParentProcess()); + + UnprocessedModuleLoads loadsToProcess = + ExtractLoadingEventsToProcess(UntrustedModulesData::kMaxEvents); + if (loadsToProcess.isEmpty()) { + // Nothing to process + return GetModulesTrustPromise::CreateAndResolve(Nothing(), __func__); + } + + if (!IsReadyForBackgroundProcessing()) { + return GetModulesTrustPromise::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + + nsTHashtable<nsStringCaseInsensitiveHashKey> moduleNtPathSet; + + // Build a set of modules to be processed by the parent + for (UnprocessedModuleLoadInfoContainer* container : loadsToProcess) { + glue::EnhancedModuleLoadInfo& entry = container->mInfo; + + if (!IsReadyForBackgroundProcessing()) { + return GetModulesTrustPromise::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + + moduleNtPathSet.PutEntry(entry.mNtLoadInfo.mSectionName.AsString()); + } + + if (!IsReadyForBackgroundProcessing()) { + return GetModulesTrustPromise::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + + MOZ_ASSERT(!moduleNtPathSet.IsEmpty()); + if (moduleNtPathSet.IsEmpty()) { + // Nothing to process + return GetModulesTrustPromise::CreateAndResolve(Nothing(), __func__); + } + + ModulePaths moduleNtPaths(std::move(moduleNtPathSet)); + + if (!IsReadyForBackgroundProcessing()) { + return GetModulesTrustPromise::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + + RefPtr<UntrustedModulesProcessor> self(this); + + auto invoker = [self = std::move(self), + moduleNtPaths = std::move(moduleNtPaths), + priority = aPriority]() mutable { + return self->SendGetModulesTrust(std::move(moduleNtPaths), priority); + }; + + RefPtr<GetModulesTrustPromise::Private> p( + new GetModulesTrustPromise::Private(__func__)); + + if (!IsReadyForBackgroundProcessing()) { + p->Reject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + return p; + } + + // Send the IPC request via the main thread + InvokeAsync(GetMainThreadSerialEventTarget(), __func__, std::move(invoker)) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [p, loads = std::move(loadsToProcess)]( + Maybe<ModulesMapResult>&& aResult) mutable { + ModulesMapResultWithLoads result(std::move(aResult), + std::move(loads)); + p->Resolve(Some(ModulesMapResultWithLoads(std::move(result))), + __func__); + }, + [p](ipc::ResponseRejectReason aReason) { + p->Reject(NS_ERROR_FAILURE, __func__); + }); + + return p; +} + +void UntrustedModulesProcessor::CompleteProcessing( + UntrustedModulesProcessor::ModulesMapResultWithLoads&& aModulesAndLoads) { + MOZ_ASSERT(!XRE_IsParentProcess()); + AssertRunningOnLazyIdleThread(); + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + if (aModulesAndLoads.mModMapResult.isNothing()) { + // No untrusted modules in this batch, nothing to save. + return; + } + + // This map only contains information about modules deemed to be untrusted, + // plus xul.dll. Any module referenced by load requests that is *not* in the + // map is deemed to be trusted. + ModulesMap& modules = aModulesAndLoads.mModMapResult.ref().mModules; + const uint32_t& trustTestFailures = + aModulesAndLoads.mModMapResult.ref().mTrustTestFailures; + UnprocessedModuleLoads& loads = aModulesAndLoads.mLoads; + + if (modules.IsEmpty() && !trustTestFailures) { + // No data, nothing to save. + return; + } + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + Telemetry::BatchProcessedStackGenerator stackProcessor; + + Maybe<double> maybeXulLoadDuration; + Vector<Telemetry::ProcessedStack> processedStacks; + UntrustedModuleLoadingEvents processedEvents; + uint32_t sanitizationFailures = 0; + + if (!modules.IsEmpty()) { + for (UnprocessedModuleLoadInfoContainer* container : loads) { + glue::EnhancedModuleLoadInfo& item = container->mInfo; + if (!IsReadyForBackgroundProcessing()) { + return; + } + + RefPtr<ModuleRecord> module(GetModuleRecord(modules, item)); + if (!module) { + // If module is null then |item| is trusted + continue; + } + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + glue::EnhancedModuleLoadInfo::BacktraceType backtrace = + std::move(item.mNtLoadInfo.mBacktrace); + ProcessedModuleLoadEvent event(std::move(item), std::move(module)); + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + if (!event) { + // We don't have a sanitized DLL path, so we cannot include this event + // for privacy reasons. + ++sanitizationFailures; + continue; + } + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + if (event.IsXULLoad()) { + maybeXulLoadDuration = event.mLoadDurationMS; + // We saved the XUL load duration, but it is still trusted, so we + // continue. + continue; + } + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + Telemetry::ProcessedStack processedStack = + stackProcessor.GetStackAndModules(backtrace); + + Unused << processedStacks.emplaceBack(std::move(processedStack)); + processedEvents.insertBack( + new ProcessedModuleLoadEventContainer(std::move(event))); + } + } + + if (processedStacks.empty() && processedEvents.isEmpty() && + !sanitizationFailures && !trustTestFailures) { + // Nothing to save + return; + } + + if (!IsReadyForBackgroundProcessing()) { + return; + } + + mProcessedModuleLoads.AddNewLoads(modules, std::move(processedEvents), + std::move(processedStacks)); + if (maybeXulLoadDuration) { + MOZ_ASSERT(!mProcessedModuleLoads.mXULLoadDurationMS); + mProcessedModuleLoads.mXULLoadDurationMS = maybeXulLoadDuration; + } + + mProcessedModuleLoads.mSanitizationFailures += sanitizationFailures; + mProcessedModuleLoads.mTrustTestFailures += trustTestFailures; +} + +// The thread priority of this job should match the priority that the child +// process is running with, as specified by |aRunAtNormalPriority|. +RefPtr<ModulesTrustPromise> UntrustedModulesProcessor::GetModulesTrustInternal( + ModulePaths&& aModPaths, bool aRunAtNormalPriority) { + MOZ_ASSERT(XRE_IsParentProcess()); + AssertRunningOnLazyIdleThread(); + + if (!IsReadyForBackgroundProcessing()) { + return ModulesTrustPromise::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + + if (aRunAtNormalPriority) { + return GetModulesTrustInternal(std::move(aModPaths)); + } + + BackgroundPriorityRegion bgRgn; + return GetModulesTrustInternal(std::move(aModPaths)); +} + +// For each module in |aModPaths|, evaluate its trustworthiness and only send +// ModuleRecords for untrusted modules back to the child process. We also save +// XUL's ModuleRecord so that the child process may report XUL's load time. +RefPtr<ModulesTrustPromise> UntrustedModulesProcessor::GetModulesTrustInternal( + ModulePaths&& aModPaths) { + MOZ_ASSERT(XRE_IsParentProcess()); + AssertRunningOnLazyIdleThread(); + + ModulesMapResult result; + + ModulesMap& modMap = result.mModules; + uint32_t& trustTestFailures = result.mTrustTestFailures; + + ModuleEvaluator modEval; + MOZ_ASSERT(!!modEval); + if (!modEval) { + return ModulesTrustPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + for (auto& resolvedNtPath : + aModPaths.mModuleNtPaths.as<ModulePaths::VecType>()) { + if (!IsReadyForBackgroundProcessing()) { + return ModulesTrustPromise::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + + MOZ_ASSERT(!resolvedNtPath.IsEmpty()); + if (resolvedNtPath.IsEmpty()) { + continue; + } + + RefPtr<ModuleRecord> module(GetOrAddModuleRecord(modEval, resolvedNtPath)); + if (!module) { + // We failed to obtain trust information. + ++trustTestFailures; + continue; + } + + if (!IsReadyForBackgroundProcessing()) { + return ModulesTrustPromise::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + + if (module->IsTrusted() && !module->IsXUL()) { + // If the module is trusted we exclude it from results, unless it's XUL. + // (We save XUL so that the child process may report XUL's load time) + continue; + } + + if (!IsReadyForBackgroundProcessing()) { + return ModulesTrustPromise::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + + modMap.InsertOrUpdate(resolvedNtPath, std::move(module)); + } + + return ModulesTrustPromise::CreateAndResolve(std::move(result), __func__); +} + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/UntrustedModulesProcessor.h b/toolkit/xre/dllservices/UntrustedModulesProcessor.h new file mode 100644 index 0000000000..1da5a25b2c --- /dev/null +++ b/toolkit/xre/dllservices/UntrustedModulesProcessor.h @@ -0,0 +1,176 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_UntrustedModulesProcessor_h +#define mozilla_UntrustedModulesProcessor_h + +#include "mozilla/Atomics.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/glue/WindowsDllServices.h" +#include "mozilla/LazyIdleThread.h" +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UntrustedModulesData.h" +#include "mozilla/Vector.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "nsCOMPtr.h" +#include "nsIObserver.h" +#include "nsIRunnable.h" +#include "nsISupportsImpl.h" +#include "nsString.h" + +namespace mozilla { + +class ModuleEvaluator; + +using UntrustedModulesPromise = + MozPromise<Maybe<UntrustedModulesData>, nsresult, true>; + +using ModulesTrustPromise = MozPromise<ModulesMapResult, nsresult, true>; + +using GetModulesTrustIpcPromise = + MozPromise<Maybe<ModulesMapResult>, ipc::ResponseRejectReason, true>; + +struct UnprocessedModuleLoadInfoContainer final + : public LinkedListElement<UnprocessedModuleLoadInfoContainer> { + glue::EnhancedModuleLoadInfo mInfo; + + template <typename T> + explicit UnprocessedModuleLoadInfoContainer(T&& aInfo) + : mInfo(std::move(aInfo)) {} + + UnprocessedModuleLoadInfoContainer( + const UnprocessedModuleLoadInfoContainer&) = delete; + UnprocessedModuleLoadInfoContainer& operator=( + const UnprocessedModuleLoadInfoContainer&) = delete; +}; +using UnprocessedModuleLoads = + AutoCleanLinkedList<UnprocessedModuleLoadInfoContainer>; + +class UntrustedModulesProcessor final : public nsIObserver { + public: + static RefPtr<UntrustedModulesProcessor> Create( + bool aIsReadyForBackgroundProcessing); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + + // Called to check if the parent process is ready when a child process + // is spanwed + bool IsReadyForBackgroundProcessing() const; + + // Called by DLL Services to explicitly begin shutting down + void Disable(); + + // Called by DLL Services to submit module load data to the processor + void Enqueue(glue::EnhancedModuleLoadInfo&& aModLoadInfo); + void Enqueue(ModuleLoadInfoVec&& aEvents); + + // Called by telemetry to retrieve the processed data + RefPtr<UntrustedModulesPromise> GetProcessedData(); + + // Called by IPC actors in the parent process to evaluate module trust + // on behalf of child processes + RefPtr<ModulesTrustPromise> GetModulesTrust(ModulePaths&& aModPaths, + bool aRunAtNormalPriority); + + UntrustedModulesProcessor(const UntrustedModulesProcessor&) = delete; + UntrustedModulesProcessor(UntrustedModulesProcessor&&) = delete; + UntrustedModulesProcessor& operator=(const UntrustedModulesProcessor&) = + delete; + UntrustedModulesProcessor& operator=(UntrustedModulesProcessor&&) = delete; + + private: + ~UntrustedModulesProcessor() = default; + explicit UntrustedModulesProcessor(bool aIsReadyForBackgroundProcessing); + + static bool IsSupportedProcessType(); + + void AddObservers(); + void RemoveObservers(); + + void ScheduleNonEmptyQueueProcessing(const MutexAutoLock& aProofOfLock); + void CancelScheduledProcessing(const MutexAutoLock& aProofOfLock); + void DispatchBackgroundProcessing(); + + void BackgroundProcessModuleLoadQueue(); + void ProcessModuleLoadQueue(); + + // Extract the loading events from mUnprocessedModuleLoads to process and + // move to mProcessedModuleLoads. It's guaranteed that the total length of + // mProcessedModuleLoads will not exceed |aMaxLength|. + UnprocessedModuleLoads ExtractLoadingEventsToProcess(size_t aMaxLength); + + class ModulesMapResultWithLoads final { + public: + ModulesMapResultWithLoads(Maybe<ModulesMapResult>&& aModMapResult, + UnprocessedModuleLoads&& aLoads) + : mModMapResult(std::move(aModMapResult)), mLoads(std::move(aLoads)) {} + Maybe<ModulesMapResult> mModMapResult; + UnprocessedModuleLoads mLoads; + }; + + using GetModulesTrustPromise = + MozPromise<Maybe<ModulesMapResultWithLoads>, nsresult, true>; + + enum class Priority { Default, Background }; + + RefPtr<GetModulesTrustPromise> ProcessModuleLoadQueueChildProcess( + Priority aPriority); + void BackgroundProcessModuleLoadQueueChildProcess(); + + void AssertRunningOnLazyIdleThread(); + + RefPtr<UntrustedModulesPromise> GetProcessedDataInternal(); + RefPtr<UntrustedModulesPromise> GetProcessedDataInternalChildProcess(); + + RefPtr<ModulesTrustPromise> GetModulesTrustInternal( + ModulePaths&& aModPaths, bool aRunAtNormalPriority); + RefPtr<ModulesTrustPromise> GetModulesTrustInternal(ModulePaths&& aModPaths); + + // This function is only called by the parent process + RefPtr<ModuleRecord> GetOrAddModuleRecord(const ModuleEvaluator& aModEval, + const nsAString& aResolvedNtPath); + + // Only called by child processes + RefPtr<ModuleRecord> GetModuleRecord( + const ModulesMap& aModules, + const glue::EnhancedModuleLoadInfo& aModuleLoadInfo); + + RefPtr<GetModulesTrustIpcPromise> SendGetModulesTrust(ModulePaths&& aModules, + Priority aPriority); + + void CompleteProcessing(ModulesMapResultWithLoads&& aModulesAndLoads); + RefPtr<UntrustedModulesPromise> GetAllProcessedData(const char* aSource); + + private: + RefPtr<LazyIdleThread> mThread; + + Mutex mUnprocessedMutex MOZ_UNANNOTATED; + Mutex mModuleCacheMutex MOZ_UNANNOTATED; + + // The members in this group are protected by mUnprocessedMutex + UnprocessedModuleLoads mUnprocessedModuleLoads; + nsCOMPtr<nsIRunnable> mIdleRunnable; + + // This member must only be touched on mThread + UntrustedModulesData mProcessedModuleLoads; + + enum class Status { StartingUp, Allowed, ShuttingDown }; + + // This member may be touched by any thread + Atomic<Status> mStatus; + + // Cache all module records, including ones trusted and ones loaded in + // child processes, in the browser process to avoid evaluating the same + // module multiple times + ModulesMap mGlobalModuleCache; +}; + +} // namespace mozilla + +#endif // mozilla_UntrustedModulesProcessor_h diff --git a/toolkit/xre/dllservices/WinDllServices.cpp b/toolkit/xre/dllservices/WinDllServices.cpp new file mode 100644 index 0000000000..b98a15252b --- /dev/null +++ b/toolkit/xre/dllservices/WinDllServices.cpp @@ -0,0 +1,142 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "mozilla/WinDllServices.h" + +#include <windows.h> +#include <psapi.h> + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/Services.h" +#include "mozilla/StaticLocalPtr.h" +#include "mozilla/UntrustedModulesProcessor.h" +#include "nsCOMPtr.h" +#include "nsIObserverService.h" +#include "nsString.h" +#include "nsXULAppAPI.h" + +namespace mozilla { + +const char* DllServices::kTopicDllLoadedMainThread = "dll-loaded-main-thread"; +const char* DllServices::kTopicDllLoadedNonMainThread = + "dll-loaded-non-main-thread"; + +/* static */ +DllServices* DllServices::Get() { + static StaticLocalRefPtr<DllServices> sInstance( + []() -> already_AddRefed<DllServices> { + RefPtr<DllServices> dllSvc(new DllServices()); + // Full DLL services require XPCOM, which GMP doesn't have + if (XRE_IsGMPluginProcess()) { + dllSvc->EnableBasic(); + } else { + dllSvc->EnableFull(); + } + + auto setClearOnShutdown = [ptr = &sInstance]() -> void { + ClearOnShutdown(ptr); + }; + + if (NS_IsMainThread()) { + setClearOnShutdown(); + return dllSvc.forget(); + } + + SchedulerGroup::Dispatch( + TaskCategory::Other, + NS_NewRunnableFunction("mozilla::DllServices::Get", + std::move(setClearOnShutdown))); + + return dllSvc.forget(); + }()); + + return sInstance; +} + +DllServices::~DllServices() { DisableFull(); } + +void DllServices::StartUntrustedModulesProcessor(bool aIsStartingUp) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mUntrustedModulesProcessor); + mUntrustedModulesProcessor = UntrustedModulesProcessor::Create(aIsStartingUp); +} + +bool DllServices::IsReadyForBackgroundProcessing() const { + return mUntrustedModulesProcessor && + mUntrustedModulesProcessor->IsReadyForBackgroundProcessing(); +} + +RefPtr<UntrustedModulesPromise> DllServices::GetUntrustedModulesData() { + if (!mUntrustedModulesProcessor) { + return UntrustedModulesPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, + __func__); + } + + return mUntrustedModulesProcessor->GetProcessedData(); +} + +void DllServices::DisableFull() { + if (XRE_IsGMPluginProcess()) { + return; + } + + if (mUntrustedModulesProcessor) { + mUntrustedModulesProcessor->Disable(); + } + + glue::DllServices::DisableFull(); +} + +RefPtr<ModulesTrustPromise> DllServices::GetModulesTrust( + ModulePaths&& aModPaths, bool aRunAtNormalPriority) { + if (!mUntrustedModulesProcessor) { + return ModulesTrustPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, + __func__); + } + + return mUntrustedModulesProcessor->GetModulesTrust(std::move(aModPaths), + aRunAtNormalPriority); +} + +void DllServices::NotifyDllLoad(glue::EnhancedModuleLoadInfo&& aModLoadInfo) { + MOZ_ASSERT(NS_IsMainThread()); + + const char* topic; + + if (aModLoadInfo.mNtLoadInfo.mThreadId == ::GetCurrentThreadId()) { + topic = kTopicDllLoadedMainThread; + } else { + topic = kTopicDllLoadedNonMainThread; + } + + // We save the path to a nsAutoString because once we have submitted + // aModLoadInfo for processing there is no guarantee that the original + // buffer will continue to be valid. + nsAutoString dllFilePath(aModLoadInfo.GetSectionName()); + + if (mUntrustedModulesProcessor) { + mUntrustedModulesProcessor->Enqueue(std::move(aModLoadInfo)); + } + + nsCOMPtr<nsIObserverService> obsServ(mozilla::services::GetObserverService()); + if (!obsServ) { + return; + } + + obsServ->NotifyObservers(nullptr, topic, dllFilePath.get()); +} + +void DllServices::NotifyModuleLoadBacklog(ModuleLoadInfoVec&& aEvents) { + MOZ_ASSERT(NS_IsMainThread()); + if (!mUntrustedModulesProcessor) { + return; + } + + mUntrustedModulesProcessor->Enqueue(std::move(aEvents)); +} + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/WinDllServices.h b/toolkit/xre/dllservices/WinDllServices.h new file mode 100644 index 0000000000..a5d4f9b363 --- /dev/null +++ b/toolkit/xre/dllservices/WinDllServices.h @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_WinDllServices_h +#define mozilla_WinDllServices_h + +#include "mozilla/glue/WindowsDllServices.h" +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" +#include "mozilla/RefPtr.h" + +namespace mozilla { + +class UntrustedModulesData; +class UntrustedModulesProcessor; + +using UntrustedModulesPromise = + MozPromise<Maybe<UntrustedModulesData>, nsresult, true>; + +struct ModulePaths; +class ModulesMapResult; + +using ModulesTrustPromise = MozPromise<ModulesMapResult, nsresult, true>; + +class DllServices final : public glue::DllServices { + public: + static DllServices* Get(); + + virtual void DisableFull() override; + + static const char* kTopicDllLoadedMainThread; + static const char* kTopicDllLoadedNonMainThread; + + void StartUntrustedModulesProcessor(bool aIsReadyForBackgroundProcessing); + bool IsReadyForBackgroundProcessing() const; + + RefPtr<UntrustedModulesPromise> GetUntrustedModulesData(); + + RefPtr<ModulesTrustPromise> GetModulesTrust(ModulePaths&& aModPaths, + bool aRunAtNormalPriority); + + private: + DllServices() = default; + ~DllServices(); + + void NotifyDllLoad(glue::EnhancedModuleLoadInfo&& aModLoadInfo) override; + void NotifyModuleLoadBacklog(ModuleLoadInfoVec&& aEvents) override; + + RefPtr<UntrustedModulesProcessor> mUntrustedModulesProcessor; +}; + +} // namespace mozilla + +#endif // mozilla_WinDllServices_h diff --git a/toolkit/xre/dllservices/moz.build b/toolkit/xre/dllservices/moz.build new file mode 100644 index 0000000000..bb23f5fb53 --- /dev/null +++ b/toolkit/xre/dllservices/moz.build @@ -0,0 +1,45 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DLL Services") + +Library("dllservices") + +FINAL_LIBRARY = "xul" + +EXPORTS.mozilla += [ + "DynamicBlocklist.h", + "ModuleVersionInfo.h", + "UntrustedModulesData.h", + "UntrustedModulesProcessor.h", + "WinDllServices.h", +] + +DIRS += [ + "mozglue", +] + +UNIFIED_SOURCES += [ + "ModuleEvaluator.cpp", + "ModuleVersionInfo.cpp", + "UntrustedModulesData.cpp", + "UntrustedModulesProcessor.cpp", + "WinDllServices.cpp", +] + +if CONFIG["MOZ_LAUNCHER_PROCESS"]: + UNIFIED_SOURCES += [ + "DynamicBlocklistWriter.cpp", + ] + +TEST_DIRS += [ + "tests", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +REQUIRES_UNIFIED_BUILD = True diff --git a/toolkit/xre/dllservices/mozglue/Authenticode.cpp b/toolkit/xre/dllservices/mozglue/Authenticode.cpp new file mode 100644 index 0000000000..ee702b2ac6 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/Authenticode.cpp @@ -0,0 +1,439 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// We need Windows 8 functions and structures to be able to verify SHA-256. +#if defined(_WIN32_WINNT) +# undef _WIN32_WINNT +# define _WIN32_WINNT _WIN32_WINNT_WIN8 +#endif // defined(_WIN32_WINNT) +#if defined(NTDDI_VERSION) +# undef NTDDI_VERSION +# define NTDDI_VERSION NTDDI_WIN8 +#endif // defined(NTDDI_VERSION) + +#include "Authenticode.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/DynamicallyLinkedFunctionPtr.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WindowsVersion.h" +#include "nsWindowsHelpers.h" + +#include <windows.h> +#include <softpub.h> +#include <wincrypt.h> +#include <wintrust.h> +#include <mscat.h> + +#include <string.h> + +namespace { + +struct CertStoreDeleter { + typedef HCERTSTORE pointer; + void operator()(pointer aStore) { ::CertCloseStore(aStore, 0); } +}; + +struct CryptMsgDeleter { + typedef HCRYPTMSG pointer; + void operator()(pointer aMsg) { ::CryptMsgClose(aMsg); } +}; + +struct CertContextDeleter { + void operator()(PCCERT_CONTEXT aCertContext) { + ::CertFreeCertificateContext(aCertContext); + } +}; + +struct CATAdminContextDeleter { + typedef HCATADMIN pointer; + void operator()(pointer aCtx) { + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::CryptCATAdminReleaseContext)> + pCryptCATAdminReleaseContext(L"wintrust.dll", + "CryptCATAdminReleaseContext"); + + MOZ_ASSERT(!!pCryptCATAdminReleaseContext); + if (!pCryptCATAdminReleaseContext) { + return; + } + + pCryptCATAdminReleaseContext(aCtx, 0); + } +}; + +typedef mozilla::UniquePtr<HCERTSTORE, CertStoreDeleter> CertStoreUniquePtr; +typedef mozilla::UniquePtr<HCRYPTMSG, CryptMsgDeleter> CryptMsgUniquePtr; +typedef mozilla::UniquePtr<const CERT_CONTEXT, CertContextDeleter> + CertContextUniquePtr; +typedef mozilla::UniquePtr<HCATADMIN, CATAdminContextDeleter> + CATAdminContextUniquePtr; + +static const DWORD kEncodingTypes = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING; + +class SignedBinary final { + public: + SignedBinary(const wchar_t* aFilePath, mozilla::AuthenticodeFlags aFlags); + + explicit operator bool() const { return mCertStore && mCryptMsg && mCertCtx; } + + mozilla::UniquePtr<wchar_t[]> GetOrgName(); + + SignedBinary(const SignedBinary&) = delete; + SignedBinary(SignedBinary&&) = delete; + SignedBinary& operator=(const SignedBinary&) = delete; + SignedBinary& operator=(SignedBinary&&) = delete; + + private: + bool VerifySignature(const wchar_t* aFilePath); + bool QueryObject(const wchar_t* aFilePath); + static bool VerifySignatureInternal(WINTRUST_DATA& aTrustData); + + private: + enum class TrustSource { eNone, eEmbedded, eCatalog }; + + private: + const mozilla::AuthenticodeFlags mFlags; + TrustSource mTrustSource; + CertStoreUniquePtr mCertStore; + CryptMsgUniquePtr mCryptMsg; + CertContextUniquePtr mCertCtx; +}; + +SignedBinary::SignedBinary(const wchar_t* aFilePath, + mozilla::AuthenticodeFlags aFlags) + : mFlags(aFlags), mTrustSource(TrustSource::eNone) { + if (!VerifySignature(aFilePath)) { + return; + } + + DWORD certInfoLen = 0; + BOOL ok = CryptMsgGetParam(mCryptMsg.get(), CMSG_SIGNER_CERT_INFO_PARAM, 0, + nullptr, &certInfoLen); + if (!ok) { + return; + } + + auto certInfoBuf = mozilla::MakeUnique<char[]>(certInfoLen); + + ok = CryptMsgGetParam(mCryptMsg.get(), CMSG_SIGNER_CERT_INFO_PARAM, 0, + certInfoBuf.get(), &certInfoLen); + if (!ok) { + return; + } + + auto certInfo = reinterpret_cast<CERT_INFO*>(certInfoBuf.get()); + + PCCERT_CONTEXT certCtx = + CertFindCertificateInStore(mCertStore.get(), kEncodingTypes, 0, + CERT_FIND_SUBJECT_CERT, certInfo, nullptr); + if (!certCtx) { + return; + } + + mCertCtx.reset(certCtx); +} + +bool SignedBinary::QueryObject(const wchar_t* aFilePath) { + DWORD encodingType, contentType, formatType; + HCERTSTORE rawCertStore; + HCRYPTMSG rawCryptMsg; + BOOL result = ::CryptQueryObject(CERT_QUERY_OBJECT_FILE, aFilePath, + CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED, + CERT_QUERY_FORMAT_FLAG_BINARY, 0, + &encodingType, &contentType, &formatType, + &rawCertStore, &rawCryptMsg, nullptr); + if (!result) { + return false; + } + + mCertStore.reset(rawCertStore); + mCryptMsg.reset(rawCryptMsg); + + return true; +} + +/** + * @param aTrustData must be a WINTRUST_DATA structure that has been zeroed out + * and then populated at least with its |cbStruct|, + * |dwUnionChoice|, and appropriate union field. This function + * will then populate the remaining fields as appropriate. + */ +/* static */ +bool SignedBinary::VerifySignatureInternal(WINTRUST_DATA& aTrustData) { + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::WinVerifyTrust)> + pWinVerifyTrust(L"wintrust.dll", "WinVerifyTrust"); + if (!pWinVerifyTrust) { + return false; + } + + aTrustData.dwUIChoice = WTD_UI_NONE; + aTrustData.fdwRevocationChecks = WTD_REVOKE_NONE; + aTrustData.dwStateAction = WTD_STATEACTION_VERIFY; + aTrustData.dwProvFlags = WTD_CACHE_ONLY_URL_RETRIEVAL; + + const HWND hwnd = (HWND)INVALID_HANDLE_VALUE; + GUID policyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2; + LONG result = pWinVerifyTrust(hwnd, &policyGUID, &aTrustData); + + aTrustData.dwStateAction = WTD_STATEACTION_CLOSE; + pWinVerifyTrust(hwnd, &policyGUID, &aTrustData); + + return result == ERROR_SUCCESS; +} + +bool SignedBinary::VerifySignature(const wchar_t* aFilePath) { + // First, try the binary itself + if (QueryObject(aFilePath)) { + mTrustSource = TrustSource::eEmbedded; + if (mFlags & mozilla::AuthenticodeFlags::SkipTrustVerification) { + return true; + } + + WINTRUST_FILE_INFO fileInfo = {sizeof(fileInfo)}; + fileInfo.pcwszFilePath = aFilePath; + + WINTRUST_DATA trustData = {sizeof(trustData)}; + trustData.dwUnionChoice = WTD_CHOICE_FILE; + trustData.pFile = &fileInfo; + + return VerifySignatureInternal(trustData); + } + + // We didn't find anything in the binary, so now try a catalog file. + + // First, we open a catalog admin context. + HCATADMIN rawCatAdmin; + + // Windows 7 also exports the CryptCATAdminAcquireContext2 API, but it does + // *not* sign its binaries with SHA-256, so we use the old API in that case. + if (mozilla::IsWin8OrLater()) { + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::CryptCATAdminAcquireContext2)> + pCryptCATAdminAcquireContext2(L"wintrust.dll", + "CryptCATAdminAcquireContext2"); + if (!pCryptCATAdminAcquireContext2) { + return false; + } + + CERT_STRONG_SIGN_PARA policy = {sizeof(policy)}; + policy.dwInfoChoice = CERT_STRONG_SIGN_OID_INFO_CHOICE; + policy.pszOID = const_cast<char*>( + szOID_CERT_STRONG_SIGN_OS_CURRENT); // -Wwritable-strings + + if (!pCryptCATAdminAcquireContext2(&rawCatAdmin, nullptr, + BCRYPT_SHA256_ALGORITHM, &policy, 0)) { + return false; + } + } else { + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::CryptCATAdminAcquireContext)> + pCryptCATAdminAcquireContext(L"wintrust.dll", + "CryptCATAdminAcquireContext"); + + if (!pCryptCATAdminAcquireContext || + !pCryptCATAdminAcquireContext(&rawCatAdmin, nullptr, 0)) { + return false; + } + } + + CATAdminContextUniquePtr catAdmin(rawCatAdmin); + + // Now we need to hash the file at aFilePath. + // Since we're hashing this file, let's open it with a sequential scan hint. + HANDLE rawFile = + ::CreateFileW(aFilePath, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, nullptr); + if (rawFile == INVALID_HANDLE_VALUE) { + return false; + } + + nsAutoHandle file(rawFile); + DWORD hashLen = 0; + mozilla::UniquePtr<BYTE[]> hashBuf; + + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::CryptCATAdminCalcHashFromFileHandle2)> + pCryptCATAdminCalcHashFromFileHandle2( + L"wintrust.dll", "CryptCATAdminCalcHashFromFileHandle2"); + if (pCryptCATAdminCalcHashFromFileHandle2) { + if (!pCryptCATAdminCalcHashFromFileHandle2(rawCatAdmin, rawFile, &hashLen, + nullptr, 0) && + ::GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + return false; + } + + hashBuf = mozilla::MakeUnique<BYTE[]>(hashLen); + + if (!pCryptCATAdminCalcHashFromFileHandle2(rawCatAdmin, rawFile, &hashLen, + hashBuf.get(), 0)) { + return false; + } + } else { + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::CryptCATAdminCalcHashFromFileHandle)> + pCryptCATAdminCalcHashFromFileHandle( + L"wintrust.dll", "CryptCATAdminCalcHashFromFileHandle"); + + if (!pCryptCATAdminCalcHashFromFileHandle) { + return false; + } + + if (!pCryptCATAdminCalcHashFromFileHandle(rawFile, &hashLen, nullptr, 0) && + ::GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + return false; + } + + hashBuf = mozilla::MakeUnique<BYTE[]>(hashLen); + + if (!pCryptCATAdminCalcHashFromFileHandle(rawFile, &hashLen, hashBuf.get(), + 0)) { + return false; + } + } + + // Now that we've hashed the file, query the catalog system to see if any + // catalogs reference a binary with our hash. + + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::CryptCATAdminEnumCatalogFromHash)> + pCryptCATAdminEnumCatalogFromHash(L"wintrust.dll", + "CryptCATAdminEnumCatalogFromHash"); + if (!pCryptCATAdminEnumCatalogFromHash) { + return false; + } + + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::CryptCATAdminReleaseCatalogContext)> + pCryptCATAdminReleaseCatalogContext(L"wintrust.dll", + "CryptCATAdminReleaseCatalogContext"); + if (!pCryptCATAdminReleaseCatalogContext) { + return false; + } + + HCATINFO catInfoHdl = pCryptCATAdminEnumCatalogFromHash( + rawCatAdmin, hashBuf.get(), hashLen, 0, nullptr); + if (!catInfoHdl) { + return false; + } + + // We can't use UniquePtr for this because the deleter function requires two + // parameters. + auto cleanCatInfoHdl = + mozilla::MakeScopeExit([rawCatAdmin, catInfoHdl]() -> void { + pCryptCATAdminReleaseCatalogContext(rawCatAdmin, catInfoHdl, 0); + }); + + // We found a catalog! Now query for the path to the catalog file. + + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::CryptCATCatalogInfoFromContext)> + pCryptCATCatalogInfoFromContext(L"wintrust.dll", + "CryptCATCatalogInfoFromContext"); + if (!pCryptCATCatalogInfoFromContext) { + return false; + } + + CATALOG_INFO_ catInfo = {sizeof(catInfo)}; + if (!pCryptCATCatalogInfoFromContext(catInfoHdl, &catInfo, 0)) { + return false; + } + + if (!QueryObject(catInfo.wszCatalogFile)) { + return false; + } + + mTrustSource = TrustSource::eCatalog; + + if (mFlags & mozilla::AuthenticodeFlags::SkipTrustVerification) { + return true; + } + + // WINTRUST_CATALOG_INFO::pcwszMemberTag is commonly set to the string + // representation of the file hash, so we build that here. + + DWORD strHashBufLen = (hashLen * 2) + 1; + auto strHashBuf = mozilla::MakeUnique<wchar_t[]>(strHashBufLen); + if (!::CryptBinaryToStringW(hashBuf.get(), hashLen, + CRYPT_STRING_HEXRAW | CRYPT_STRING_NOCRLF, + strHashBuf.get(), &strHashBufLen)) { + return false; + } + + // Ensure that the tag is uppercase for WinVerifyTrust + // NB: CryptBinaryToStringW overwrites strHashBufLen with the length excluding + // the null terminator, so we need to add it back for this call. + if (_wcsupr_s(strHashBuf.get(), strHashBufLen + 1)) { + return false; + } + + // Now, given the path to the catalog, and the path to the member (ie, the + // binary whose hash we are validating), we may now validate. If the + // validation is successful, we then QueryObject on the *catalog file* + // instead of the binary. + + WINTRUST_CATALOG_INFO wtCatInfo = {sizeof(wtCatInfo)}; + wtCatInfo.pcwszCatalogFilePath = catInfo.wszCatalogFile; + wtCatInfo.pcwszMemberTag = strHashBuf.get(); + wtCatInfo.pcwszMemberFilePath = aFilePath; + wtCatInfo.hMemberFile = rawFile; + if (mozilla::IsWin8OrLater()) { + wtCatInfo.hCatAdmin = rawCatAdmin; + } + + WINTRUST_DATA trustData = {sizeof(trustData)}; + trustData.dwUnionChoice = WTD_CHOICE_CATALOG; + trustData.pCatalog = &wtCatInfo; + + return VerifySignatureInternal(trustData); +} + +mozilla::UniquePtr<wchar_t[]> SignedBinary::GetOrgName() { + DWORD charCount = CertGetNameStringW( + mCertCtx.get(), CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, nullptr, nullptr, 0); + if (charCount <= 1) { + // Not found + return nullptr; + } + + auto result = mozilla::MakeUnique<wchar_t[]>(charCount); + charCount = CertGetNameStringW(mCertCtx.get(), CERT_NAME_SIMPLE_DISPLAY_TYPE, + 0, nullptr, result.get(), charCount); + MOZ_ASSERT(charCount > 1); + + return result; +} + +} // anonymous namespace + +namespace mozilla { + +class AuthenticodeImpl : public Authenticode { + public: + virtual UniquePtr<wchar_t[]> GetBinaryOrgName( + const wchar_t* aFilePath, + AuthenticodeFlags aFlags = AuthenticodeFlags::Default) override; +}; + +UniquePtr<wchar_t[]> AuthenticodeImpl::GetBinaryOrgName( + const wchar_t* aFilePath, AuthenticodeFlags aFlags) { + SignedBinary bin(aFilePath, aFlags); + if (!bin) { + return nullptr; + } + + return bin.GetOrgName(); +} + +static AuthenticodeImpl sAuthenticodeImpl; + +Authenticode* GetAuthenticode() { return &sAuthenticodeImpl; } + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/mozglue/Authenticode.h b/toolkit/xre/dllservices/mozglue/Authenticode.h new file mode 100644 index 0000000000..182512da2c --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/Authenticode.h @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_Authenticode_h +#define mozilla_Authenticode_h + +#include "mozilla/Maybe.h" +#include "mozilla/TypedEnumBits.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { + +enum class AuthenticodeFlags : uint32_t { + Default = 0, + SkipTrustVerification = 1, +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(AuthenticodeFlags) + +class Authenticode { + public: + virtual UniquePtr<wchar_t[]> GetBinaryOrgName( + const wchar_t* aFilePath, + AuthenticodeFlags aFlags = AuthenticodeFlags::Default) = 0; +}; + +} // namespace mozilla + +#endif // mozilla_Authenticode_h diff --git a/toolkit/xre/dllservices/mozglue/CacheNtDllThunk.cpp b/toolkit/xre/dllservices/mozglue/CacheNtDllThunk.cpp new file mode 100644 index 0000000000..f3eef13504 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/CacheNtDllThunk.cpp @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CacheNtDllThunk.h" + +#include "mozilla/Maybe.h" +#include "mozilla/Span.h" +#include "mozilla/StaticPtr.h" + +namespace mozilla { + +static StaticAutoPtr<Buffer<IMAGE_THUNK_DATA>> sCachedNtDllThunk; + +// This static method initializes sCachedNtDllThunk. Because it's called in +// XREMain::XRE_main, which happens long before WindowsProcessLauncher's ctor +// accesses sCachedNtDllThunk, there is no race on sCachedNtDllThunk, thus +// no mutex is needed. +static void CacheNtDllThunk() { + if (sCachedNtDllThunk) { + return; + } + + do { + nt::PEHeaders ourExeImage(::GetModuleHandleW(nullptr)); + if (!ourExeImage) { + break; + } + + nt::PEHeaders ntdllImage(::GetModuleHandleW(L"ntdll.dll")); + if (!ntdllImage) { + break; + } + + Maybe<Range<const uint8_t>> ntdllBoundaries = ntdllImage.GetBounds(); + if (!ntdllBoundaries) { + break; + } + + Maybe<Span<IMAGE_THUNK_DATA>> maybeNtDllThunks = + ourExeImage.GetIATThunksForModule("ntdll.dll", ntdllBoundaries.ptr()); + if (maybeNtDllThunks.isNothing()) { + break; + } + + sCachedNtDllThunk = new Buffer<IMAGE_THUNK_DATA>(maybeNtDllThunks.value()); + return; + } while (false); + + // Failed to cache IAT. Initializing the variable with nullptr. + sCachedNtDllThunk = new Buffer<IMAGE_THUNK_DATA>(); +} + +static Buffer<IMAGE_THUNK_DATA>* GetCachedNtDllThunk() { + return sCachedNtDllThunk; +} + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/mozglue/CacheNtDllThunk.h b/toolkit/xre/dllservices/mozglue/CacheNtDllThunk.h new file mode 100644 index 0000000000..403e001afc --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/CacheNtDllThunk.h @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_CacheNtDllThunk_h +#define mozilla_CacheNtDllThunk_h + +#include "mozilla/Types.h" +#include "mozilla/Buffer.h" +#include "mozilla/NativeNt.h" + +namespace mozilla { + +MFBT_API void CacheNtDllThunk(); +MFBT_API Buffer<IMAGE_THUNK_DATA>* GetCachedNtDllThunk(); + +}; // namespace mozilla + +#endif // mozilla_CacheNtDllThunk_h diff --git a/toolkit/xre/dllservices/mozglue/LoaderAPIInterfaces.h b/toolkit/xre/dllservices/mozglue/LoaderAPIInterfaces.h new file mode 100644 index 0000000000..16ee93c987 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/LoaderAPIInterfaces.h @@ -0,0 +1,125 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_LoaderAPIInterfaces_h +#define mozilla_LoaderAPIInterfaces_h + +#include "nscore.h" +#include "mozilla/glue/SharedSection.h" +#include "mozilla/ModuleLoadInfo.h" + +namespace mozilla { +namespace nt { + +class NS_NO_VTABLE LoaderObserver { + public: + /** + * Notification that a DLL load has begun. + * + * @param aContext Outparam that allows this observer to store any context + * information pertaining to the current load. + * @param aRequestedDllName The DLL name requested by whatever invoked the + * loader. This name may not match the effective + * name of the DLL once the loader has completed + * its path search. + */ + virtual void OnBeginDllLoad(void** aContext, + PCUNICODE_STRING aRequestedDllName) = 0; + + /** + * Query the observer to determine whether the DLL named |aLSPLeafName| needs + * to be substituted with another module, and substitute the module handle + * when necessary. + * + * @return true when substitution occurs, otherwise false + */ + virtual bool SubstituteForLSP(PCUNICODE_STRING aLSPLeafName, + PHANDLE aOutHandle) = 0; + + /** + * Notification that a DLL load has ended. + * + * @param aContext The context that was set by the corresponding call to + * OnBeginDllLoad + * @param aNtStatus The NTSTATUS returned by LdrLoadDll + * @param aModuleLoadInfo Telemetry information that was gathered about the + * load. + */ + virtual void OnEndDllLoad(void* aContext, NTSTATUS aNtStatus, + ModuleLoadInfo&& aModuleLoadInfo) = 0; + + /** + * Called to inform the observer that it is no longer active and, if + * necessary, call aNext->OnForward() with any accumulated telemetry + * information. + */ + virtual void Forward(LoaderObserver* aNext) = 0; + + /** + * Receives a vector of module load telemetry from a previous LoaderObserver. + */ + virtual void OnForward(ModuleLoadInfoVec&& aInfo) = 0; +}; + +class NS_NO_VTABLE LoaderAPI { + public: + /** + * Construct a new ModuleLoadInfo structure and notify the LoaderObserver + * that a library load is beginning. + */ + virtual ModuleLoadInfo ConstructAndNotifyBeginDllLoad( + void** aContext, PCUNICODE_STRING aRequestedDllName) = 0; + + /** + * Query to determine whether the DLL named |aLSPLeafName| needs to be + * substituted with another module, and substitute the module handle when + * necessary. + * + * @return true when substitution occurs, otherwise false + */ + virtual bool SubstituteForLSP(PCUNICODE_STRING aLSPLeafName, + PHANDLE aOutHandle) = 0; + + /** + * Notification that a DLL load has ended. + */ + virtual void NotifyEndDllLoad(void* aContext, NTSTATUS aLoadNtStatus, + ModuleLoadInfo&& aModuleLoadInfo) = 0; + + /** + * Given the address of a mapped section, obtain the name of the file that is + * backing it. + */ + virtual AllocatedUnicodeString GetSectionName(void* aSectionAddr) = 0; + + using InitDllBlocklistOOPFnPtr = LauncherVoidResultWithLineInfo (*)( + const wchar_t*, HANDLE, const IMAGE_THUNK_DATA*, const bool, const bool); + using HandleLauncherErrorFnPtr = void (*)(const LauncherError&, const char*); + + /** + * Return a pointer to winlauncher's function. + * Used by sandboxBroker::LaunchApp. + */ + virtual InitDllBlocklistOOPFnPtr GetDllBlocklistInitFn() = 0; + virtual HandleLauncherErrorFnPtr GetHandleLauncherErrorFn() = 0; + virtual SharedSection* GetSharedSection() = 0; +}; + +struct WinLauncherServices final { + nt::LoaderAPI::InitDllBlocklistOOPFnPtr mInitDllBlocklistOOP; + nt::LoaderAPI::HandleLauncherErrorFnPtr mHandleLauncherError; + SharedSection* mSharedSection; + + WinLauncherServices() + : mInitDllBlocklistOOP(nullptr), + mHandleLauncherError(nullptr), + mSharedSection(nullptr) {} +}; + +} // namespace nt +} // namespace mozilla + +#endif // mozilla_LoaderAPIInterfaces_h diff --git a/toolkit/xre/dllservices/mozglue/LoaderObserver.cpp b/toolkit/xre/dllservices/mozglue/LoaderObserver.cpp new file mode 100644 index 0000000000..d2014365c4 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/LoaderObserver.cpp @@ -0,0 +1,149 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "LoaderObserver.h" + +#include "mozilla/AutoProfilerLabel.h" +#include "mozilla/BaseProfilerMarkers.h" +#include "mozilla/glue/WindowsUnicode.h" +#include "mozilla/StackWalk_windows.h" +#include "mozilla/TimeStamp.h" + +namespace mozilla { + +extern glue::Win32SRWLock gDllServicesLock; +extern glue::detail::DllServicesBase* gDllServices; + +namespace glue { + +void LoaderObserver::OnBeginDllLoad(void** aContext, + PCUNICODE_STRING aRequestedDllName) { + MOZ_ASSERT(aContext); + if (IsProfilerPresent()) { + UniquePtr<char[]> utf8RequestedDllName(WideToUTF8(aRequestedDllName)); + BASE_PROFILER_MARKER_TEXT( + "DllLoad", OTHER, MarkerTiming::IntervalStart(), + mozilla::ProfilerString8View::WrapNullTerminatedString( + utf8RequestedDllName.get())); + *aContext = utf8RequestedDllName.release(); + } + +#ifdef _M_AMD64 + // Prevent the stack walker from suspending this thread when LdrLoadDll + // holds the RtlLookupFunctionEntry lock. + SuppressStackWalking(); +#endif +} + +bool LoaderObserver::SubstituteForLSP(PCUNICODE_STRING aLSPLeafName, + PHANDLE aOutHandle) { + // Currently unsupported + return false; +} + +void LoaderObserver::OnEndDllLoad(void* aContext, NTSTATUS aNtStatus, + ModuleLoadInfo&& aModuleLoadInfo) { +#ifdef _M_AMD64 + DesuppressStackWalking(); +#endif + + if (aContext) { + UniquePtr<char[]> utf8RequestedDllName{static_cast<char*>(aContext)}; + BASE_PROFILER_MARKER_TEXT( + "DllLoad", OTHER, MarkerTiming::IntervalEnd(), + mozilla::ProfilerString8View::WrapNullTerminatedString( + utf8RequestedDllName.get())); + } + + // We want to record a denied DLL load regardless of |aNtStatus| because + // |aNtStatus| is set to access-denied when DLL load was blocked. + if ((!NT_SUCCESS(aNtStatus) && !aModuleLoadInfo.WasDenied()) || + !aModuleLoadInfo.WasMapped()) { + return; + } + + { // Scope for lock + AutoSharedLock lock(gDllServicesLock); + if (gDllServices) { + gDllServices->DispatchDllLoadNotification(std::move(aModuleLoadInfo)); + return; + } + } + + // No dll services, save for later + AutoExclusiveLock lock(mLock); + if (!mEnabled) { + return; + } + + if (!mModuleLoads) { + mModuleLoads = new ModuleLoadInfoVec(); + } + + Unused << mModuleLoads->emplaceBack( + std::forward<ModuleLoadInfo>(aModuleLoadInfo)); +} + +void LoaderObserver::Forward(nt::LoaderObserver* aNext) { + MOZ_ASSERT_UNREACHABLE( + "This implementation does not forward to any more " + "nt::LoaderObserver objects"); +} + +void LoaderObserver::Forward(detail::DllServicesBase* aNext) { + MOZ_ASSERT(aNext); + if (!aNext) { + return; + } + + ModuleLoadInfoVec* moduleLoads = nullptr; + + { // Scope for lock + AutoExclusiveLock lock(mLock); + moduleLoads = mModuleLoads; + mModuleLoads = nullptr; + } + + if (!moduleLoads) { + return; + } + + aNext->DispatchModuleLoadBacklogNotification(std::move(*moduleLoads)); + delete moduleLoads; +} + +void LoaderObserver::Disable() { + ModuleLoadInfoVec* moduleLoads = nullptr; + + { // Scope for lock + AutoExclusiveLock lock(mLock); + moduleLoads = mModuleLoads; + mModuleLoads = nullptr; + mEnabled = false; + } + + delete moduleLoads; +} + +void LoaderObserver::OnForward(ModuleLoadInfoVec&& aInfo) { + AutoExclusiveLock lock(mLock); + if (!mModuleLoads) { + mModuleLoads = new ModuleLoadInfoVec(); + } + + MOZ_ASSERT(mModuleLoads->empty()); + if (mModuleLoads->empty()) { + *mModuleLoads = std::move(aInfo); + } else { + // This should not happen, but we can handle it + for (auto&& item : aInfo) { + Unused << mModuleLoads->append(std::move(item)); + } + } +} + +} // namespace glue +} // namespace mozilla diff --git a/toolkit/xre/dllservices/mozglue/LoaderObserver.h b/toolkit/xre/dllservices/mozglue/LoaderObserver.h new file mode 100644 index 0000000000..39b13035a3 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/LoaderObserver.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glue_LoaderObserver_h +#define mozilla_glue_LoaderObserver_h + +#include "mozilla/Attributes.h" +#include "mozilla/LoaderAPIInterfaces.h" +#include "mozilla/glue/WindowsDllServices.h" +#include "mozilla/glue/WinUtils.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { +namespace glue { + +class MOZ_ONLY_USED_TO_AVOID_STATIC_CONSTRUCTORS LoaderObserver final + : public nt::LoaderObserver { + public: + constexpr LoaderObserver() : mModuleLoads(nullptr), mEnabled(true) {} + + void OnBeginDllLoad(void** aContext, + PCUNICODE_STRING aPreliminaryDllName) final; + bool SubstituteForLSP(PCUNICODE_STRING aLspLeafName, + PHANDLE aOutHandle) final; + void OnEndDllLoad(void* aContext, NTSTATUS aNtStatus, + ModuleLoadInfo&& aModuleLoadInfo) final; + void Forward(nt::LoaderObserver* aNext) final; + void OnForward(ModuleLoadInfoVec&& aInfo) final; + + void Forward(mozilla::glue::detail::DllServicesBase* aSvc); + void Disable(); + + private: + Win32SRWLock mLock; + ModuleLoadInfoVec* mModuleLoads; + bool mEnabled; +}; + +} // namespace glue +} // namespace mozilla + +#endif // mozilla_glue_LoaderObserver_h diff --git a/toolkit/xre/dllservices/mozglue/ModuleLoadFrame.cpp b/toolkit/xre/dllservices/mozglue/ModuleLoadFrame.cpp new file mode 100644 index 0000000000..a49505adbc --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/ModuleLoadFrame.cpp @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ModuleLoadFrame.h" +#include "mozilla/NativeNt.h" +#include "mozilla/UniquePtr.h" +#include "NtLoaderAPI.h" + +#include <string.h> + +#include "WindowsFallbackLoaderAPI.h" + +static bool IsNullTerminated(PCUNICODE_STRING aStr) { + return aStr && (aStr->MaximumLength >= (aStr->Length + sizeof(WCHAR))) && + aStr->Buffer && aStr->Buffer[aStr->Length / sizeof(WCHAR)] == 0; +} + +static mozilla::FallbackLoaderAPI gFallbackLoaderAPI; + +namespace mozilla { +namespace glue { + +nt::LoaderAPI* ModuleLoadFrame::sLoaderAPI; + +using GetNtLoaderAPIFn = decltype(&mozilla::GetNtLoaderAPI); + +/* static */ +void ModuleLoadFrame::StaticInit(nt::LoaderObserver* aNewObserver, + nt::WinLauncherServices* aOutWinLauncher) { + const auto pGetNtLoaderAPI = reinterpret_cast<GetNtLoaderAPIFn>( + ::GetProcAddress(::GetModuleHandleW(nullptr), "GetNtLoaderAPI")); + if (!pGetNtLoaderAPI) { + // This case occurs in processes other than firefox.exe that do not contain + // the launcher process blocklist. + gFallbackLoaderAPI.SetObserver(aNewObserver); + sLoaderAPI = &gFallbackLoaderAPI; + + if (aOutWinLauncher) { + aOutWinLauncher->mHandleLauncherError = [](const mozilla::LauncherError&, + const char*) {}; + // We intentionally leave mInitDllBlocklistOOP null to make sure calling + // mInitDllBlocklistOOP in non-Firefox hits MOZ_RELEASE_ASSERT. + } + return; + } + + sLoaderAPI = pGetNtLoaderAPI(aNewObserver); + MOZ_ASSERT(sLoaderAPI); + + if (aOutWinLauncher) { + aOutWinLauncher->mInitDllBlocklistOOP = sLoaderAPI->GetDllBlocklistInitFn(); + aOutWinLauncher->mHandleLauncherError = + sLoaderAPI->GetHandleLauncherErrorFn(); + aOutWinLauncher->mSharedSection = sLoaderAPI->GetSharedSection(); + } +} + +ModuleLoadFrame::ModuleLoadFrame(PCUNICODE_STRING aRequestedDllName) + : mAlreadyLoaded(false), + mContext(nullptr), + mDllLoadStatus(STATUS_UNSUCCESSFUL), + mLoadInfo(sLoaderAPI->ConstructAndNotifyBeginDllLoad(&mContext, + aRequestedDllName)) { + if (!aRequestedDllName) { + return; + } + + UniquePtr<WCHAR[]> nameBuf; + const WCHAR* name = nullptr; + + if (IsNullTerminated(aRequestedDllName)) { + name = aRequestedDllName->Buffer; + } else { + USHORT charLenExclNul = aRequestedDllName->Length / sizeof(WCHAR); + USHORT charLenInclNul = charLenExclNul + 1; + nameBuf = MakeUnique<WCHAR[]>(charLenInclNul); + if (!wcsncpy_s(nameBuf.get(), charLenInclNul, aRequestedDllName->Buffer, + charLenExclNul)) { + name = nameBuf.get(); + } + } + + mAlreadyLoaded = name && !!::GetModuleHandleW(name); +} + +ModuleLoadFrame::~ModuleLoadFrame() { + sLoaderAPI->NotifyEndDllLoad(mContext, mDllLoadStatus, std::move(mLoadInfo)); +} + +void ModuleLoadFrame::SetLoadStatus(NTSTATUS aNtStatus, HANDLE aHandle) { + mDllLoadStatus = aNtStatus; + void* baseAddr = mozilla::nt::PEHeaders::HModuleToBaseAddr<void*>( + reinterpret_cast<HMODULE>(aHandle)); + mLoadInfo.mBaseAddr = baseAddr; + if (!mAlreadyLoaded) { + mLoadInfo.mSectionName = sLoaderAPI->GetSectionName(baseAddr); + } +} + +} // namespace glue +} // namespace mozilla diff --git a/toolkit/xre/dllservices/mozglue/ModuleLoadFrame.h b/toolkit/xre/dllservices/mozglue/ModuleLoadFrame.h new file mode 100644 index 0000000000..d47e42ab0c --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/ModuleLoadFrame.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glue_ModuleLoadFrame_h +#define mozilla_glue_ModuleLoadFrame_h + +#include "mozilla/Attributes.h" +#include "mozilla/LoaderAPIInterfaces.h" + +namespace mozilla { +namespace glue { + +class MOZ_RAII ModuleLoadFrame final { + public: + explicit ModuleLoadFrame(PCUNICODE_STRING aRequestedDllName); + ~ModuleLoadFrame(); + + void SetLoadStatus(NTSTATUS aNtStatus, HANDLE aHandle); + + ModuleLoadFrame(const ModuleLoadFrame&) = delete; + ModuleLoadFrame(ModuleLoadFrame&&) = delete; + ModuleLoadFrame& operator=(const ModuleLoadFrame&) = delete; + ModuleLoadFrame& operator=(ModuleLoadFrame&&) = delete; + + static void StaticInit(nt::LoaderObserver* aNewObserver, + nt::WinLauncherServices* aOutWinLauncher); + + private: + bool mAlreadyLoaded; + void* mContext; + NTSTATUS mDllLoadStatus; + ModuleLoadInfo mLoadInfo; + + private: + static nt::LoaderAPI* sLoaderAPI; +}; + +} // namespace glue +} // namespace mozilla + +#endif // mozilla_glue_ModuleLoadFrame_h diff --git a/toolkit/xre/dllservices/mozglue/ModuleLoadInfo.h b/toolkit/xre/dllservices/mozglue/ModuleLoadInfo.h new file mode 100644 index 0000000000..807adaae0a --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/ModuleLoadInfo.h @@ -0,0 +1,174 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_ModuleLoadInfo_h +#define mozilla_ModuleLoadInfo_h + +#include "mozilla/NativeNt.h" +#include "mozilla/Vector.h" +#include "mozilla/Unused.h" + +namespace mozilla { + +struct ModuleLoadInfo final { + enum class Status : uint32_t { + Loaded = 0, + Blocked, + Redirected, + }; + + // We do not provide these methods inside Gecko proper. +#if !defined(MOZILLA_INTERNAL_API) + + /** + * This constructor is for use by the LdrLoadDll hook. + */ + explicit ModuleLoadInfo(PCUNICODE_STRING aRequestedDllName) + : mLoadTimeInfo(), + mThreadId(nt::RtlGetCurrentThreadId()), + mRequestedDllName(aRequestedDllName), + mBaseAddr(nullptr), + mStatus(Status::Loaded), + mIsDependent(false) { +# if defined(IMPL_MFBT) + ::QueryPerformanceCounter(&mBeginTimestamp); +# else + ::RtlQueryPerformanceCounter(&mBeginTimestamp); +# endif // defined(IMPL_MFBT) + } + + /** + * This constructor is used by the NtMapViewOfSection hook IF AND ONLY IF + * the LdrLoadDll hook did not already construct a ModuleLoadInfo for the + * current DLL load. This may occur while the loader is loading dependencies + * of another library. + */ + ModuleLoadInfo(nt::AllocatedUnicodeString&& aSectionName, + const void* aBaseAddr, Status aLoadStatus, bool aIsDependent) + : mLoadTimeInfo(), + mThreadId(nt::RtlGetCurrentThreadId()), + mSectionName(std::move(aSectionName)), + mBaseAddr(aBaseAddr), + mStatus(aLoadStatus), + mIsDependent(aIsDependent) { +# if defined(IMPL_MFBT) + ::QueryPerformanceCounter(&mBeginTimestamp); +# else + ::RtlQueryPerformanceCounter(&mBeginTimestamp); +# endif // defined(IMPL_MFBT) + } + + /** + * Marks the time that LdrLoadDll began loading this library. + */ + void SetBeginLoadTimeStamp() { +# if defined(IMPL_MFBT) + ::QueryPerformanceCounter(&mLoadTimeInfo); +# else + ::RtlQueryPerformanceCounter(&mLoadTimeInfo); +# endif // defined(IMPL_MFBT) + } + + /** + * Marks the time that LdrLoadDll finished loading this library. + */ + void SetEndLoadTimeStamp() { + LARGE_INTEGER endTimeStamp; +# if defined(IMPL_MFBT) + ::QueryPerformanceCounter(&endTimeStamp); +# else + ::RtlQueryPerformanceCounter(&endTimeStamp); +# endif // defined(IMPL_MFBT) + + LONGLONG& timeInfo = mLoadTimeInfo.QuadPart; + if (!timeInfo) { + return; + } + + timeInfo = endTimeStamp.QuadPart - timeInfo; + } + + /** + * Saves the current thread's call stack. + */ + void CaptureBacktrace() { + const DWORD kMaxBacktraceSize = 512; + + if (!mBacktrace.resize(kMaxBacktraceSize)) { + return; + } + + // We don't use a Win32 variant here because Win32's CaptureStackBackTrace + // is just a macro that resolve to this function anyway. + WORD numCaptured = ::RtlCaptureStackBackTrace(2, kMaxBacktraceSize, + mBacktrace.begin(), nullptr); + Unused << mBacktrace.resize(numCaptured); + // These backtraces might stick around for a while, so let's trim any + // excess memory. + mBacktrace.shrinkStorageToFit(); + } + +#endif // !defined(MOZILLA_INTERNAL_API) + + ModuleLoadInfo(ModuleLoadInfo&&) = default; + ModuleLoadInfo& operator=(ModuleLoadInfo&&) = default; + + ModuleLoadInfo() = delete; + ModuleLoadInfo(const ModuleLoadInfo&) = delete; + ModuleLoadInfo& operator=(const ModuleLoadInfo&) = delete; + + /** + * A "bare" module load is one that was mapped without the code passing + * through a call to ntdll!LdrLoadDll. + */ + bool IsBare() const { + // SetBeginLoadTimeStamp() and SetEndLoadTimeStamp() are only called by the + // LdrLoadDll hook, so when mLoadTimeInfo == 0, we know that we are bare. + return !mLoadTimeInfo.QuadPart; + } + + /** + * Returns true for DLL loads where LdrLoadDll was called but + * NtMapViewOfSection was not. This will happen for DLL requests where the DLL + * was already mapped into memory by a previous request. + */ + bool WasMapped() const { return !mSectionName.IsEmpty(); } + + /** + * Returns true for DLL load which was denied by our blocklist. + */ + bool WasDenied() const { + return mStatus == ModuleLoadInfo::Status::Blocked || + mStatus == ModuleLoadInfo::Status::Redirected; + } + + // Timestamp for the creation of this event + LARGE_INTEGER mBeginTimestamp; + // Duration of the LdrLoadDll call + LARGE_INTEGER mLoadTimeInfo; + // Thread ID of this DLL load + DWORD mThreadId; + // The name requested of LdrLoadDll by its caller + nt::AllocatedUnicodeString mRequestedDllName; + // The name of the DLL that backs section that was mapped by the loader. This + // string is the effective name of the DLL that was resolved by the loader's + // path search algorithm. + nt::AllocatedUnicodeString mSectionName; + // The base address of the module's mapped section + const void* mBaseAddr; + // If the module was successfully loaded, stack trace of the DLL load request + Vector<PVOID, 0, nt::RtlAllocPolicy> mBacktrace; + // The status of DLL load + Status mStatus; + // Whether the module is one of the executables's dependent modules or not + bool mIsDependent; +}; + +using ModuleLoadInfoVec = Vector<ModuleLoadInfo, 0, nt::RtlAllocPolicy>; + +} // namespace mozilla + +#endif // mozilla_ModuleLoadInfo_h diff --git a/toolkit/xre/dllservices/mozglue/NtLoaderAPI.h b/toolkit/xre/dllservices/mozglue/NtLoaderAPI.h new file mode 100644 index 0000000000..628609092b --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/NtLoaderAPI.h @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_NtLoaderAPI_h +#define mozilla_NtLoaderAPI_h + +#include "mozilla/LoaderAPIInterfaces.h" + +#if !defined(IMPL_MFBT) +# error "This should only be included from mozglue!" +#endif // !defined(IMPL_MFBT) + +namespace mozilla { + +extern "C" MOZ_IMPORT_API nt::LoaderAPI* GetNtLoaderAPI( + nt::LoaderObserver* aNewObserver); + +} // namespace mozilla + +#endif // mozilla_NtLoaderAPI_h diff --git a/toolkit/xre/dllservices/mozglue/SharedSection.h b/toolkit/xre/dllservices/mozglue/SharedSection.h new file mode 100644 index 0000000000..162c8e8343 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/SharedSection.h @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glue_SharedSection_h +#define mozilla_glue_SharedSection_h + +#include <winternl.h> +#include "nscore.h" +#include "mozilla/Span.h" +#include "mozilla/WindowsDllBlocklistInfo.h" + +namespace mozilla::nt { + +// This interface provides a way to access winlauncher's shared section +// through DllServices. +struct NS_NO_VTABLE SharedSection { + virtual Span<const wchar_t> GetDependentModules() = 0; + // Returns a span of the whole space for dynamic blocklist entries. + // Use IsValidDynamicBlocklistEntry() to determine the end of the list. + virtual Span<const DllBlockInfoT<UNICODE_STRING>> GetDynamicBlocklist() = 0; +}; + +} // namespace mozilla::nt + +#endif // mozilla_glue_SharedSection_h diff --git a/toolkit/xre/dllservices/mozglue/WindowsDllBlocklist.cpp b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklist.cpp new file mode 100644 index 0000000000..9f0b50a583 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklist.cpp @@ -0,0 +1,781 @@ +/* -*- Mode: C++; tab-width: 40; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> +#include <winternl.h> + +#pragma warning(push) +#pragma warning(disable : 4275 4530) // See msvc-stl-wrapper.template.h +#include <map> +#pragma warning(pop) + +#include "Authenticode.h" +#include "BaseProfiler.h" +#include "nsWindowsDllInterceptor.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/StackWalk_windows.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Vector.h" +#include "mozilla/WindowsProcessMitigations.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "nsWindowsHelpers.h" +#include "WindowsDllBlocklist.h" +#include "mozilla/AutoProfilerLabel.h" +#include "mozilla/glue/Debug.h" +#include "mozilla/glue/WindowsDllServices.h" +#include "mozilla/glue/WinUtils.h" + +// Start new implementation +#include "LoaderObserver.h" +#include "ModuleLoadFrame.h" +#include "mozilla/glue/WindowsUnicode.h" + +namespace mozilla { + +glue::Win32SRWLock gDllServicesLock; +glue::detail::DllServicesBase* gDllServices; + +} // namespace mozilla + +using namespace mozilla; + +using CrashReporter::Annotation; +using CrashReporter::AnnotationWriter; + +#define DLL_BLOCKLIST_ENTRY(name, ...) {name, __VA_ARGS__}, +#define DLL_BLOCKLIST_STRING_TYPE const char* +#include "mozilla/WindowsDllBlocklistLegacyDefs.h" + +// define this for very verbose dll load debug spew +#undef DEBUG_very_verbose + +static uint32_t sInitFlags; +static bool sBlocklistInitAttempted; +static bool sBlocklistInitFailed; +static bool sUser32BeforeBlocklist; + +typedef MOZ_NORETURN_PTR void(__fastcall* BaseThreadInitThunk_func)( + BOOL aIsInitialThread, void* aStartAddress, void* aThreadParam); +static WindowsDllInterceptor::FuncHookType<BaseThreadInitThunk_func> + stub_BaseThreadInitThunk; + +typedef NTSTATUS(NTAPI* LdrLoadDll_func)(PWCHAR filePath, PULONG flags, + PUNICODE_STRING moduleFileName, + PHANDLE handle); +static WindowsDllInterceptor::FuncHookType<LdrLoadDll_func> stub_LdrLoadDll; + +#ifdef _M_AMD64 +typedef decltype(RtlInstallFunctionTableCallback)* + RtlInstallFunctionTableCallback_func; +static WindowsDllInterceptor::FuncHookType<RtlInstallFunctionTableCallback_func> + stub_RtlInstallFunctionTableCallback; + +extern uint8_t* sMsMpegJitCodeRegionStart; +extern size_t sMsMpegJitCodeRegionSize; + +BOOLEAN WINAPI patched_RtlInstallFunctionTableCallback( + DWORD64 TableIdentifier, DWORD64 BaseAddress, DWORD Length, + PGET_RUNTIME_FUNCTION_CALLBACK Callback, PVOID Context, + PCWSTR OutOfProcessCallbackDll) { + // msmpeg2vdec.dll sets up a function table callback for their JIT code that + // just terminates the process, because their JIT doesn't have unwind info. + // If we see this callback being registered, record the region address, so + // that StackWalk.cpp can avoid unwinding addresses in this region. + // + // To keep things simple I'm not tracking unloads of msmpeg2vdec.dll. + // Worst case the stack walker will needlessly avoid a few pages of memory. + + // Tricky: GetModuleHandleExW adds a ref by default; GetModuleHandleW doesn't. + HMODULE callbackModule = nullptr; + DWORD moduleFlags = GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT; + + // These GetModuleHandle calls enter a critical section on Win7. + AutoSuppressStackWalking suppress; + + if (GetModuleHandleExW(moduleFlags, (LPWSTR)Callback, &callbackModule) && + GetModuleHandleW(L"msmpeg2vdec.dll") == callbackModule) { + sMsMpegJitCodeRegionStart = (uint8_t*)BaseAddress; + sMsMpegJitCodeRegionSize = Length; + } + + return stub_RtlInstallFunctionTableCallback(TableIdentifier, BaseAddress, + Length, Callback, Context, + OutOfProcessCallbackDll); +} +#endif + +template <class T> +struct RVAMap { + RVAMap(HANDLE map, DWORD offset) { + SYSTEM_INFO info; + GetSystemInfo(&info); + + DWORD alignedOffset = + (offset / info.dwAllocationGranularity) * info.dwAllocationGranularity; + + MOZ_ASSERT(offset - alignedOffset < info.dwAllocationGranularity, "Wtf"); + + mRealView = ::MapViewOfFile(map, FILE_MAP_READ, 0, alignedOffset, + sizeof(T) + (offset - alignedOffset)); + + mMappedView = + mRealView + ? reinterpret_cast<T*>((char*)mRealView + (offset - alignedOffset)) + : nullptr; + } + ~RVAMap() { + if (mRealView) { + ::UnmapViewOfFile(mRealView); + } + } + operator const T*() const { return mMappedView; } + const T* operator->() const { return mMappedView; } + + private: + const T* mMappedView; + void* mRealView; +}; + +static DWORD GetTimestamp(const wchar_t* path) { + DWORD timestamp = 0; + + HANDLE file = ::CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file != INVALID_HANDLE_VALUE) { + HANDLE map = + ::CreateFileMappingW(file, nullptr, PAGE_READONLY, 0, 0, nullptr); + if (map) { + RVAMap<IMAGE_DOS_HEADER> peHeader(map, 0); + if (peHeader) { + RVAMap<IMAGE_NT_HEADERS> ntHeader(map, peHeader->e_lfanew); + if (ntHeader) { + timestamp = ntHeader->FileHeader.TimeDateStamp; + } + } + ::CloseHandle(map); + } + ::CloseHandle(file); + } + + return timestamp; +} + +// This lock protects both the reentrancy sentinel and the crash reporter +// data structures. +static CRITICAL_SECTION sLock; + +/** + * Some versions of Windows call LoadLibraryEx to get the version information + * for a DLL, which causes our patched LdrLoadDll implementation to re-enter + * itself and cause infinite recursion and a stack-exhaustion crash. We protect + * against reentrancy by allowing recursive loads of the same DLL. + * + * Note that we don't use __declspec(thread) because that doesn't work in DLLs + * loaded via LoadLibrary and there can be a limited number of TLS slots, so + * we roll our own. + */ +class ReentrancySentinel { + public: + explicit ReentrancySentinel(const char* dllName) { + DWORD currentThreadId = GetCurrentThreadId(); + AutoCriticalSection lock(&sLock); + mPreviousDllName = (*sThreadMap)[currentThreadId]; + + // If there is a DLL currently being loaded and it has the same name + // as the current attempt, we're re-entering. + mReentered = mPreviousDllName && !stricmp(mPreviousDllName, dllName); + (*sThreadMap)[currentThreadId] = dllName; + } + + ~ReentrancySentinel() { + DWORD currentThreadId = GetCurrentThreadId(); + AutoCriticalSection lock(&sLock); + (*sThreadMap)[currentThreadId] = mPreviousDllName; + } + + bool BailOut() const { return mReentered; }; + + static void InitializeStatics() { + InitializeCriticalSection(&sLock); + sThreadMap = new std::map<DWORD, const char*>; + } + + private: + static std::map<DWORD, const char*>* sThreadMap; + + const char* mPreviousDllName; + bool mReentered; +}; + +std::map<DWORD, const char*>* ReentrancySentinel::sThreadMap; + +using WritableBuffer = mozilla::glue::detail::WritableBuffer<1024>; + +/** + * This is a linked list of DLLs that have been blocked. It doesn't use + * mozilla::LinkedList because this is an append-only list and doesn't need + * to be doubly linked. + */ +class DllBlockSet { + public: + static void Add(const char* name, unsigned long long version); + + // Write the list of blocked DLLs to a WritableBuffer object. This method is + // run after a crash occurs and must therefore not use the heap, etc. + static void Write(WritableBuffer& buffer); + + private: + DllBlockSet(const char* name, unsigned long long version) + : mName(name), mVersion(version), mNext(nullptr) {} + + const char* mName; // points into the gWindowsDllBlocklist string + unsigned long long mVersion; + DllBlockSet* mNext; + + static DllBlockSet* gFirst; +}; + +DllBlockSet* DllBlockSet::gFirst; + +void DllBlockSet::Add(const char* name, unsigned long long version) { + AutoCriticalSection lock(&sLock); + for (DllBlockSet* b = gFirst; b; b = b->mNext) { + if (0 == strcmp(b->mName, name) && b->mVersion == version) { + return; + } + } + // Not already present + DllBlockSet* n = new DllBlockSet(name, version); + n->mNext = gFirst; + gFirst = n; +} + +void DllBlockSet::Write(WritableBuffer& buffer) { + // It would be nicer to use AutoCriticalSection here. However, its destructor + // might not run if an exception occurs, in which case we would never leave + // the critical section. (MSVC warns about this possibility.) So we + // enter and leave manually. + ::EnterCriticalSection(&sLock); + + // Because this method is called after a crash occurs, and uses heap memory, + // protect this entire block with a structured exception handler. + MOZ_SEH_TRY { + for (DllBlockSet* b = gFirst; b; b = b->mNext) { + // write name[,v.v.v.v]; + buffer.Write(b->mName, strlen(b->mName)); + if (b->mVersion != DllBlockInfo::ALL_VERSIONS) { + buffer.Write(",", 1); + uint16_t parts[4]; + parts[0] = b->mVersion >> 48; + parts[1] = (b->mVersion >> 32) & 0xFFFF; + parts[2] = (b->mVersion >> 16) & 0xFFFF; + parts[3] = b->mVersion & 0xFFFF; + for (int p = 0; p < 4; ++p) { + char buf[32]; + _ltoa_s(parts[p], buf, sizeof(buf), 10); + buffer.Write(buf, strlen(buf)); + if (p != 3) { + buffer.Write(".", 1); + } + } + } + buffer.Write(";", 1); + } + } + MOZ_SEH_EXCEPT(EXCEPTION_EXECUTE_HANDLER) {} + + ::LeaveCriticalSection(&sLock); +} + +static UniquePtr<wchar_t[]> getFullPath(PWCHAR filePath, wchar_t* fname) { + // In Windows 8, the first parameter seems to be used for more than just the + // path name. For example, its numerical value can be 1. Passing a non-valid + // pointer to SearchPathW will cause a crash, so we need to check to see if we + // are handed a valid pointer, and otherwise just pass nullptr to SearchPathW. + PWCHAR sanitizedFilePath = nullptr; + if ((uintptr_t(filePath) >= 65536) && ((uintptr_t(filePath) & 1) == 0)) { + sanitizedFilePath = filePath; + } + + // figure out the length of the string that we need + DWORD pathlen = + SearchPathW(sanitizedFilePath, fname, L".dll", 0, nullptr, nullptr); + if (pathlen == 0) { + return nullptr; + } + + auto full_fname = MakeUnique<wchar_t[]>(pathlen + 1); + if (!full_fname) { + // couldn't allocate memory? + return nullptr; + } + + // now actually grab it + SearchPathW(sanitizedFilePath, fname, L".dll", pathlen + 1, full_fname.get(), + nullptr); + return full_fname; +} + +// No builtin function to find the last character matching a set +static wchar_t* lastslash(wchar_t* s, int len) { + for (wchar_t* c = s + len - 1; c >= s; --c) { + if (*c == L'\\' || *c == L'/') { + return c; + } + } + return nullptr; +} + +static NTSTATUS NTAPI patched_LdrLoadDll(PWCHAR filePath, PULONG flags, + PUNICODE_STRING moduleFileName, + PHANDLE handle) { + // We have UCS2 (UTF16?), we want ASCII, but we also just want the filename + // portion +#define DLLNAME_MAX 128 + char dllName[DLLNAME_MAX + 1]; + wchar_t* dll_part; + char* dot; + + int len = moduleFileName->Length / 2; + wchar_t* fname = moduleFileName->Buffer; + UniquePtr<wchar_t[]> full_fname; + + // The filename isn't guaranteed to be null terminated, but in practice + // it always will be; ensure that this is so, and bail if not. + // This is done instead of the more robust approach because of bug 527122, + // where lots of weird things were happening when we tried to make a copy. + if (moduleFileName->MaximumLength < moduleFileName->Length + 2 || + fname[len] != 0) { +#ifdef DEBUG + printf_stderr("LdrLoadDll: non-null terminated string found!\n"); +#endif + goto continue_loading; + } + + dll_part = lastslash(fname, len); + if (dll_part) { + dll_part = dll_part + 1; + len -= dll_part - fname; + } else { + dll_part = fname; + } + +#ifdef DEBUG_very_verbose + printf_stderr("LdrLoadDll: dll_part '%S' %d\n", dll_part, len); +#endif + + // if it's too long, then, we assume we won't want to block it, + // since DLLNAME_MAX should be at least long enough to hold the longest + // entry in our blocklist. + if (len > DLLNAME_MAX) { +#ifdef DEBUG + printf_stderr("LdrLoadDll: len too long! %d\n", len); +#endif + goto continue_loading; + } + + // copy over to our char byte buffer, lowercasing ASCII as we go + for (int i = 0; i < len; i++) { + wchar_t c = dll_part[i]; + + if (c > 0x7f) { + // welp, it's not ascii; if we need to add non-ascii things to + // our blocklist, we'll have to remove this limitation. + goto continue_loading; + } + + // ensure that dll name is all lowercase + if (c >= 'A' && c <= 'Z') c += 'a' - 'A'; + + dllName[i] = (char)c; + } + + dllName[len] = 0; + +#ifdef DEBUG_very_verbose + printf_stderr("LdrLoadDll: dll name '%s'\n", dllName); +#endif + + if (!(sInitFlags & eDllBlocklistInitFlagWasBootstrapped)) { + // Block a suspicious binary that uses various 12-digit hex strings + // e.g. MovieMode.48CA2AEFA22D.dll (bug 973138) + dot = strchr(dllName, '.'); + if (dot && (strchr(dot + 1, '.') == dot + 13)) { + char* end = nullptr; + _strtoui64(dot + 1, &end, 16); + if (end == dot + 13) { + return STATUS_DLL_NOT_FOUND; + } + } + // Block binaries where the filename is at least 16 hex digits + if (dot && ((dot - dllName) >= 16)) { + char* current = dllName; + while (current < dot && isxdigit(*current)) { + current++; + } + if (current == dot) { + return STATUS_DLL_NOT_FOUND; + } + } + + // then compare to everything on the blocklist + DECLARE_POINTER_TO_FIRST_DLL_BLOCKLIST_ENTRY(info); + while (info->mName) { + if (strcmp(info->mName, dllName) == 0) break; + + info++; + } + + if (info->mName) { + bool load_ok = false; + +#ifdef DEBUG_very_verbose + printf_stderr("LdrLoadDll: info->mName: '%s'\n", info->mName); +#endif + + if (info->mFlags & DllBlockInfo::REDIRECT_TO_NOOP_ENTRYPOINT) { + printf_stderr( + "LdrLoadDll: " + "Ignoring the REDIRECT_TO_NOOP_ENTRYPOINT flag\n"); + } + + if ((info->mFlags & DllBlockInfo::BLOCK_WIN8_AND_OLDER) && + IsWin8Point1OrLater()) { + goto continue_loading; + } + + if ((info->mFlags & DllBlockInfo::BLOCK_WIN7_AND_OLDER) && + IsWin8OrLater()) { + goto continue_loading; + } + + if ((info->mFlags & DllBlockInfo::CHILD_PROCESSES_ONLY) && + !(sInitFlags & eDllBlocklistInitFlagIsChildProcess)) { + goto continue_loading; + } + + if ((info->mFlags & DllBlockInfo::UTILITY_PROCESSES_ONLY) && + !(sInitFlags & eDllBlocklistInitFlagIsUtilityProcess)) { + goto continue_loading; + } + + if ((info->mFlags & DllBlockInfo::SOCKET_PROCESSES_ONLY) && + !(sInitFlags & eDllBlocklistInitFlagIsSocketProcess)) { + goto continue_loading; + } + + if ((info->mFlags & DllBlockInfo::BROWSER_PROCESS_ONLY) && + (sInitFlags & eDllBlocklistInitFlagIsChildProcess)) { + goto continue_loading; + } + + unsigned long long fVersion = DllBlockInfo::ALL_VERSIONS; + + if (info->mMaxVersion != DllBlockInfo::ALL_VERSIONS) { + ReentrancySentinel sentinel(dllName); + if (sentinel.BailOut()) { + goto continue_loading; + } + + full_fname = getFullPath(filePath, fname); + if (!full_fname) { + // uh, we couldn't find the DLL at all, so... + printf_stderr( + "LdrLoadDll: Blocking load of '%s' (SearchPathW didn't find " + "it?)\n", + dllName); + return STATUS_DLL_NOT_FOUND; + } + + if (info->mFlags & DllBlockInfo::USE_TIMESTAMP) { + fVersion = GetTimestamp(full_fname.get()); + if (fVersion > info->mMaxVersion) { + load_ok = true; + } + } else { + LauncherResult<ModuleVersion> version = + GetModuleVersion(full_fname.get()); + // If we failed to get the version information, we block. + if (version.isOk()) { + load_ok = !info->IsVersionBlocked(version.unwrap()); + } + } + } + + if (!load_ok) { + printf_stderr( + "LdrLoadDll: Blocking load of '%s' -- see " + "http://www.mozilla.com/en-US/blocklist/\n", + dllName); + DllBlockSet::Add(info->mName, fVersion); + return STATUS_DLL_NOT_FOUND; + } + } + } + +continue_loading: +#ifdef DEBUG_very_verbose + printf_stderr("LdrLoadDll: continuing load... ('%S')\n", + moduleFileName->Buffer); +#endif + + glue::ModuleLoadFrame loadFrame(moduleFileName); + + NTSTATUS ret; + HANDLE myHandle; + + { +#if defined(_M_AMD64) || defined(_M_ARM64) + AutoSuppressStackWalking suppress; +#endif + ret = stub_LdrLoadDll(filePath, flags, moduleFileName, &myHandle); + } + + if (handle) { + *handle = myHandle; + } + + loadFrame.SetLoadStatus(ret, myHandle); + + return ret; +} + +#if defined(NIGHTLY_BUILD) +// Map of specific thread proc addresses we should block. In particular, +// LoadLibrary* APIs which indicate DLL injection +static void* gStartAddressesToBlock[4]; +#endif // defined(NIGHTLY_BUILD) + +static bool ShouldBlockThread(void* aStartAddress) { + // Allows crashfirefox.exe to continue to work. Also if your threadproc is + // null, this crash is intentional. + if (aStartAddress == nullptr) { + return false; + } + +#if defined(NIGHTLY_BUILD) + for (auto p : gStartAddressesToBlock) { + if (p == aStartAddress) { + return true; + } + } +#endif + + bool shouldBlock = false; + MEMORY_BASIC_INFORMATION startAddressInfo = {0}; + if (VirtualQuery(aStartAddress, &startAddressInfo, + sizeof(startAddressInfo))) { + shouldBlock |= startAddressInfo.State != MEM_COMMIT; + shouldBlock |= startAddressInfo.Protect != PAGE_EXECUTE_READ; + } + + return shouldBlock; +} + +// Allows blocked threads to still run normally through BaseThreadInitThunk, in +// case there's any magic there that we shouldn't skip. +static DWORD WINAPI NopThreadProc(void* /* aThreadParam */) { return 0; } + +static MOZ_NORETURN void __fastcall patched_BaseThreadInitThunk( + BOOL aIsInitialThread, void* aStartAddress, void* aThreadParam) { + if (ShouldBlockThread(aStartAddress)) { + aStartAddress = (void*)NopThreadProc; + } + + stub_BaseThreadInitThunk(aIsInitialThread, aStartAddress, aThreadParam); +} + +static WindowsDllInterceptor NtDllIntercept; +static WindowsDllInterceptor Kernel32Intercept; + +static void GetNativeNtBlockSetWriter(); + +static glue::LoaderObserver gMozglueLoaderObserver; +static nt::WinLauncherServices gWinLauncher; + +MFBT_API void DllBlocklist_Initialize(uint32_t aInitFlags) { + if (sBlocklistInitAttempted) { + return; + } + sBlocklistInitAttempted = true; + + sInitFlags = aInitFlags; + + glue::ModuleLoadFrame::StaticInit(&gMozglueLoaderObserver, &gWinLauncher); + +#ifdef _M_AMD64 + if (!IsWin8OrLater()) { + Kernel32Intercept.Init(L"kernel32.dll"); + // The crash that this hook works around is only seen on Win7. + stub_RtlInstallFunctionTableCallback.Set( + Kernel32Intercept, "RtlInstallFunctionTableCallback", + &patched_RtlInstallFunctionTableCallback); + } +#endif + + // Bug 1361410: WRusr.dll will overwrite our hook and cause a crash. + // Workaround: If we detect WRusr.dll, don't hook. + if (!GetModuleHandleW(L"WRusr.dll")) { + Kernel32Intercept.Init(L"kernel32.dll"); + if (!stub_BaseThreadInitThunk.SetDetour(Kernel32Intercept, + "BaseThreadInitThunk", + &patched_BaseThreadInitThunk)) { +#ifdef DEBUG + printf_stderr("BaseThreadInitThunk hook failed\n"); +#endif + } + } + +#if defined(NIGHTLY_BUILD) + // Populate a list of thread start addresses to block. + HMODULE hKernel = GetModuleHandleW(L"kernel32.dll"); + if (hKernel) { + void* pProc; + + pProc = (void*)GetProcAddress(hKernel, "LoadLibraryA"); + gStartAddressesToBlock[0] = pProc; + + pProc = (void*)GetProcAddress(hKernel, "LoadLibraryW"); + gStartAddressesToBlock[1] = pProc; + + pProc = (void*)GetProcAddress(hKernel, "LoadLibraryExA"); + gStartAddressesToBlock[2] = pProc; + + pProc = (void*)GetProcAddress(hKernel, "LoadLibraryExW"); + gStartAddressesToBlock[3] = pProc; + } +#endif + + if (aInitFlags & eDllBlocklistInitFlagWasBootstrapped) { + GetNativeNtBlockSetWriter(); + return; + } + + // There are a couple of exceptional cases where we skip user32.dll check. + // - If the the process was bootstrapped by the launcher process, AppInit + // DLLs will be intercepted by the new DllBlockList. No need to check + // here. + // - The code to initialize the base profiler loads winmm.dll which + // statically links user32.dll on an older Windows. This means if the base + // profiler is active before coming here, we cannot fully intercept AppInit + // DLLs. Given that the base profiler is used outside the typical use + // cases, it's ok not to check user32.dll in this scenario. + const bool skipUser32Check = + (sInitFlags & eDllBlocklistInitFlagWasBootstrapped) || + (!IsWin10AnniversaryUpdateOrLater() && + baseprofiler::profiler_is_active()); + + // In order to be effective against AppInit DLLs, the blocklist must be + // initialized before user32.dll is loaded into the process (bug 932100). + if (!skipUser32Check && GetModuleHandleW(L"user32.dll")) { + sUser32BeforeBlocklist = true; +#ifdef DEBUG + printf_stderr("DLL blocklist was unable to intercept AppInit DLLs.\n"); +#endif + } + + NtDllIntercept.Init("ntdll.dll"); + + ReentrancySentinel::InitializeStatics(); + + // We specifically use a detour, because there are cases where external + // code also tries to hook LdrLoadDll, and doesn't know how to relocate our + // nop space patches. (Bug 951827) + bool ok = stub_LdrLoadDll.SetDetour(NtDllIntercept, "LdrLoadDll", + &patched_LdrLoadDll); + + if (!ok) { + sBlocklistInitFailed = true; +#ifdef DEBUG + printf_stderr("LdrLoadDll hook failed, no dll blocklisting active\n"); +#endif + } + + // If someone injects a thread early that causes user32.dll to load off the + // main thread this causes issues, so load it as soon as we've initialized + // the block-list. (See bug 1400637) + if (!sUser32BeforeBlocklist && !IsWin32kLockedDown()) { + ::LoadLibraryW(L"user32.dll"); + } +} + +#ifdef DEBUG +MFBT_API void DllBlocklist_Shutdown() {} +#endif // DEBUG + +static void InternalWriteNotes(AnnotationWriter& aWriter) { + WritableBuffer buffer; + DllBlockSet::Write(buffer); + + aWriter.Write(Annotation::BlockedDllList, buffer.Data(), buffer.Length()); + + if (sBlocklistInitFailed) { + aWriter.Write(Annotation::BlocklistInitFailed, "1"); + } + + if (sUser32BeforeBlocklist) { + aWriter.Write(Annotation::User32BeforeBlocklist, "1"); + } +} + +using WriterFn = void (*)(AnnotationWriter&); +static WriterFn gWriterFn = &InternalWriteNotes; + +static void GetNativeNtBlockSetWriter() { + auto nativeWriter = reinterpret_cast<WriterFn>( + ::GetProcAddress(::GetModuleHandleW(nullptr), "NativeNtBlockSet_Write")); + if (nativeWriter) { + gWriterFn = nativeWriter; + } +} + +MFBT_API void DllBlocklist_WriteNotes(AnnotationWriter& aWriter) { + MOZ_ASSERT(gWriterFn); + gWriterFn(aWriter); +} + +MFBT_API bool DllBlocklist_CheckStatus() { + if (sBlocklistInitFailed || sUser32BeforeBlocklist) return false; + return true; +} + +// ============================================================================ +// This section is for DLL Services +// ============================================================================ + +namespace mozilla { +Authenticode* GetAuthenticode(); +} // namespace mozilla + +/** + * Please note that DllBlocklist_SetFullDllServices is called with + * aSvc = nullptr to de-initialize the resources even though they + * have been initialized via DllBlocklist_SetBasicDllServices. + */ +MFBT_API void DllBlocklist_SetFullDllServices( + mozilla::glue::detail::DllServicesBase* aSvc) { + glue::AutoExclusiveLock lock(gDllServicesLock); + if (aSvc) { + aSvc->SetAuthenticodeImpl(GetAuthenticode()); + aSvc->SetWinLauncherServices(gWinLauncher); + gMozglueLoaderObserver.Forward(aSvc); + } + + gDllServices = aSvc; +} + +MFBT_API void DllBlocklist_SetBasicDllServices( + mozilla::glue::detail::DllServicesBase* aSvc) { + if (!aSvc) { + return; + } + + aSvc->SetAuthenticodeImpl(GetAuthenticode()); + gMozglueLoaderObserver.Disable(); +} diff --git a/toolkit/xre/dllservices/mozglue/WindowsDllBlocklist.h b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklist.h new file mode 100644 index 0000000000..d67f49a17e --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklist.h @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_windowsdllblocklist_h +#define mozilla_windowsdllblocklist_h + +#if (defined(_MSC_VER) || defined(__MINGW32__)) && \ + (defined(_M_IX86) || defined(_M_X64) || defined(_M_ARM64)) + +# include <windows.h> +# include "CrashAnnotations.h" +# include "mozilla/Attributes.h" +# include "mozilla/Types.h" + +# define HAS_DLL_BLOCKLIST + +enum DllBlocklistInitFlags { + eDllBlocklistInitFlagDefault = 0, + eDllBlocklistInitFlagIsChildProcess = 1, + eDllBlocklistInitFlagWasBootstrapped = 2, + eDllBlocklistInitFlagIsUtilityProcess = 4, + eDllBlocklistInitFlagIsSocketProcess = 8 +}; + +// Only available from within firefox.exe +# if !defined(IMPL_MFBT) && !defined(MOZILLA_INTERNAL_API) +extern uint32_t gBlocklistInitFlags; +# endif // !defined(IMPL_MFBT) && !defined(MOZILLA_INTERNAL_API) + +MFBT_API void DllBlocklist_Initialize( + uint32_t aInitFlags = eDllBlocklistInitFlagDefault); +MFBT_API void DllBlocklist_WriteNotes(CrashReporter::AnnotationWriter& aWriter); +MFBT_API bool DllBlocklist_CheckStatus(); + +// This export intends to clean up after DllBlocklist_Initialize(). +// It's disabled in release builds for performance and to limit callers' ability +// to interfere with dll blocking. +# ifdef DEBUG +MFBT_API void DllBlocklist_Shutdown(); +# endif // DEBUG + +namespace mozilla { +namespace glue { +namespace detail { +// Forward declaration +class DllServicesBase; + +template <size_t N> +class WritableBuffer { + char mBuffer[N]; + size_t mLen; + + size_t Available() const { return sizeof(mBuffer) - mLen; } + + public: + WritableBuffer() : mBuffer{0}, mLen(0) {} + + void Write(const char* aData, size_t aLen) { + size_t writable_len = std::min(aLen, Available()); + memcpy(mBuffer + mLen, aData, writable_len); + mLen += writable_len; + } + + size_t Length() const { return mLen; } + const char* Data() const { return mBuffer; } +}; +} // namespace detail +} // namespace glue +} // namespace mozilla + +MFBT_API void DllBlocklist_SetFullDllServices( + mozilla::glue::detail::DllServicesBase* aSvc); +MFBT_API void DllBlocklist_SetBasicDllServices( + mozilla::glue::detail::DllServicesBase* aSvc); + +#endif // defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64)) +#endif // mozilla_windowsdllblocklist_h diff --git a/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistCommon.h b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistCommon.h new file mode 100644 index 0000000000..b9f176ac02 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistCommon.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_WindowsDllBlocklistCommon_h +#define mozilla_WindowsDllBlocklistCommon_h + +#include "mozilla/ArrayUtils.h" +#include "mozilla/WindowsDllBlocklistInfo.h" + +#if !defined(DLL_BLOCKLIST_STRING_TYPE) +# error "You must define DLL_BLOCKLIST_STRING_TYPE" +#endif // !defined(DLL_BLOCKLIST_STRING_TYPE) + +#define DLL_BLOCKLIST_DEFINITIONS_BEGIN_NAMED(name) \ + using DllBlockInfo = mozilla::DllBlockInfoT<DLL_BLOCKLIST_STRING_TYPE>; \ + static const DllBlockInfo name[] = { +#define DLL_BLOCKLIST_DEFINITIONS_BEGIN \ + DLL_BLOCKLIST_DEFINITIONS_BEGIN_NAMED(gWindowsDllBlocklist) + +#define DLL_BLOCKLIST_DEFINITIONS_END \ + {} \ + } \ + ; + +#define DECLARE_POINTER_TO_FIRST_DLL_BLOCKLIST_ENTRY_FOR(name, list) \ + const DllBlockInfo* name = &list[0] + +#define DECLARE_POINTER_TO_FIRST_DLL_BLOCKLIST_ENTRY(name) \ + DECLARE_POINTER_TO_FIRST_DLL_BLOCKLIST_ENTRY_FOR(name, gWindowsDllBlocklist) + +#define DECLARE_POINTER_TO_LAST_DLL_BLOCKLIST_ENTRY_FOR(name, list) \ + const DllBlockInfo* name = &list[mozilla::ArrayLength(list) - 1] + +#define DECLARE_POINTER_TO_LAST_DLL_BLOCKLIST_ENTRY(name) \ + DECLARE_POINTER_TO_LAST_DLL_BLOCKLIST_ENTRY_FOR(name, gWindowsDllBlocklist) + +#define DECLARE_DLL_BLOCKLIST_NUM_ENTRIES_FOR(name, list) \ + const size_t name = mozilla::ArrayLength(list) - 1 + +#define DECLARE_DLL_BLOCKLIST_NUM_ENTRIES(name) \ + DECLARE_DLL_BLOCKLIST_NUM_ENTRIES_FOR(name, gWindowsDllBlocklist) + +#endif // mozilla_WindowsDllBlocklistCommon_h diff --git a/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistDefs.in b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistDefs.in new file mode 100644 index 0000000000..99360930ca --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistDefs.in @@ -0,0 +1,356 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# This file exposes five lists: +# ALL_PROCESSES, BROWSER_PROCESS, CHILD_PROCESSES, UTILITY_PROCESSES, +# and SOCKET_PROCESSES +# +# In addition, each of those lists supports a special variant for test-only +# entries: +# ALL_PROCESSES_TESTS, BROWSER_PROCESS_TESTS, CHILD_PROCESSES_TESTS, +# UTILITY_PROCESSES_TESTS, and SOCKET_PROCESSES_TESTS +# +# Choose the list that is applicable to the applicable process type(s) for your +# DLL block. +# +# The currently supported blocklist entry types are: +# DllBlocklistEntry, A11yBlocklistEntry, LspBlocklistEntry, +# RedirectToNoOpEntryPoint +# (See gen_dll_blocklist_defs.py for their documentation.) +# +# Example: +# ALL_PROCESSES += [ +# DllBlocklistEntry("foo.dll", (1, 2, 3, 4)), +# DllBlocklistEntry("foo.dll", ALL_VERSIONS), +# DllBlocklistEntry("foo.dll", UNVERSIONED), +# DllBlocklistEntry("foo.dll", 0x0000123400000000), +# DllBlocklistEntry("foo.dll", PETimeStamp(0x12345678)), +# ] +# +# The version parameter the "last bad" version, that is, we block anything that +# is less-than or equal to that version. + +ALL_PROCESSES += [ + # NPFFAddon - Known malware + DllBlocklistEntry("npffaddon.dll", ALL_VERSIONS), + + # AVG 8 - Antivirus vendor AVG, old version, plugin already blocklisted + DllBlocklistEntry("avgrsstx.dll", (8,5,0,401)), + + # calc.dll - Suspected malware + DllBlocklistEntry("calc.dll", (1,0,0,1)), + + # hook.dll - Suspected malware + DllBlocklistEntry("hook.dll", ALL_VERSIONS), + + # GoogleDesktopNetwork3.dll - Extremely old, unversioned instances + # of this DLL cause crashes + DllBlocklistEntry("googledesktopnetwork3.dll", UNVERSIONED), + + # rdolib.dll - Suspected malware + DllBlocklistEntry("rdolib.dll", (6,0,88,4)), + + # fgjk4wvb.dll - Suspected malware + DllBlocklistEntry("fgjk4wvb.dll", (8,8,8,8)), + + # radhslib.dll - Naomi internet filter - unmaintained since 2006 + DllBlocklistEntry("radhslib.dll", UNVERSIONED), + + # Music download filter for vkontakte.ru - old instances + # of this DLL cause crashes + DllBlocklistEntry("vksaver.dll", (2,2,2,0)), + + # Topcrash in Firefox 4.0b1 + DllBlocklistEntry("rlxf.dll", (1,2,323,1)), + + # psicon.dll - Topcrashes in Thunderbird, and some crashes in Firefox + # Adobe photoshop library, now redundant in later installations + DllBlocklistEntry("psicon.dll", ALL_VERSIONS), + + # Topcrash in Firefox 4 betas (bug 618899), + DllBlocklistEntry("accelerator.dll", (3,2,1,6)), + + # Topcrash with Roboform in Firefox 8 (bug 699134), + DllBlocklistEntry("rf-firefox.dll", (7,6,1,0)), + DllBlocklistEntry("roboform.dll", (7,6,1,0)), + + # Topcrash with Babylon Toolbar on FF16+ (bug 721264), + DllBlocklistEntry("babyfox.dll", ALL_VERSIONS), + + # sprotector.dll crashes, bug 957258 + DllBlocklistEntry("sprotector.dll", ALL_VERSIONS), + + # Windows Media Foundation FLAC decoder and type sniffer (bug 839031). + DllBlocklistEntry("mfflac.dll", ALL_VERSIONS), + + # Older Relevant Knowledge DLLs cause us to crash (bug 904001). + DllBlocklistEntry("rlnx.dll", (1, 3, 334, 9)), + DllBlocklistEntry("pmnx.dll", (1, 3, 334, 9)), + DllBlocklistEntry("opnx.dll", (1, 3, 334, 9)), + DllBlocklistEntry("prnx.dll", (1, 3, 334, 9)), + + # Older belgian ID card software causes Firefox to crash or hang on + # shutdown, bug 831285 and 918399. + DllBlocklistEntry("beid35cardlayer.dll", (3, 5, 6, 6968)), + + # bug 925459, bitguard crashes + DllBlocklistEntry("bitguard.dll", ALL_VERSIONS), + + # bug 812683 - crashes in Windows library when Asus Gamer OSD is installed + # Software is discontinued/unsupported + DllBlocklistEntry("atkdx11disp.dll", ALL_VERSIONS), + + # Topcrash with Conduit SearchProtect, bug 944542 + DllBlocklistEntry("spvc32.dll", ALL_VERSIONS), + + # Topcrash with V-bates, bug 1002748 and bug 1023239 + DllBlocklistEntry("libinject.dll", UNVERSIONED), + DllBlocklistEntry("libinject2.dll", PETimeStamp(0x537DDC93)), + DllBlocklistEntry("libredir2.dll", PETimeStamp(0x5385B7ED)), + + # Crashes with RoboForm2Go written against old SDK, bug 988311/1196859 + DllBlocklistEntry("rf-firefox-22.dll", ALL_VERSIONS), + DllBlocklistEntry("rf-firefox-40.dll", ALL_VERSIONS), + + # Crashes with DesktopTemperature, bug 1046382 + DllBlocklistEntry("dtwxsvc.dll", PETimeStamp(0x53153234)), + + # Startup crashes with Lenovo Onekey Theater, bug 1123778 + DllBlocklistEntry("activedetect32.dll", UNVERSIONED), + DllBlocklistEntry("activedetect64.dll", UNVERSIONED), + DllBlocklistEntry("windowsapihookdll32.dll", UNVERSIONED), + DllBlocklistEntry("windowsapihookdll64.dll", UNVERSIONED), + + # Flash crashes with RealNetworks RealDownloader, bug 1132663 + DllBlocklistEntry("rndlnpshimswf.dll", ALL_VERSIONS), + DllBlocklistEntry("rndlmainbrowserrecordplugin.dll", ALL_VERSIONS), + + # Startup crashes with RealNetworks Browser Record Plugin, bug 1170141 + DllBlocklistEntry("nprpffbrowserrecordext.dll", ALL_VERSIONS), + DllBlocklistEntry("nprndlffbrowserrecordext.dll", ALL_VERSIONS), + + # Crashes with CyberLink YouCam, bug 1136968 + DllBlocklistEntry("ycwebcamerasource.ax", (2, 0, 0, 1611)), + + # Old version of WebcamMax crashes WebRTC, bug 1130061 + DllBlocklistEntry("vwcsource.ax", (1, 5, 0, 0)), + + # NetOp School, discontinued product, bug 763395 + DllBlocklistEntry("nlsp.dll", (6, 23, 2012, 19)), + + # Orbit Downloader, bug 1222819 + DllBlocklistEntry("grabdll.dll", (2, 6, 1, 0)), + DllBlocklistEntry("grabkernel.dll", (1, 0, 0, 1)), + + # ESET, bug 1229252 + DllBlocklistEntry("eoppmonitor.dll", ALL_VERSIONS), + + # SS2OSD, bug 1262348 + DllBlocklistEntry("ss2osd.dll", ALL_VERSIONS), + DllBlocklistEntry("ss2devprops.dll", ALL_VERSIONS), + + # NHASUSSTRIXOSD.DLL, bug 1269244 + DllBlocklistEntry("nhasusstrixosd.dll", ALL_VERSIONS), + DllBlocklistEntry("nhasusstrixdevprops.dll", ALL_VERSIONS), + + # Crashes with PremierOpinion/RelevantKnowledge, bug 1277846 + DllBlocklistEntry("opls.dll", ALL_VERSIONS), + DllBlocklistEntry("opls64.dll", ALL_VERSIONS), + DllBlocklistEntry("pmls.dll", ALL_VERSIONS), + DllBlocklistEntry("pmls64.dll", ALL_VERSIONS), + DllBlocklistEntry("prls.dll", ALL_VERSIONS), + DllBlocklistEntry("prls64.dll", ALL_VERSIONS), + DllBlocklistEntry("rlls.dll", ALL_VERSIONS), + DllBlocklistEntry("rlls64.dll", ALL_VERSIONS), + + # Vorbis DirectShow filters, bug 1239690. + DllBlocklistEntry("vorbis.acm", (0, 0, 3, 6)), + + # AhnLab Internet Security, bug 1311969 + DllBlocklistEntry("nzbrcom.dll", ALL_VERSIONS), + + # K7TotalSecurity, bug 1339083 and bug 1694489. + DllBlocklistEntry("k7pswsen.dll", (15, 2, 2, 102)), + + # smci*.dll - goobzo crashware (bug 1339908), + DllBlocklistEntry("smci32.dll", ALL_VERSIONS), + DllBlocklistEntry("smci64.dll", ALL_VERSIONS), + + # Crashes with Internet Download Manager, bug 1333486 + DllBlocklistEntry("idmcchandler7.dll", ALL_VERSIONS), + DllBlocklistEntry("idmcchandler7_64.dll", ALL_VERSIONS), + DllBlocklistEntry("idmcchandler5.dll", ALL_VERSIONS), + DllBlocklistEntry("idmcchandler5_64.dll", ALL_VERSIONS), + + # Nahimic 2 breaks applicaton update (bug 1356637), + DllBlocklistEntry("nahimic2devprops.dll", (2, 5, 19, 0xffff)), + # Nahimic is causing crashes, bug 1233556 + DllBlocklistEntry("nahimicmsiosd.dll", UNVERSIONED), + # Nahimic is causing crashes, bug 1360029 + DllBlocklistEntry("nahimicvrdevprops.dll", UNVERSIONED), + DllBlocklistEntry("nahimic2osd.dll", (2, 5, 19, 0xffff)), + DllBlocklistEntry("nahimicmsidevprops.dll", UNVERSIONED), + + # Bug 1268470 - crashes with Kaspersky Lab on Windows 8 + DllBlocklistEntry("klsihk64.dll", (14, 0, 456, 0xffff), + BLOCK_WIN8_AND_OLDER), + + # Bug 1579758, crashes with OpenSC nightly version 0.19.0.448 and lower + DllBlocklistEntry("onepin-opensc-pkcs11.dll", (0, 19, 0, 448)), + + # Avecto Privilege Guard causes crashes, bug 1385542 + DllBlocklistEntry("pghook.dll", ALL_VERSIONS), + + # Old versions of G DATA BankGuard, bug 1421991 + DllBlocklistEntry("banksafe64.dll", (1, 2, 15299, 65535)), + + # Old versions of G DATA, bug 1043775 + DllBlocklistEntry("gdkbfltdll64.dll", (1, 0, 14141, 240)), + + # Dell Backup and Recovery tool causes crashes, bug 1433408 + DllBlocklistEntry("dbroverlayiconnotbackuped.dll", (1, 8, 0, 9)), + DllBlocklistEntry("dbroverlayiconbackuped.dll", (1, 8, 0, 9)), + + # NVIDIA nView Desktop Management causes crashes, bug 1465787 + DllBlocklistEntry("nviewh64.dll", (6, 14, 10, 14847)), + + # Ivanti Endpoint Security, bug 1553776 + DllBlocklistEntry("sxwmon.dll", ALL_VERSIONS), + DllBlocklistEntry("sxwmon64.dll", ALL_VERSIONS), + + # 360 Safeguard/360 Total Security causes a11y crashes, bug 1536227. + DllBlocklistEntry("safemon64.dll", ALL_VERSIONS), + + # Old versions of Digital Guardian, bug 1318858, bug 1603974, + # and bug 1672367 + RedirectToNoOpEntryPoint("dgapi.dll", (7, 5, 0xffff, 0xffff)), + RedirectToNoOpEntryPoint("dgapi64.dll", (7, 5, 0xffff, 0xffff)), + + # Old versions of COMODO Internet Security, bug 1608048 + DllBlocklistEntry("IseGuard32.dll", (1, 6, 13835, 184)), + DllBlocklistEntry("IseGuard64.dll", (1, 6, 13835, 184)), + + # Old version of COMODO Firewall, bug 1407712 and bug 1624336 + DllBlocklistEntry("guard64.dll", (8, 4, 0, 65535)), + + # Old version of Panda Security, bug 1637984 and bug 1705125 + DllBlocklistEntry("PavLspHook64.dll", (9, 2, 2, 1), BLOCK_WIN7_AND_OLDER), + DllBlocklistEntry("PavSHook64.dll", (9, 2, 2, 1), BLOCK_WIN7_AND_OLDER), + + # Webroot SecureAnywhere causes crashes, bug 1700281 + DllBlocklistEntry("WRDll.x64.dll", (1, 1, 0, 227)), + DllBlocklistEntry("WRDll.x86.dll", (1, 1, 0, 227)), + + # Webroot SecureAnywhere causes deadlocks, bug 1752466 + DllBlocklistEntry("WRusr.dll", (9, 0, 32, 49)), + + # InfoWatch Device Monitor causes crashes, bug 1704276 + DllBlocklistEntry("iwprn.dll", (6, 9, 11, 360)), + DllBlocklistEntry("iwprn_x86.dll", (6, 9, 11, 360)), + + # Forcepoint DLLs causing crashes, bug 1767993 + DllBlocklistEntry("qipcap.dll", (7, 7, 819, 1)), + DllBlocklistEntry("qipcap64.dll", (7, 7, 819, 1)), + + # Cylance, bug 1756190 and bug 1799562 + DllBlocklistEntry("CylanceMemDef64.dll", ALL_VERSIONS), +] + +ALL_PROCESSES_TESTS += [ + # DLLs used by TestDllBlocklist* gTests + DllBlocklistEntry("testdllblocklist_matchbyname.dll", ALL_VERSIONS), + DllBlocklistEntry("testdllblocklist_matchbyversion.dll", (5, 5, 5, 5)), + DllBlocklistEntry("testdllblocklist_allowbyversion.dll", (5, 5, 5, 5)), + RedirectToNoOpEntryPoint("testdllblocklist_noopentrypoint.dll", + (5, 5, 5, 5)), +] + +BROWSER_PROCESS += [ + # RealPlayer, bug 1418535, bug 1437417 + # Versions before 18.1.11.0 cause severe performance problems. + A11yBlocklistEntry("dtvhooks.dll", (18, 1, 10, 0xffff)), + A11yBlocklistEntry("dtvhooks64.dll", (18, 1, 10, 0xffff)), + + # SolidWorks Windows Explorer integration causes crashes, bug 1566109 + # and bug 1468250 + DllBlocklistEntry("Database.dll", ALL_VERSIONS), + + # Hancom Office shell extension causes crashes when the file picker is + # opened. See bug 1581092. + DllBlocklistEntry("hncshellext64.dll", (1, 0, 0 ,3)), + + # Cambridge Silicon Radio, bug 1634538 + DllBlocklistEntry("BLEtokenCredentialProvider.dll", (2, 1, 63, 0)), + + # FYunZip and PuddingZip, loaded as shell extension, cause crashes + # bug 1576728 + DllBlocklistEntry("oly64.dll", (1, 1, 3, 19920)), + DllBlocklistEntry("oly.dll", (1, 1, 3, 19920)), + DllBlocklistEntry("pdzipmenu64.dll", (1, 4, 4, 20103)), + DllBlocklistEntry("pdzipmenu32.dll", (1, 4, 4, 20103)), + + # McAfee Data Loss Prevention causes crashs with multiple signatures, + # bug 1634090, bug 1717676 + DllBlocklistEntry("fcagff.dll", (11, 6, 300, 2)), + DllBlocklistEntry("fcagff64.dll", (11, 6, 300, 2)), + + # AVerMedia's virtual camera module causes crashes (bug 1692908) + DllBlocklistEntry("avmvirtualsource.ax", (1, 0, 0, 3)), + + # Avast (bug 1794064) + DllBlocklistEntry("aswJsFlt.dll", (18, 0, 1473, 0)), + + # ASUS Web Storage (bug 1714940) + DllBlocklistEntry("asuswsshellext64.dll", (1, 1, 0, 27)), + + # ExplorerPatcher (bug 1798707) + DllBlocklistEntry("explorerpatcher.amd64.dll", ALL_VERSIONS), +] + +CHILD_PROCESSES += [ + # Causes crashes in the GPU process with WebRender enabled, bug 1544435 + DllBlocklistEntry("wbload.dll", ALL_VERSIONS), + + # Causes crashes in the content process with win32k lockdown, bug 1766022 + DllBlocklistEntry("videocapturer.dll", ALL_VERSIONS), + DllBlocklistEntry("videocapturerhk32.dll", ALL_VERSIONS), + DllBlocklistEntry("videocapturerhk64.dll", ALL_VERSIONS), + + # Causes crashes in the content process with win32k lockdown, bug 1766029 + # The crash seems to be in the *_m module which has been unloaded. + DllBlocklistEntry("safaweb.dll", (3, 0, 21, 3181)), + DllBlocklistEntry("safaweb_m.dll", (3, 0, 21, 3181)), + DllBlocklistEntry("safaweb64.dll", (3, 0, 21, 3181)), + DllBlocklistEntry("safaweb64_m.dll", (3, 0, 21, 3181)), + + # Causes crashes in the content process with win32k lockdown, bug 1769309 + DllBlocklistEntry("hmpalert.dll", (3, 8, 8, 889)), +] + +SOCKET_PROCESSES += [ + # Causes crashes in the socket process, bug 1760668 + DllBlocklistEntry("ipseng64.dll", (17, 2, 6, 25)), + DllBlocklistEntry("ipseng32.dll", (17, 2, 6, 25)), + + # Causes crashes in the socket process, bug 1807038 + DllBlocklistEntry("kwsui64.dll", (2022, 2, 8, 125)), +] + +SOCKET_PROCESSES_TESTS += [ + # DLLs used by TestDllBlocklist* gTests + DllBlocklistEntry("testdllblocklist_socketprocessonly.dll", ALL_VERSIONS), +] + +UTILITY_PROCESSES += [ + # Generated dynamic code that we block + DllBlocklistEntry("cyinjct.dll", ALL_VERSIONS), +] + +UTILITY_PROCESSES_TESTS += [ + # DLLs used by TestDllBlocklist* gTests + DllBlocklistEntry("testdllblocklist_utilityprocessonly.dll", ALL_VERSIONS), +] + diff --git a/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistInfo.h b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistInfo.h new file mode 100644 index 0000000000..c808e49ca1 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistInfo.h @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_WindowsDllBlocklistInfo_h +#define mozilla_WindowsDllBlocklistInfo_h + +#include <stdint.h> + +namespace mozilla { + +template <typename StrType> +struct DllBlockInfoT { + // The name of the DLL -- in LOWERCASE! It will be compared to + // a lowercase version of the DLL name only. + StrType mName; + + // If mMaxVersion is ALL_VERSIONS, we'll block all versions of this + // dll. Otherwise, we'll block all versions less than or equal to + // the given version, as queried by GetFileVersionInfo and + // VS_FIXEDFILEINFO's dwFileVersionMS and dwFileVersionLS fields. + // + // Note that the version is usually 4 components, which is A.B.C.D + // encoded as 0x AAAA BBBB CCCC DDDD ULL (spaces added for clarity), + // but it's not required to be of that format. + uint64_t mMaxVersion; + + // If the USE_TIMESTAMP flag is set, then we use the timestamp from + // the IMAGE_FILE_HEADER in lieu of a version number. + // + // If the UTILITY_PROCESSES_ONLY or SOCKET_PROCESSES_ONLY flags are + // set, the dll will only be blocked for these specific child + // processes. Note that these are a subset of CHILD_PROCESSES_ONLY. + enum Flags { + FLAGS_DEFAULT = 0, + BLOCK_WIN7_AND_OLDER = 1 << 0, + BLOCK_WIN8_AND_OLDER = 1 << 1, + USE_TIMESTAMP = 1 << 2, + CHILD_PROCESSES_ONLY = 1 << 3, + BROWSER_PROCESS_ONLY = 1 << 4, + REDIRECT_TO_NOOP_ENTRYPOINT = 1 << 5, + UTILITY_PROCESSES_ONLY = 1 << 6, + SOCKET_PROCESSES_ONLY = 1 << 7, + } mFlags; + + bool IsVersionBlocked(const uint64_t aOther) const { + if (mMaxVersion == ALL_VERSIONS) { + return true; + } + + return aOther <= mMaxVersion; + } + + bool IsValidDynamicBlocklistEntry() const; + + static const uint64_t ALL_VERSIONS = (uint64_t)-1LL; + + // DLLs sometimes ship without a version number, particularly early + // releases. Blocking "version <= 0" has the effect of blocking unversioned + // DLLs (since the call to get version info fails), but not blocking + // any versioned instance. + static const uint64_t UNVERSIONED = 0ULL; +}; + +} // namespace mozilla + +// Convert the 4 (decimal) components of a DLL version number into a +// single unsigned long long, as needed by the blocklist +#if defined(_MSC_VER) && !defined(__clang__) + +// MSVC does not properly handle the constexpr MAKE_VERSION, so we use a macro +// instead (ugh). +# define MAKE_VERSION(a, b, c, d) \ + ((a##ULL << 48) + (b##ULL << 32) + (c##ULL << 16) + d##ULL) + +#else + +static inline constexpr uint64_t MAKE_VERSION(uint16_t a, uint16_t b, + uint16_t c, uint16_t d) { + return static_cast<uint64_t>(a) << 48 | static_cast<uint64_t>(b) << 32 | + static_cast<uint64_t>(c) << 16 | static_cast<uint64_t>(d); +} + +#endif + +#endif // mozilla_WindowsDllBlocklistInfo_h diff --git a/toolkit/xre/dllservices/mozglue/WindowsDllServices.h b/toolkit/xre/dllservices/mozglue/WindowsDllServices.h new file mode 100644 index 0000000000..17e85991e5 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/WindowsDllServices.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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glue_WindowsDllServices_h +#define mozilla_glue_WindowsDllServices_h + +#include <utility> + +#include "mozilla/Assertions.h" +#include "mozilla/Authenticode.h" +#include "mozilla/LoaderAPIInterfaces.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Vector.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "mozilla/WindowsDllBlocklist.h" +#include "mozilla/mozalloc.h" + +#if defined(MOZILLA_INTERNAL_API) +# include "MainThreadUtils.h" +# include "nsISupportsImpl.h" +# include "nsString.h" +# include "nsThreadUtils.h" +# include "prthread.h" +# include "mozilla/SchedulerGroup.h" +#endif // defined(MOZILLA_INTERNAL_API) + +// For PCUNICODE_STRING +#include <winternl.h> + +namespace mozilla { +namespace glue { +namespace detail { + +class DllServicesBase : public Authenticode { + public: + /** + * WARNING: This method is called from within an unsafe context that holds + * multiple locks inside the Windows loader. The only thing that + * this function should be used for is dispatching the event to our + * event loop so that it may be handled in a safe context. + */ + virtual void DispatchDllLoadNotification(ModuleLoadInfo&& aModLoadInfo) = 0; + + /** + * This function accepts module load events to be processed later for + * the untrusted modules telemetry ping. + * + * WARNING: This method is run from within the Windows loader and should + * only perform trivial, loader-friendly operations. + */ + virtual void DispatchModuleLoadBacklogNotification( + ModuleLoadInfoVec&& aEvents) = 0; + + void SetAuthenticodeImpl(Authenticode* aAuthenticode) { + mAuthenticode = aAuthenticode; + } + + void SetWinLauncherServices(const nt::WinLauncherServices& aWinLauncher) { + mWinLauncher = aWinLauncher; + } + + template <typename... Args> + LauncherVoidResultWithLineInfo InitDllBlocklistOOP(Args&&... aArgs) { + MOZ_RELEASE_ASSERT(mWinLauncher.mInitDllBlocklistOOP); + return mWinLauncher.mInitDllBlocklistOOP(std::forward<Args>(aArgs)...); + } + + template <typename... Args> + void HandleLauncherError(Args&&... aArgs) { + MOZ_RELEASE_ASSERT(mWinLauncher.mHandleLauncherError); + mWinLauncher.mHandleLauncherError(std::forward<Args>(aArgs)...); + } + + nt::SharedSection* GetSharedSection() { return mWinLauncher.mSharedSection; } + + // In debug builds we override GetBinaryOrgName to add a Gecko-specific + // assertion. OTOH, we normally do not want people overriding this function, + // so we'll make it final in the release case, thus covering all bases. +#if defined(DEBUG) + UniquePtr<wchar_t[]> GetBinaryOrgName( + const wchar_t* aFilePath, + AuthenticodeFlags aFlags = AuthenticodeFlags::Default) override +#else + UniquePtr<wchar_t[]> GetBinaryOrgName( + const wchar_t* aFilePath, + AuthenticodeFlags aFlags = AuthenticodeFlags::Default) final +#endif // defined(DEBUG) + { + if (!mAuthenticode) { + return nullptr; + } + + return mAuthenticode->GetBinaryOrgName(aFilePath, aFlags); + } + + virtual void DisableFull() { DllBlocklist_SetFullDllServices(nullptr); } + + DllServicesBase(const DllServicesBase&) = delete; + DllServicesBase(DllServicesBase&&) = delete; + DllServicesBase& operator=(const DllServicesBase&) = delete; + DllServicesBase& operator=(DllServicesBase&&) = delete; + + protected: + DllServicesBase() : mAuthenticode(nullptr) {} + + virtual ~DllServicesBase() = default; + + void EnableFull() { DllBlocklist_SetFullDllServices(this); } + void EnableBasic() { DllBlocklist_SetBasicDllServices(this); } + + private: + Authenticode* mAuthenticode; + nt::WinLauncherServices mWinLauncher; +}; + +} // namespace detail + +#if defined(MOZILLA_INTERNAL_API) + +struct EnhancedModuleLoadInfo final { + explicit EnhancedModuleLoadInfo(ModuleLoadInfo&& aModLoadInfo) + : mNtLoadInfo(std::move(aModLoadInfo)) { + // Only populate mThreadName when we're on the same thread as the event + if (mNtLoadInfo.mThreadId == ::GetCurrentThreadId()) { + mThreadName = PR_GetThreadName(PR_GetCurrentThread()); + } + MOZ_ASSERT(!mNtLoadInfo.mSectionName.IsEmpty()); + } + + EnhancedModuleLoadInfo(EnhancedModuleLoadInfo&&) = default; + EnhancedModuleLoadInfo& operator=(EnhancedModuleLoadInfo&&) = default; + + EnhancedModuleLoadInfo(const EnhancedModuleLoadInfo&) = delete; + EnhancedModuleLoadInfo& operator=(const EnhancedModuleLoadInfo&) = delete; + + nsDependentString GetSectionName() const { + return mNtLoadInfo.mSectionName.AsString(); + } + + using BacktraceType = decltype(ModuleLoadInfo::mBacktrace); + + ModuleLoadInfo mNtLoadInfo; + nsCString mThreadName; +}; + +class DllServices : public detail::DllServicesBase { + public: + void DispatchDllLoadNotification(ModuleLoadInfo&& aModLoadInfo) final { + nsCOMPtr<nsIRunnable> runnable( + NewRunnableMethod<StoreCopyPassByRRef<EnhancedModuleLoadInfo>>( + "DllServices::NotifyDllLoad", this, &DllServices::NotifyDllLoad, + std::move(aModLoadInfo))); + + SchedulerGroup::Dispatch(TaskCategory::Other, runnable.forget()); + } + + void DispatchModuleLoadBacklogNotification( + ModuleLoadInfoVec&& aEvents) final { + nsCOMPtr<nsIRunnable> runnable( + NewRunnableMethod<StoreCopyPassByRRef<ModuleLoadInfoVec>>( + "DllServices::NotifyModuleLoadBacklog", this, + &DllServices::NotifyModuleLoadBacklog, std::move(aEvents))); + + SchedulerGroup::Dispatch(TaskCategory::Other, runnable.forget()); + } + +# if defined(DEBUG) + UniquePtr<wchar_t[]> GetBinaryOrgName( + const wchar_t* aFilePath, + AuthenticodeFlags aFlags = AuthenticodeFlags::Default) final { + // This function may perform disk I/O, so we should never call it on the + // main thread. + MOZ_ASSERT(!NS_IsMainThread()); + return detail::DllServicesBase::GetBinaryOrgName(aFilePath, aFlags); + } +# endif // defined(DEBUG) + + NS_INLINE_DECL_THREADSAFE_VIRTUAL_REFCOUNTING(DllServices) + + protected: + DllServices() = default; + ~DllServices() = default; + + virtual void NotifyDllLoad(EnhancedModuleLoadInfo&& aModLoadInfo) = 0; + virtual void NotifyModuleLoadBacklog(ModuleLoadInfoVec&& aEvents) = 0; +}; + +#else + +class BasicDllServices final : public detail::DllServicesBase { + public: + BasicDllServices() { EnableBasic(); } + + ~BasicDllServices() = default; + + // Not useful in this class, so provide a default implementation + virtual void DispatchDllLoadNotification( + ModuleLoadInfo&& aModLoadInfo) override {} + + virtual void DispatchModuleLoadBacklogNotification( + ModuleLoadInfoVec&& aEvents) override {} +}; + +#endif // defined(MOZILLA_INTERNAL_API) + +} // namespace glue +} // namespace mozilla + +#endif // mozilla_glue_WindowsDllServices_h diff --git a/toolkit/xre/dllservices/mozglue/WindowsFallbackLoaderAPI.cpp b/toolkit/xre/dllservices/mozglue/WindowsFallbackLoaderAPI.cpp new file mode 100644 index 0000000000..bda309d499 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/WindowsFallbackLoaderAPI.cpp @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "WindowsFallbackLoaderAPI.h" + +namespace mozilla { + +ModuleLoadInfo FallbackLoaderAPI::ConstructAndNotifyBeginDllLoad( + void** aContext, PCUNICODE_STRING aRequestedDllName) { + ModuleLoadInfo loadInfo(aRequestedDllName); + + MOZ_ASSERT(mLoaderObserver); + if (mLoaderObserver) { + mLoaderObserver->OnBeginDllLoad(aContext, aRequestedDllName); + } + + return loadInfo; +} + +bool FallbackLoaderAPI::SubstituteForLSP(PCUNICODE_STRING aLSPLeafName, + PHANDLE aOutHandle) { + MOZ_ASSERT(mLoaderObserver); + if (!mLoaderObserver) { + return false; + } + + return mLoaderObserver->SubstituteForLSP(aLSPLeafName, aOutHandle); +} + +void FallbackLoaderAPI::NotifyEndDllLoad(void* aContext, NTSTATUS aLoadNtStatus, + ModuleLoadInfo&& aModuleLoadInfo) { + aModuleLoadInfo.SetEndLoadTimeStamp(); + + if (NT_SUCCESS(aLoadNtStatus)) { + aModuleLoadInfo.CaptureBacktrace(); + } + + MOZ_ASSERT(mLoaderObserver); + if (mLoaderObserver) { + mLoaderObserver->OnEndDllLoad(aContext, aLoadNtStatus, + std::move(aModuleLoadInfo)); + } +} + +nt::AllocatedUnicodeString FallbackLoaderAPI::GetSectionName( + void* aSectionAddr) { + static const StaticDynamicallyLinkedFunctionPtr< + decltype(&::NtQueryVirtualMemory)> + pNtQueryVirtualMemory(L"ntdll.dll", "NtQueryVirtualMemory"); + MOZ_ASSERT(pNtQueryVirtualMemory); + + if (!pNtQueryVirtualMemory) { + return nt::AllocatedUnicodeString(); + } + + nt::MemorySectionNameBuf buf; + NTSTATUS ntStatus = + pNtQueryVirtualMemory(::GetCurrentProcess(), aSectionAddr, + MemorySectionName, &buf, sizeof(buf), nullptr); + if (!NT_SUCCESS(ntStatus)) { + return nt::AllocatedUnicodeString(); + } + + return nt::AllocatedUnicodeString(&buf.mSectionFileName); +} + +nt::LoaderAPI::InitDllBlocklistOOPFnPtr +FallbackLoaderAPI::GetDllBlocklistInitFn() { + MOZ_ASSERT_UNREACHABLE("This should not be called so soon!"); + return nullptr; +} + +nt::LoaderAPI::HandleLauncherErrorFnPtr +FallbackLoaderAPI::GetHandleLauncherErrorFn() { + MOZ_ASSERT_UNREACHABLE("This should not be called so soon!"); + return nullptr; +} + +nt::SharedSection* FallbackLoaderAPI::GetSharedSection() { + MOZ_ASSERT_UNREACHABLE("This should not be called so soon!"); + return nullptr; +} + +void FallbackLoaderAPI::SetObserver(nt::LoaderObserver* aLoaderObserver) { + mLoaderObserver = aLoaderObserver; +} + +} // namespace mozilla diff --git a/toolkit/xre/dllservices/mozglue/WindowsFallbackLoaderAPI.h b/toolkit/xre/dllservices/mozglue/WindowsFallbackLoaderAPI.h new file mode 100644 index 0000000000..8d6fcb2f0c --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/WindowsFallbackLoaderAPI.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_WindowsFallbackLoaderAPI_h +#define mozilla_WindowsFallbackLoaderAPI_h + +#include "mozilla/Attributes.h" +#include "NtLoaderAPI.h" + +namespace mozilla { + +class MOZ_ONLY_USED_TO_AVOID_STATIC_CONSTRUCTORS FallbackLoaderAPI final + : public nt::LoaderAPI { + public: + constexpr FallbackLoaderAPI() : mLoaderObserver(nullptr) {} + + ModuleLoadInfo ConstructAndNotifyBeginDllLoad( + void** aContext, PCUNICODE_STRING aRequestedDllName) final; + bool SubstituteForLSP(PCUNICODE_STRING aLSPLeafName, + PHANDLE aOutHandle) final; + void NotifyEndDllLoad(void* aContext, NTSTATUS aLoadNtStatus, + ModuleLoadInfo&& aModuleLoadInfo) final; + nt::AllocatedUnicodeString GetSectionName(void* aSectionAddr) final; + nt::LoaderAPI::InitDllBlocklistOOPFnPtr GetDllBlocklistInitFn() final; + nt::LoaderAPI::HandleLauncherErrorFnPtr GetHandleLauncherErrorFn() final; + nt::SharedSection* GetSharedSection() final; + + void SetObserver(nt::LoaderObserver* aLoaderObserver); + + private: + nt::LoaderObserver* mLoaderObserver; +}; + +} // namespace mozilla + +#endif // mozilla_WindowsFallbackLoaderAPI_h diff --git a/toolkit/xre/dllservices/mozglue/gen_dll_blocklist_defs.py b/toolkit/xre/dllservices/mozglue/gen_dll_blocklist_defs.py new file mode 100644 index 0000000000..056db4d995 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/gen_dll_blocklist_defs.py @@ -0,0 +1,750 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +from copy import deepcopy +from struct import unpack +from uuid import UUID + +from six import iteritems + +H_HEADER = """/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This file was auto-generated from {0} by gen_dll_blocklist_data.py. */ + +#ifndef mozilla_{1}_h +#define mozilla_{1}_h + +""" + +H_FOOTER = """#endif // mozilla_{1}_h + +""" + +H_DEFS_BEGIN_DEFAULT = """#include "mozilla/WindowsDllBlocklistCommon.h" + +DLL_BLOCKLIST_DEFINITIONS_BEGIN + +""" + +H_DEFS_END_DEFAULT = """ +DLL_BLOCKLIST_DEFINITIONS_END + +""" + +H_BEGIN_LSP = """#include <guiddef.h> + +static const GUID gLayerGuids[] = { + +""" + +H_END_LSP = """ +}; + +""" + +H_BEGIN_A11Y = """#include "mozilla/WindowsDllBlocklistCommon.h" + +DLL_BLOCKLIST_DEFINITIONS_BEGIN_NAMED(gBlockedInprocDlls) + +""" + +# These flag names should match the ones defined in WindowsDllBlocklistCommon.h +FLAGS_DEFAULT = "FLAGS_DEFAULT" +BLOCK_WIN8_AND_OLDER = "BLOCK_WIN8_AND_OLDER" +BLOCK_WIN7_AND_OLDER = "BLOCK_WIN7_AND_OLDER" +USE_TIMESTAMP = "USE_TIMESTAMP" +CHILD_PROCESSES_ONLY = "CHILD_PROCESSES_ONLY" +BROWSER_PROCESS_ONLY = "BROWSER_PROCESS_ONLY" +SUBSTITUTE_LSP_PASSTHROUGH = "SUBSTITUTE_LSP_PASSTHROUGH" +REDIRECT_TO_NOOP_ENTRYPOINT = "REDIRECT_TO_NOOP_ENTRYPOINT" +UTILITY_PROCESSES_ONLY = "UTILITY_PROCESSES_ONLY" +SOCKET_PROCESSES_ONLY = "SOCKET_PROCESSES_ONLY" + +# Only these flags are available in the input script +INPUT_ONLY_FLAGS = { + BLOCK_WIN8_AND_OLDER, + BLOCK_WIN7_AND_OLDER, +} + + +def FILTER_ALLOW_ALL(entry): + # A11y entries are special, so we always exclude those by default + # (so it's not really allowing 'all', but it is simpler to reason about by + # pretending that it is allowing all.) + return not isinstance(entry, A11yBlocklistEntry) + + +def FILTER_DENY_ALL(entry): + return False + + +def FILTER_ALLOW_ONLY_A11Y(entry): + return isinstance(entry, A11yBlocklistEntry) + + +def FILTER_ALLOW_ONLY_LSP(entry): + return isinstance(entry, LspBlocklistEntry) + + +def FILTER_TESTS_ONLY(entry): + return not isinstance(entry, A11yBlocklistEntry) and entry.is_test() + + +def derive_test_key(key): + return key + "_TESTS" + + +ALL_DEFINITION_LISTS = ( + "ALL_PROCESSES", + "BROWSER_PROCESS", + "CHILD_PROCESSES", + "UTILITY_PROCESSES", + "SOCKET_PROCESSES", +) + + +class BlocklistDescriptor(object): + """This class encapsulates every file that is output from this script. + Each instance has a name, an "input specification", and optional "flag + specification" and "output specification" entries. + """ + + DEFAULT_OUTSPEC = { + "mode": "", + "filter": FILTER_ALLOW_ALL, + "begin": H_DEFS_BEGIN_DEFAULT, + "end": H_DEFS_END_DEFAULT, + } + + FILE_NAME_TPL = "WindowsDllBlocklist{0}Defs" + + OutputDir = None + ExistingFd = None + ExistingFdLeafName = None + + def __init__(self, name, inspec, **kwargs): + """Positional arguments: + + name -- String containing the name of the output list. + + inspec -- One or more members of ALL_DEFINITION_LISTS. The input used + for this blocklist file is the union of all lists specified by this + variable. + + Keyword arguments: + + outspec -- an optional list of dicts that specify how the lists output + will be written out to a file. Each dict may contain the following keys: + + 'mode' -- a string that specifies a mode that is used when writing + out list entries to this particular output. This is passed in as the + mode argument to DllBlocklistEntry's write method. + + 'filter' -- a function that, given a blocklist entry, decides + whether or not that entry shall be included in this output file. + + 'begin' -- a string that is written to the output file after writing + the file's header, but prior to writing out any blocklist entries. + + 'end' -- a string that is written to the output file after writing + out any blocklist entries but before the file's footer. + + Any unspecified keys will be assigned default values. + + flagspec -- an optional dict whose keys consist of one or more of the + list names from inspec. Each corresponding value is a set of flags that + should be applied to each entry from that list. For example, the + flagspec: + + {'CHILD_PROCESSES': {CHILD_PROCESSES_ONLY}} + + causes any blocklist entries from the CHILD_PROCESSES list to be output + with the CHILD_PROCESSES_ONLY flag set. + + """ + + self._name = name + + # inspec's elements must all come from ALL_DEFINITION_LISTS + assert not (set(inspec).difference(set(ALL_DEFINITION_LISTS))) + + # Internally to the descriptor, we store input specifications as a dict + # that maps each input blocklist name to the set of flags to be applied + # to each entry in that blocklist. + self._inspec = {blocklist: set() for blocklist in inspec} + + self._outspecs = kwargs.get("outspec", BlocklistDescriptor.DEFAULT_OUTSPEC) + if isinstance(self._outspecs, dict): + # _outspecs should always be stored as a list of dicts + self._outspecs = [self._outspecs] + + flagspecs = kwargs.get("flagspec", dict()) + # flagspec's keys must all come from ALL_DEFINITION_LISTS + assert not (set(flagspecs.keys()).difference(set(self._inspec.keys()))) + + # Merge the flags from flagspec into _inspec's sets + for blocklist, flagspec in iteritems(flagspecs): + spec = self._inspec[blocklist] + if not isinstance(spec, set): + raise TypeError("Flag spec for list %s must be a set!" % blocklist) + spec.update(flagspec) + + @staticmethod + def set_output_fd(fd): + """The build system has already provided an open file descriptor for + one of our outputs. We save that here so that we may use that fd once + we're ready to process that fd's file. We also obtain the output dir for + use as the base directory for the other output files that we open. + """ + BlocklistDescriptor.ExistingFd = fd + ( + BlocklistDescriptor.OutputDir, + BlocklistDescriptor.ExistingFdLeafName, + ) = os.path.split(fd.name) + + @staticmethod + def ensure_no_dupes(defs): + """Ensure that defs does not contain any dupes. We raise a ValueError + because this is a bug in the input and requires developer intervention. + """ + seen = set() + for e in defs: + name = e.get_name() + if name not in seen: + seen.add(name) + else: + raise ValueError("Duplicate entry found: %s" % name) + + @staticmethod + def get_test_entries(exec_env, blocklist): + """Obtains all test entries for the corresponding blocklist, and also + ensures that each entry has its test flag set. + + Positional arguments: + + exec_env -- dict containing the globals that were passed to exec to + when the input script was run. + + blocklist -- The name of the blocklist to obtain tests from. This + should be one of the members of ALL_DEFINITION_LISTS + """ + test_key = derive_test_key(blocklist) + + def set_is_test(elem): + elem.set_is_test() + return elem + + return map(set_is_test, exec_env[test_key]) + + def gen_list(self, exec_env, filter_func): + """Generates a sorted list of blocklist entries from the input script, + filtered via filter_func. + + Positional arguments: + + exec_env -- dict containing the globals that were passed to exec to + when the input script was run. This function expects exec_env to + contain lists of blocklist entries, keyed using one of the members of + ALL_DEFINITION_LISTS. + + filter_func -- a filter function that evaluates each blocklist entry + to determine whether or not it should be included in the results. + """ + + # This list contains all the entries across all blocklists that we + # potentially want to process + unified_list = [] + + # For each blocklist specified in the _inspec, we query the globals + # for their entries, add any flags, and then add them to the + # unified_list. + for blocklist, listflags in iteritems(self._inspec): + + def add_list_flags(elem): + # We deep copy so that flags set for an entry in one blocklist + # do not interfere with flags set for an entry in a different + # list. + result = deepcopy(elem) + result.add_flags(listflags) + return result + + # We add list flags *before* filtering because the filters might + # want to access flags as part of their operation. + unified_list.extend(map(add_list_flags, exec_env[blocklist])) + + # We also add any test entries for the lists specified by _inspec + unified_list.extend( + map(add_list_flags, self.get_test_entries(exec_env, blocklist)) + ) + + # There should be no dupes in the input. If there are, raise an error. + self.ensure_no_dupes(unified_list) + + # Now we filter out any unwanted list entries + filtered_list = filter(filter_func, unified_list) + + # Sort the list on entry name so that the blocklist code may use + # binary search if it so chooses. + return sorted(filtered_list, key=lambda e: e.get_name()) + + @staticmethod + def get_fd(outspec_leaf_name): + """If BlocklistDescriptor.ExistingFd corresponds to outspec_leaf_name, + then we return that. Otherwise, we construct a new absolute path to + outspec_leaf_name and open a new file descriptor for writing. + """ + if ( + not BlocklistDescriptor.ExistingFd + or BlocklistDescriptor.ExistingFdLeafName != outspec_leaf_name + ): + new_name = os.path.join(BlocklistDescriptor.OutputDir, outspec_leaf_name) + return open(new_name, "w") + + fd = BlocklistDescriptor.ExistingFd + BlocklistDescriptor.ExistingFd = None + return fd + + def write(self, src_file, exec_env): + """Write out all output files that are specified by this descriptor. + + Positional arguments: + + src_file -- name of the input file from which the lists were generated. + + exec_env -- dictionary containing the lists that were parsed from the + input file when it was executed. + """ + + for outspec in self._outspecs: + # Use DEFAULT_OUTSPEC to supply defaults for any unused outspec keys + effective_outspec = BlocklistDescriptor.DEFAULT_OUTSPEC.copy() + effective_outspec.update(outspec) + + entries = self.gen_list(exec_env, effective_outspec["filter"]) + if not entries: + continue + + mode = effective_outspec["mode"] + + # Since each output descriptor may generate output across multiple + # modes, each list is uniquified via the concatenation of our name + # and the mode. + listname = self._name + mode + leafname_no_ext = BlocklistDescriptor.FILE_NAME_TPL.format(listname) + leafname = leafname_no_ext + ".h" + + with self.get_fd(leafname) as output: + print(H_HEADER.format(src_file, leafname_no_ext), file=output, end="") + print(effective_outspec["begin"], file=output, end="") + + for e in entries: + e.write(output, mode) + + print(effective_outspec["end"], file=output, end="") + print(H_FOOTER.format(src_file, leafname_no_ext), file=output, end="") + + +A11Y_OUTPUT_SPEC = { + "filter": FILTER_ALLOW_ONLY_A11Y, + "begin": H_BEGIN_A11Y, +} + +LSP_MODE_GUID = "Guid" + +LSP_OUTPUT_SPEC = [ + { + "mode": LSP_MODE_GUID, + "filter": FILTER_ALLOW_ONLY_LSP, + "begin": H_BEGIN_LSP, + "end": H_END_LSP, + }, +] + +GENERATED_BLOCKLIST_FILES = [ + BlocklistDescriptor("A11y", ["BROWSER_PROCESS"], outspec=A11Y_OUTPUT_SPEC), + BlocklistDescriptor( + "Launcher", + ALL_DEFINITION_LISTS, + flagspec={ + "BROWSER_PROCESS": {BROWSER_PROCESS_ONLY}, + "CHILD_PROCESSES": {CHILD_PROCESSES_ONLY}, + "UTILITY_PROCESSES": {UTILITY_PROCESSES_ONLY}, + "SOCKET_PROCESSES": {SOCKET_PROCESSES_ONLY}, + }, + ), + BlocklistDescriptor( + "Legacy", + ALL_DEFINITION_LISTS, + flagspec={ + "BROWSER_PROCESS": {BROWSER_PROCESS_ONLY}, + "CHILD_PROCESSES": {CHILD_PROCESSES_ONLY}, + "UTILITY_PROCESSES": {UTILITY_PROCESSES_ONLY}, + "SOCKET_PROCESSES": {SOCKET_PROCESSES_ONLY}, + }, + ), + # Roughed-in for the moment; we'll enable this in bug 1238735 + # BlocklistDescriptor('LSP', ALL_DEFINITION_LISTS, outspec=LSP_OUTPUT_SPEC), + BlocklistDescriptor( + "Test", ALL_DEFINITION_LISTS, outspec={"filter": FILTER_TESTS_ONLY} + ), +] + + +class PETimeStamp(object): + def __init__(self, ts): + max_timestamp = (2 ** 32) - 1 + if ts < 0 or ts > max_timestamp: + raise ValueError("Invalid timestamp value") + self._value = ts + + def __str__(self): + return "0x%08XU" % self._value + + +class Version(object): + """Encapsulates a DLL version.""" + + ALL_VERSIONS = 0xFFFFFFFFFFFFFFFF + UNVERSIONED = 0 + + def __init__(self, *args): + """There are multiple ways to construct a Version: + + As a tuple containing four elements (recommended); + As four integral arguments; + As a PETimeStamp; + As a long integer. + + The tuple and list formats require the value of each element to be + between 0 and 0xFFFF, inclusive. + """ + + self._ver = Version.UNVERSIONED + + if len(args) == 1: + if isinstance(args[0], tuple): + self.validate_iterable(args[0]) + + self._ver = "MAKE_VERSION%r" % (args[0],) + elif isinstance(args[0], PETimeStamp): + self._ver = args[0] + else: + self._ver = int(args[0]) + elif len(args) == 4: + self.validate_iterable(args) + + self._ver = "MAKE_VERSION%r" % (tuple(args),) + else: + raise ValueError("Bad arguments to Version constructor") + + def validate_iterable(self, arg): + if len(arg) != 4: + raise ValueError("Versions must be a 4-tuple") + + for component in arg: + if not isinstance(component, int) or component < 0 or component > 0xFFFF: + raise ValueError( + "Each version component must be a 16-bit " "unsigned integer" + ) + + def build_long(self, args): + self.validate_iterable(args) + return ( + (int(args[0]) << 48) + | (int(args[1]) << 32) + | (int(args[2]) << 16) + | int(args[3]) + ) + + def is_timestamp(self): + return isinstance(self._ver, PETimeStamp) + + def __str__(self): + if isinstance(self._ver, int): + if self._ver == Version.ALL_VERSIONS: + return "DllBlockInfo::ALL_VERSIONS" + + if self._ver == Version.UNVERSIONED: + return "DllBlockInfo::UNVERSIONED" + + return "0x%016XULL" % self._ver + + return str(self._ver) + + +class DllBlocklistEntry(object): + TEST_CONDITION = "defined(ENABLE_TESTS)" + + def __init__(self, name, ver, flags=(), **kwargs): + """Positional arguments: + + name -- The leaf name of the DLL. + + ver -- The maximum version to be blocked. NB: The comparison used by the + blocklist is <=, so you should specify the last bad version, as opposed + to the first good version. + + flags -- iterable containing the flags that should be applicable to + this blocklist entry. + + Keyword arguments: + + condition -- a string containing a C++ preprocessor expression. This + condition is written as a condition for an #if/#endif block that is + generated around the entry during output. + """ + + self.check_ascii(name) + self._name = name.lower() + self._ver = Version(ver) + + self._flags = set() + self.add_flags(flags) + if self._ver.is_timestamp(): + self._flags.add(USE_TIMESTAMP) + + self._cond = kwargs.get("condition", set()) + if isinstance(self._cond, str): + self._cond = {self._cond} + + @staticmethod + def check_ascii(name): + try: + # Supported in Python 3.7 + if not name.isascii(): + raise ValueError('DLL name "%s" must be ASCII!' % name) + return + except AttributeError: + pass + + try: + name.encode("ascii") + except UnicodeEncodeError: + raise ValueError('DLL name "%s" must be ASCII!' % name) + + def get_name(self): + return self._name + + def set_condition(self, cond): + self._cond.add(cond) + + def get_condition(self): + if len(self._cond) == 1: + fmt = "{0}" + else: + fmt = "({0})" + + return " && ".join([fmt.format(c) for c in self._cond]) + + def set_is_test(self): + self.set_condition(DllBlocklistEntry.TEST_CONDITION) + + def is_test(self): + return self._cond and DllBlocklistEntry.TEST_CONDITION in self._cond + + def add_flags(self, new_flags): + if isinstance(new_flags, str): + self._flags.add(new_flags) + else: + self._flags.update(new_flags) + + @staticmethod + def get_flag_string(flag): + return "DllBlockInfo::" + flag + + def get_flags_list(self): + return self._flags + + def write(self, output, mode): + if self._cond: + print("#if %s" % self.get_condition(), file=output) + + flags_str = "" + + flags = self.get_flags_list() + if flags: + flags_str = ", " + " | ".join(map(self.get_flag_string, flags)) + + entry_str = ' DLL_BLOCKLIST_ENTRY("%s", %s%s)' % ( + self._name, + str(self._ver), + flags_str, + ) + print(entry_str, file=output) + + if self._cond: + print("#endif // %s" % self.get_condition(), file=output) + + +class A11yBlocklistEntry(DllBlocklistEntry): + """Represents a blocklist entry for injected a11y DLLs. This class does + not need to do anything special compared to a DllBlocklistEntry; we just + use this type to distinguish a11y entries from "regular" blocklist entries. + """ + + def __init__(self, name, ver, flags=(), **kwargs): + """These arguments are identical to DllBlocklistEntry.__init__""" + + super(A11yBlocklistEntry, self).__init__(name, ver, flags, **kwargs) + + +class RedirectToNoOpEntryPoint(DllBlocklistEntry): + """Represents a blocklist entry to hook the entrypoint into a function + just returning TRUE to keep a module alive and harmless. + This entry is intended to block a DLL which is injected by IAT patching + which is planted by a kernel callback routine for LoadImage because + blocking such a DLL makes a process fail to launch. + """ + + def __init__(self, name, ver, flags=(), **kwargs): + """These arguments are identical to DllBlocklistEntry.__init__""" + + super(RedirectToNoOpEntryPoint, self).__init__(name, ver, flags, **kwargs) + + def get_flags_list(self): + flags = super(RedirectToNoOpEntryPoint, self).get_flags_list() + # RedirectToNoOpEntryPoint items always include the following flag + flags.add(REDIRECT_TO_NOOP_ENTRYPOINT) + return flags + + +class LspBlocklistEntry(DllBlocklistEntry): + """Represents a blocklist entry for a WinSock Layered Service Provider (LSP).""" + + GUID_UNPACK_FMT_LE = "<IHHBBBBBBBB" + Guids = dict() + + def __init__(self, name, ver, guids, flags=(), **kwargs): + """Positional arguments: + + name -- The leaf name of the DLL. + + ver -- The maximum version to be blocked. NB: The comparison used by the + blocklist is <=, so you should specify the last bad version, as opposed + to the first good version. + + guids -- Either a string or list of strings containing the GUIDs that + uniquely identify the LSP. These GUIDs are generated by the developer of + the LSP and are registered with WinSock alongside the LSP. We record + this GUID as part of the "Winsock_LSP" annotation in our crash reports. + + flags -- iterable containing the flags that should be applicable to + this blocklist entry. + + Keyword arguments: + + condition -- a string containing a C++ preprocessor expression. This + condition is written as a condition for an #if/#endif block that is + generated around the entry during output. + """ + + super(LspBlocklistEntry, self).__init__(name, ver, flags, **kwargs) + if not guids: + raise ValueError("Missing GUID(s)!") + + if isinstance(guids, str): + self.insert(UUID(guids), name) + else: + for guid in guids: + self.insert(UUID(guid), name) + + def insert(self, guid, name): + # Some explanation here: Multiple DLLs (and thus multiple instances of + # LspBlocklistEntry) may share the same GUIDs. To ensure that we do not + # have any duplicates, we store each GUID in a class variable, Guids. + # We include the original DLL name from the blocklist definitions so + # that we may output a comment above each GUID indicating which entries + # correspond to which GUID. + LspBlocklistEntry.Guids.setdefault(guid, []).append(name) + + def get_flags_list(self): + flags = super(LspBlocklistEntry, self).get_flags_list() + # LSP entries always include the following flag + flags.add(SUBSTITUTE_LSP_PASSTHROUGH) + return flags + + @staticmethod + def as_c_struct(guid, names): + parts = unpack(LspBlocklistEntry.GUID_UNPACK_FMT_LE, guid.bytes_le) + str_guid = ( + " // %r\n // {%s}\n { 0x%08x, 0x%04x, 0x%04x, " + "{ 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x }" + " }" + % ( + names, + str(guid), + parts[0], + parts[1], + parts[2], + parts[3], + parts[4], + parts[5], + parts[6], + parts[7], + parts[8], + parts[9], + parts[10], + ) + ) + return str_guid + + def write(self, output, mode): + if mode != LSP_MODE_GUID: + super(LspBlocklistEntry, self).write(output, mode) + return + + # We dump the entire contents of Guids on the first call, and then + # clear it. Remaining invocations of this method are no-ops. + if LspBlocklistEntry.Guids: + result = ",\n".join( + [ + self.as_c_struct(guid, names) + for guid, names in iteritems(LspBlocklistEntry.Guids) + ] + ) + print(result, file=output) + LspBlocklistEntry.Guids.clear() + + +def exec_script_file(script_name, globals): + with open(script_name) as script: + exec(compile(script.read(), script_name, "exec"), globals) + + +def gen_blocklists(first_fd, defs_filename): + + BlocklistDescriptor.set_output_fd(first_fd) + + # exec_env defines the global variables that will be present in the + # execution environment when defs_filename is run by exec. + exec_env = { + # Add the blocklist entry types + "A11yBlocklistEntry": A11yBlocklistEntry, + "DllBlocklistEntry": DllBlocklistEntry, + "LspBlocklistEntry": LspBlocklistEntry, + "RedirectToNoOpEntryPoint": RedirectToNoOpEntryPoint, + # Add the special version types + "ALL_VERSIONS": Version.ALL_VERSIONS, + "UNVERSIONED": Version.UNVERSIONED, + "PETimeStamp": PETimeStamp, + } + + # Import definition lists into exec_env + for defname in ALL_DEFINITION_LISTS: + exec_env[defname] = [] + # For each defname, add a special list for test-only entries + exec_env[derive_test_key(defname)] = [] + + # Import flags into exec_env + exec_env.update({flag: flag for flag in INPUT_ONLY_FLAGS}) + + # Now execute the input script with exec_env providing the globals + exec_script_file(defs_filename, exec_env) + + # Tell the output descriptors to write out the output files. + for desc in GENERATED_BLOCKLIST_FILES: + desc.write(defs_filename, exec_env) diff --git a/toolkit/xre/dllservices/mozglue/interceptor/Arm64.cpp b/toolkit/xre/dllservices/mozglue/interceptor/Arm64.cpp new file mode 100644 index 0000000000..81d8e6d09b --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/Arm64.cpp @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "Arm64.h" + +#include "mozilla/ResultVariant.h" + +namespace mozilla { +namespace interceptor { +namespace arm64 { + +struct PCRelativeLoadTest { + // Bitmask to be ANDed with the instruction to isolate the bits that this + // instance is interested in + uint32_t mTestMask; + // The desired bits that we want to see after masking + uint32_t mMatchBits; + // If we match, mDecodeFn provide the code to decode the instruction. + LoadOrBranch (*mDecodeFn)(const uintptr_t aPC, const uint32_t aInst); +}; + +static LoadOrBranch ADRPDecode(const uintptr_t aPC, const uint32_t aInst) { + // Keep in mind that on Windows aarch64, uint32_t is little-endian + const uint32_t kMaskDataProcImmPcRelativeImmLo = 0x60000000; + const uint32_t kMaskDataProcImmPcRelativeImmHi = 0x00FFFFE0; + + uintptr_t base = aPC; + intptr_t offset = SignExtend<intptr_t>( + ((aInst & kMaskDataProcImmPcRelativeImmHi) >> 3) | + ((aInst & kMaskDataProcImmPcRelativeImmLo) >> 29), + 21); + + base &= ~0xFFFULL; + offset <<= 12; + + uint8_t reg = aInst & 0x1F; + + return LoadOrBranch(base + offset, reg); +} + +MFBT_API LoadOrBranch BUncondImmDecode(const uintptr_t aPC, + const uint32_t aInst) { + int32_t offset = SignExtend<int32_t>(aInst & 0x03FFFFFFU, 26); + return LoadOrBranch(aPC + offset); +} + +// Order is important here; more specific encoding tests must be placed before +// less specific encoding tests. +static const PCRelativeLoadTest gPCRelTests[] = { + {0x9FC00000, 0x10000000, nullptr}, // ADR + {0x9FC00000, 0x90000000, &ADRPDecode}, // ADRP + {0xFF000000, 0x58000000, nullptr}, // LDR (literal) 64-bit GPR + {0x3B000000, 0x18000000, nullptr}, // LDR (literal) (remaining forms) + {0x7C000000, 0x14000000, nullptr}, // B (unconditional immediate) + {0xFE000000, 0x54000000, nullptr}, // B.Cond + {0x7E000000, 0x34000000, nullptr}, // Compare and branch (imm) + {0x7E000000, 0x36000000, nullptr}, // Test and branch (imm) + {0xFE000000, 0xD6000000, nullptr} // Unconditional branch (reg) +}; + +/** + * In this function we interate through each entry in |gPCRelTests|, AND + * |aInst| with |test.mTestMask| to isolate the bits that we're interested in, + * then compare that result against |test.mMatchBits|. If we have a match, + * then that particular entry is applicable to |aInst|. If |test.mDecodeFn| is + * present, then we call it to decode the instruction. If it is not present, + * then we assume that this particular instruction is unsupported. + */ +MFBT_API Result<LoadOrBranch, PCRelCheckError> CheckForPCRel( + const uintptr_t aPC, const uint32_t aInst) { + for (auto&& test : gPCRelTests) { + if ((aInst & test.mTestMask) == test.mMatchBits) { + if (!test.mDecodeFn) { + return Err(PCRelCheckError::NoDecoderAvailable); + } + + return test.mDecodeFn(aPC, aInst); + } + } + + return Err(PCRelCheckError::InstructionNotPCRel); +} + +} // namespace arm64 +} // namespace interceptor +} // namespace mozilla diff --git a/toolkit/xre/dllservices/mozglue/interceptor/Arm64.h b/toolkit/xre/dllservices/mozglue/interceptor/Arm64.h new file mode 100644 index 0000000000..4070fbf99f --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/Arm64.h @@ -0,0 +1,221 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_interceptor_Arm64_h +#define mozilla_interceptor_Arm64_h + +#include <type_traits> + +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "mozilla/Saturate.h" +#include "mozilla/Types.h" + +namespace mozilla { +namespace interceptor { +namespace arm64 { + +// clang-format off +enum class IntegerConditionCode : uint8_t { + // From the ARMv8 Architectural Reference Manual, Section C1.2.4 + // Description Condition Flags + EQ = 0b0000, // == Z == 1 + NE = 0b0001, // != Z == 0 + CS = 0b0010, // carry set C == 1 + HS = 0b0010, // carry set (alias) C == 1 + CC = 0b0011, // carry clear C == 0 + LO = 0b0011, // carry clear (alias) C == 0 + MI = 0b0100, // < 0 N == 1 + PL = 0b0101, // >= 0 N == 0 + VS = 0b0110, // overflow V == 1 + VC = 0b0111, // no overflow V == 0 + HI = 0b1000, // unsigned > C == 1 && Z == 0 + LS = 0b1001, // unsigned <= !(C == 1 && Z == 0) + GE = 0b1010, // signed >= N == V + LT = 0b1011, // signed < N != V + GT = 0b1100, // signed > Z == 0 && N == V + LE = 0b1101, // signed <= !(Z == 0 && N == V) + AL = 0b1110, // unconditional <Any> + NV = 0b1111 // unconditional (but AL is the preferred encoding) +}; +// clang-format on + +struct LoadOrBranch { + enum class Type { + Load, + Branch, + }; + + // Load constructor + LoadOrBranch(const uintptr_t aAbsAddress, const uint8_t aDestReg) + : mType(Type::Load), mAbsAddress(aAbsAddress), mDestReg(aDestReg) { + MOZ_ASSERT(aDestReg < 32); + } + + // Unconditional branch constructor + explicit LoadOrBranch(const uintptr_t aAbsAddress) + : mType(Type::Branch), + mAbsAddress(aAbsAddress), + mCond(IntegerConditionCode::AL) {} + + // Conditional branch constructor + LoadOrBranch(const uintptr_t aAbsAddress, const IntegerConditionCode aCond) + : mType(Type::Branch), mAbsAddress(aAbsAddress), mCond(aCond) {} + + Type mType; + + // The absolute address to be loaded into a register, or branched to + uintptr_t mAbsAddress; + + union { + // The destination register for the load + uint8_t mDestReg; + + // The condition code for the branch + IntegerConditionCode mCond; + }; +}; + +enum class PCRelCheckError { + InstructionNotPCRel, + NoDecoderAvailable, +}; + +MFBT_API Result<LoadOrBranch, PCRelCheckError> CheckForPCRel( + const uintptr_t aPC, const uint32_t aInst); + +/** + * Casts |aValue| to a |ResultT| via sign extension. + * + * This function should be used when extracting signed immediate values from + * an instruction. + * + * @param aValue The value to be sign extended. This value should already be + * isolated from the remainder of the instruction's bits and + * shifted all the way to the right. + * @param aNumValidBits The number of bits in |aValue| that contain the + * immediate signed value, including the sign bit. + */ +template <typename ResultT> +inline ResultT SignExtend(const uint32_t aValue, const uint8_t aNumValidBits) { + static_assert(std::is_integral_v<ResultT> && std::is_signed_v<ResultT>, + "ResultT must be a signed integral type"); + MOZ_ASSERT(aNumValidBits < 32U && aNumValidBits > 1); + + using UnsignedResultT = std::decay_t<std::make_unsigned_t<ResultT>>; + + const uint8_t kResultWidthBits = sizeof(ResultT) * 8; + + // Shift left unsigned + const uint8_t shiftAmt = kResultWidthBits - aNumValidBits; + UnsignedResultT shiftedLeft = static_cast<UnsignedResultT>(aValue) + << shiftAmt; + + // Now shift right signed + auto result = static_cast<ResultT>(shiftedLeft); + result >>= shiftAmt; + + return result; +} + +inline static uint32_t BuildUnconditionalBranchToRegister(const uint32_t aReg) { + MOZ_ASSERT(aReg < 32); + // BR aReg + return 0xD61F0000 | (aReg << 5); +} + +MFBT_API LoadOrBranch BUncondImmDecode(const uintptr_t aPC, + const uint32_t aInst); + +/** + * If |aTarget| is more than 128MB away from |aPC|, we need to use a veneer. + */ +inline static bool IsVeneerRequired(const uintptr_t aPC, + const uintptr_t aTarget) { + detail::Saturate<intptr_t> saturated(aTarget); + saturated -= aPC; + + uintptr_t absDiff = Abs(saturated.value()); + + return absDiff >= 0x08000000U; +} + +inline static bool IsUnconditionalBranchImm(const uint32_t aInst) { + return (aInst & 0xFC000000U) == 0x14000000U; +} + +inline static Maybe<uint32_t> BuildUnconditionalBranchImm( + const uintptr_t aPC, const uintptr_t aTarget) { + detail::Saturate<intptr_t> saturated(aTarget); + saturated -= aPC; + + CheckedInt<int32_t> offset(saturated.value()); + if (!offset.isValid()) { + return Nothing(); + } + + // offset should be a multiple of 4 + MOZ_ASSERT(offset.value() % 4 == 0); + if (offset.value() % 4) { + return Nothing(); + } + + offset /= 4; + if (!offset.isValid()) { + return Nothing(); + } + + uint32_t signbits = static_cast<uint32_t>(offset.value()) & 0xFE000000; + // Ensure that offset is small enough to fit into the 26 bit region. + // We check that the sign bits are either all ones or all zeros. + MOZ_ASSERT(signbits == 0xFE000000 || !signbits); + if (signbits && signbits != 0xFE000000) { + return Nothing(); + } + + uint32_t masked = static_cast<uint32_t>(offset.value()) & 0x03FFFFFF; + + // B imm26 + return Some(0x14000000U | masked); +} + +/** + * Allocate and construct a veneer that provides an absolute 64-bit branch to + * the hook function. + */ +template <typename TrampPoolT> +inline static uintptr_t MakeVeneer(TrampPoolT& aTrampPool, void* aPrimaryTramp, + const uintptr_t aDestAddress) { + auto maybeVeneer = aTrampPool.GetNextTrampoline(); + if (!maybeVeneer) { + return 0; + } + + Trampoline<typename TrampPoolT::MMPolicyT> veneer( + std::move(maybeVeneer.ref())); + + // Write the same header information that is used for trampolines + veneer.WriteEncodedPointer(nullptr); + veneer.WriteEncodedPointer(aPrimaryTramp); + + veneer.StartExecutableCode(); + + // Register 16 is explicitly intended for veneers in ARM64, so we use that + // register without fear of clobbering anything important. + veneer.WriteLoadLiteral(aDestAddress, 16); + veneer.WriteInstruction(BuildUnconditionalBranchToRegister(16)); + + return reinterpret_cast<uintptr_t>(veneer.EndExecutableCode()); +} + +} // namespace arm64 +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_Arm64_h diff --git a/toolkit/xre/dllservices/mozglue/interceptor/MMPolicies.h b/toolkit/xre/dllservices/mozglue/interceptor/MMPolicies.h new file mode 100644 index 0000000000..0a309a1065 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/MMPolicies.h @@ -0,0 +1,1031 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_interceptor_MMPolicies_h +#define mozilla_interceptor_MMPolicies_h + +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/DynamicallyLinkedFunctionPtr.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/Maybe.h" +#include "mozilla/Span.h" +#include "mozilla/TypedEnumBits.h" +#include "mozilla/Types.h" +#include "mozilla/WindowsMapRemoteView.h" +#include "mozilla/WindowsUnwindInfo.h" + +#include <windows.h> + +#if (NTDDI_VERSION < NTDDI_WIN10_RS4) || defined(__MINGW32__) +PVOID WINAPI VirtualAlloc2(HANDLE Process, PVOID BaseAddress, SIZE_T Size, + ULONG AllocationType, ULONG PageProtection, + MEM_EXTENDED_PARAMETER* ExtendedParameters, + ULONG ParameterCount); +PVOID WINAPI MapViewOfFile3(HANDLE FileMapping, HANDLE Process, + PVOID BaseAddress, ULONG64 Offset, SIZE_T ViewSize, + ULONG AllocationType, ULONG PageProtection, + MEM_EXTENDED_PARAMETER* ExtendedParameters, + ULONG ParameterCount); +#endif // (NTDDI_VERSION < NTDDI_WIN10_RS4) || defined(__MINGW32__) + +// _CRT_RAND_S is not defined everywhere, but we need it. +#if !defined(_CRT_RAND_S) +extern "C" errno_t rand_s(unsigned int* randomValue); +#endif // !defined(_CRT_RAND_S) + +// Declaring only the functions we need in NativeNt.h. To include the entire +// NativeNt.h causes circular dependency. +namespace mozilla { +namespace nt { +SIZE_T WINAPI VirtualQueryEx(HANDLE aProcess, LPCVOID aAddress, + PMEMORY_BASIC_INFORMATION aMemInfo, + SIZE_T aMemInfoLen); + +SIZE_T WINAPI VirtualQuery(LPCVOID aAddress, PMEMORY_BASIC_INFORMATION aMemInfo, + SIZE_T aMemInfoLen); +} // namespace nt +} // namespace mozilla + +namespace mozilla { +namespace interceptor { + +// This class implements memory operations not involving any kernel32's +// functions, so that derived classes can use them. +class MOZ_TRIVIAL_CTOR_DTOR MMPolicyInProcessPrimitive { + protected: + bool ProtectInternal(decltype(&::VirtualProtect) aVirtualProtect, + void* aVAddress, size_t aSize, uint32_t aProtFlags, + uint32_t* aPrevProtFlags) const { + MOZ_ASSERT(aPrevProtFlags); + BOOL ok = aVirtualProtect(aVAddress, aSize, aProtFlags, + reinterpret_cast<PDWORD>(aPrevProtFlags)); + if (!ok && aPrevProtFlags) { + // VirtualProtect can fail but still set valid protection flags. + // Let's clear those upon failure. + *aPrevProtFlags = 0; + } + + return !!ok; + } + + public: + bool Read(void* aToPtr, const void* aFromPtr, size_t aLen) const { + ::memcpy(aToPtr, aFromPtr, aLen); + return true; + } + + bool Write(void* aToPtr, const void* aFromPtr, size_t aLen) const { + ::memcpy(aToPtr, aFromPtr, aLen); + return true; + } + + /** + * @return true if the page that hosts aVAddress is accessible. + */ + bool IsPageAccessible(uintptr_t aVAddress) const { + MEMORY_BASIC_INFORMATION mbi; + SIZE_T result = nt::VirtualQuery(reinterpret_cast<LPCVOID>(aVAddress), &mbi, + sizeof(mbi)); + + return result && mbi.AllocationProtect && mbi.State == MEM_COMMIT && + mbi.Protect != PAGE_NOACCESS; + } +}; + +class MOZ_TRIVIAL_CTOR_DTOR MMPolicyBase { + protected: + static uintptr_t AlignDown(const uintptr_t aUnaligned, + const uintptr_t aAlignTo) { + MOZ_ASSERT(IsPowerOfTwo(aAlignTo)); +#pragma warning(suppress : 4146) + return aUnaligned & (-aAlignTo); + } + + static uintptr_t AlignUp(const uintptr_t aUnaligned, + const uintptr_t aAlignTo) { + MOZ_ASSERT(IsPowerOfTwo(aAlignTo)); +#pragma warning(suppress : 4146) + return aUnaligned + ((-aUnaligned) & (aAlignTo - 1)); + } + + static PVOID AlignUpToRegion(PVOID aUnaligned, uintptr_t aAlignTo, + size_t aLen, size_t aDesiredLen) { + uintptr_t unaligned = reinterpret_cast<uintptr_t>(aUnaligned); + uintptr_t aligned = AlignUp(unaligned, aAlignTo); + MOZ_ASSERT(aligned >= unaligned); + + if (aLen < aligned - unaligned) { + return nullptr; + } + + aLen -= (aligned - unaligned); + return reinterpret_cast<PVOID>((aLen >= aDesiredLen) ? aligned : 0); + } + + public: +#if defined(NIGHTLY_BUILD) + Maybe<DetourError> mLastError; + const Maybe<DetourError>& GetLastDetourError() const { return mLastError; } + template <typename... Args> + void SetLastDetourError(Args&&... aArgs) { + mLastError = Some(DetourError(std::forward<Args>(aArgs)...)); + } +#else + template <typename... Args> + void SetLastDetourError(Args&&... aArgs) {} +#endif // defined(NIGHTLY_BUILD) + + DWORD ComputeAllocationSize(const uint32_t aRequestedSize) const { + MOZ_ASSERT(aRequestedSize); + DWORD result = aRequestedSize; + + const uint32_t granularity = GetAllocGranularity(); + + uint32_t mod = aRequestedSize % granularity; + if (mod) { + result += (granularity - mod); + } + + return result; + } + + DWORD GetAllocGranularity() const { + static const DWORD kAllocGranularity = []() -> DWORD { + SYSTEM_INFO sysInfo; + ::GetSystemInfo(&sysInfo); + return sysInfo.dwAllocationGranularity; + }(); + + return kAllocGranularity; + } + + DWORD GetPageSize() const { + static const DWORD kPageSize = []() -> DWORD { + SYSTEM_INFO sysInfo; + ::GetSystemInfo(&sysInfo); + return sysInfo.dwPageSize; + }(); + + return kPageSize; + } + + uintptr_t GetMaxUserModeAddress() const { + static const uintptr_t kMaxUserModeAddr = []() -> uintptr_t { + SYSTEM_INFO sysInfo; + ::GetSystemInfo(&sysInfo); + return reinterpret_cast<uintptr_t>(sysInfo.lpMaximumApplicationAddress); + }(); + + return kMaxUserModeAddr; + } + + static const uint8_t* GetLowerBound(const Span<const uint8_t>& aBounds) { + return &(*aBounds.cbegin()); + } + + static const uint8_t* GetUpperBoundIncl(const Span<const uint8_t>& aBounds) { + // We return an upper bound that is inclusive. + return &(*(aBounds.cend() - 1)); + } + + static const uint8_t* GetUpperBoundExcl(const Span<const uint8_t>& aBounds) { + // We return an upper bound that is exclusive by adding 1 to the inclusive + // upper bound. + return GetUpperBoundIncl(aBounds) + 1; + } + + /** + * It is convenient for us to provide address range information based on a + * "pivot" and a distance from that pivot, as branch instructions operate + * within a range of the program counter. OTOH, to actually manage the + * regions of memory, it is easier to think about them in terms of their + * lower and upper bounds. This function converts from the former format to + * the latter format. + */ + Maybe<Span<const uint8_t>> SpanFromPivotAndDistance( + const uint32_t aSize, const uintptr_t aPivotAddr, + const uint32_t aMaxDistanceFromPivot) const { + if (!aPivotAddr || !aMaxDistanceFromPivot) { + return Nothing(); + } + + // We don't allow regions below 1MB so that we're not allocating near any + // sensitive areas in our address space. + const uintptr_t kMinAllowableAddress = 0x100000; + + const uintptr_t kGranularity(GetAllocGranularity()); + + // We subtract the max distance from the pivot to determine our lower bound. + CheckedInt<uintptr_t> lowerBound(aPivotAddr); + lowerBound -= aMaxDistanceFromPivot; + if (lowerBound.isValid()) { + // In this case, the subtraction has not underflowed, but we still want + // the lower bound to be at least kMinAllowableAddress. + lowerBound = std::max(lowerBound.value(), kMinAllowableAddress); + } else { + // In this case, we underflowed. Forcibly set the lower bound to + // kMinAllowableAddress. + lowerBound = CheckedInt<uintptr_t>(kMinAllowableAddress); + } + + // Align up to the next unit of allocation granularity when necessary. + lowerBound = AlignUp(lowerBound.value(), kGranularity); + MOZ_ASSERT(lowerBound.isValid()); + if (!lowerBound.isValid()) { + return Nothing(); + } + + // We must ensure that our region is below the maximum allowable user-mode + // address, or our reservation will fail. + const uintptr_t kMaxUserModeAddr = GetMaxUserModeAddress(); + + // We add the max distance from the pivot to determine our upper bound. + CheckedInt<uintptr_t> upperBound(aPivotAddr); + upperBound += aMaxDistanceFromPivot; + if (upperBound.isValid()) { + // In this case, the addition has not overflowed, but we still want + // the upper bound to be at most kMaxUserModeAddr. + upperBound = std::min(upperBound.value(), kMaxUserModeAddr); + } else { + // In this case, we overflowed. Forcibly set the upper bound to + // kMaxUserModeAddr. + upperBound = CheckedInt<uintptr_t>(kMaxUserModeAddr); + } + + // Subtract the desired allocation size so that any chunk allocated in the + // region will be reachable. + upperBound -= aSize; + if (!upperBound.isValid()) { + return Nothing(); + } + + // Align down to the next unit of allocation granularity when necessary. + upperBound = AlignDown(upperBound.value(), kGranularity); + if (!upperBound.isValid()) { + return Nothing(); + } + + MOZ_ASSERT(lowerBound.value() < upperBound.value()); + if (lowerBound.value() >= upperBound.value()) { + return Nothing(); + } + + // Return the result as a Span + return Some(Span(reinterpret_cast<const uint8_t*>(lowerBound.value()), + upperBound.value() - lowerBound.value())); + } + + /** + * This function locates a virtual memory region of |aDesiredBytesLen| that + * resides in the interval [aRangeMin, aRangeMax). We do this by scanning the + * virtual memory space for a block of unallocated memory that is sufficiently + * large. + */ + PVOID FindRegion(HANDLE aProcess, const size_t aDesiredBytesLen, + const uint8_t* aRangeMin, const uint8_t* aRangeMax) { + // Convert the given pointers to uintptr_t because we should not + // compare two pointers unless they are from the same array or object. + uintptr_t rangeMin = reinterpret_cast<uintptr_t>(aRangeMin); + uintptr_t rangeMax = reinterpret_cast<uintptr_t>(aRangeMax); + + const DWORD kGranularity = GetAllocGranularity(); + if (!aDesiredBytesLen) { + SetLastDetourError(MMPOLICY_RESERVE_FINDREGION_INVALIDLEN); + return nullptr; + } + + MOZ_ASSERT(rangeMin < rangeMax); + if (rangeMin >= rangeMax) { + SetLastDetourError(MMPOLICY_RESERVE_FINDREGION_INVALIDRANGE); + return nullptr; + } + + // Generate a randomized base address that falls within the interval + // [aRangeMin, aRangeMax - aDesiredBytesLen] + unsigned int rnd = 0; + rand_s(&rnd); + + // Reduce rnd to a value that falls within the acceptable range + uintptr_t maxOffset = + (rangeMax - rangeMin - aDesiredBytesLen) / kGranularity; + // Divide by maxOffset + 1 because maxOffset * kGranularity is acceptable. + uintptr_t offset = (uintptr_t(rnd) % (maxOffset + 1)) * kGranularity; + + // Start searching at this address + const uintptr_t searchStart = rangeMin + offset; + // The max address needs to incorporate the desired length + const uintptr_t kMaxPtr = rangeMax - aDesiredBytesLen; + + MOZ_DIAGNOSTIC_ASSERT(searchStart <= kMaxPtr); + + MEMORY_BASIC_INFORMATION mbi; + SIZE_T len = sizeof(mbi); + + // Scan the range for a free chunk that is at least as large as + // aDesiredBytesLen + // Scan [searchStart, kMaxPtr] + for (uintptr_t address = searchStart; address <= kMaxPtr;) { + if (nt::VirtualQueryEx(aProcess, reinterpret_cast<uint8_t*>(address), + &mbi, len) != len) { + SetLastDetourError(MMPOLICY_RESERVE_FINDREGION_VIRTUALQUERY_ERROR, + ::GetLastError()); + return nullptr; + } + + if (mbi.State == MEM_FREE) { + // |mbi.BaseAddress| is aligned with the page granularity, but may not + // be aligned with the allocation granularity. VirtualAlloc does not + // accept such a non-aligned address unless the corresponding allocation + // region is free. So we get the next boundary's start address. + PVOID regionStart = AlignUpToRegion(mbi.BaseAddress, kGranularity, + mbi.RegionSize, aDesiredBytesLen); + if (regionStart) { + return regionStart; + } + } + + address = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize; + } + + // Scan [aRangeMin, searchStart) + for (uintptr_t address = rangeMin; address < searchStart;) { + if (nt::VirtualQueryEx(aProcess, reinterpret_cast<uint8_t*>(address), + &mbi, len) != len) { + SetLastDetourError(MMPOLICY_RESERVE_FINDREGION_VIRTUALQUERY_ERROR, + ::GetLastError()); + return nullptr; + } + + if (mbi.State == MEM_FREE) { + PVOID regionStart = AlignUpToRegion(mbi.BaseAddress, kGranularity, + mbi.RegionSize, aDesiredBytesLen); + if (regionStart) { + return regionStart; + } + } + + address = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize; + } + + SetLastDetourError(MMPOLICY_RESERVE_FINDREGION_NO_FREE_REGION, + ::GetLastError()); + return nullptr; + } + + /** + * This function reserves a |aSize| block of virtual memory. + * + * When |aBounds| is Nothing, it just calls |aReserveFn| and lets Windows + * choose the base address. + * + * Otherwise, it tries to call |aReserveRangeFn| to reserve the memory within + * the bounds provided by |aBounds|. It is advantageous to use this function + * because the OS's VM manager has better information as to which base + * addresses are the best to use. + * + * If |aReserveRangeFn| retuns Nothing, this means that the platform support + * is not available. In that case, we fall back to manually computing a region + * to use for reserving the memory by calling |FindRegion|. + */ + template <typename ReserveFnT, typename ReserveRangeFnT> + PVOID Reserve(HANDLE aProcess, const uint32_t aSize, + const ReserveFnT& aReserveFn, + const ReserveRangeFnT& aReserveRangeFn, + const Maybe<Span<const uint8_t>>& aBounds) { + if (!aBounds) { + // No restrictions, let the OS choose the base address + PVOID ret = aReserveFn(aProcess, nullptr, aSize); + if (!ret) { + SetLastDetourError(MMPOLICY_RESERVE_NOBOUND_RESERVE_ERROR, + ::GetLastError()); + } + return ret; + } + + const uint8_t* lowerBound = GetLowerBound(aBounds.ref()); + const uint8_t* upperBoundExcl = GetUpperBoundExcl(aBounds.ref()); + + Maybe<PVOID> result = + aReserveRangeFn(aProcess, aSize, lowerBound, upperBoundExcl); + if (result) { + return result.value(); + } + + // aReserveRangeFn is not available on this machine. We'll do a manual + // search. + + size_t curAttempt = 0; + const size_t kMaxAttempts = 8; + + // We loop here because |FindRegion| may return a base address that + // is reserved elsewhere before we have had a chance to reserve it + // ourselves. + while (curAttempt < kMaxAttempts) { + PVOID base = FindRegion(aProcess, aSize, lowerBound, upperBoundExcl); + if (!base) { + return nullptr; + } + + result = Some(aReserveFn(aProcess, base, aSize)); + if (result.value()) { + return result.value(); + } + + ++curAttempt; + } + + // If we run out of attempts, we fall through to the default case where + // the system chooses any base address it wants. In that case, the hook + // will be set on a best-effort basis. + PVOID ret = aReserveFn(aProcess, nullptr, aSize); + if (!ret) { + SetLastDetourError(MMPOLICY_RESERVE_FINAL_RESERVE_ERROR, + ::GetLastError()); + } + return ret; + } +}; + +class MOZ_TRIVIAL_CTOR_DTOR MMPolicyInProcess + : public MMPolicyInProcessPrimitive, + public MMPolicyBase { + public: + typedef MMPolicyInProcess MMPolicyT; + + constexpr MMPolicyInProcess() + : mBase(nullptr), mReservationSize(0), mCommitOffset(0) {} + + MMPolicyInProcess(const MMPolicyInProcess&) = delete; + MMPolicyInProcess& operator=(const MMPolicyInProcess&) = delete; + + MMPolicyInProcess(MMPolicyInProcess&& aOther) + : mBase(nullptr), mReservationSize(0), mCommitOffset(0) { + *this = std::move(aOther); + } + + MMPolicyInProcess& operator=(MMPolicyInProcess&& aOther) { + mBase = aOther.mBase; + aOther.mBase = nullptr; + + mCommitOffset = aOther.mCommitOffset; + aOther.mCommitOffset = 0; + + mReservationSize = aOther.mReservationSize; + aOther.mReservationSize = 0; + + return *this; + } + + explicit operator bool() const { return !!mBase; } + + /** + * Should we unhook everything upon destruction? + */ + bool ShouldUnhookUponDestruction() const { return true; } + +#if defined(_M_IX86) + bool WriteAtomic(void* aDestPtr, const uint16_t aValue) const { + *static_cast<uint16_t*>(aDestPtr) = aValue; + return true; + } +#endif // defined(_M_IX86) + + bool Protect(void* aVAddress, size_t aSize, uint32_t aProtFlags, + uint32_t* aPrevProtFlags) const { + return ProtectInternal(::VirtualProtect, aVAddress, aSize, aProtFlags, + aPrevProtFlags); + } + + bool FlushInstructionCache() const { + return !!::FlushInstructionCache(::GetCurrentProcess(), nullptr, 0); + } + + static DWORD GetTrampWriteProtFlags() { return PAGE_EXECUTE_READWRITE; } + +#if defined(_M_X64) + bool IsTrampolineSpaceInLowest2GB() const { + return (mBase + mReservationSize) <= + reinterpret_cast<uint8_t*>(0x0000000080000000ULL); + } + + static constexpr bool kSupportsUnwindInfo = true; + + mozilla::UniquePtr<uint8_t[]> LookupUnwindInfo( + uintptr_t aOrigFuncAddr, uint32_t* aOffsetFromBeginAddr, + uint32_t* aOffsetToEndAddr, uintptr_t* aOrigImageBase) const { + DWORD64 origImageBase = 0; + auto origFuncEntry = + RtlLookupFunctionEntry(aOrigFuncAddr, &origImageBase, nullptr); + if (!origFuncEntry) { + return nullptr; + } + + if (aOffsetFromBeginAddr) { + *aOffsetFromBeginAddr = + aOrigFuncAddr - (origImageBase + origFuncEntry->BeginAddress); + } + if (aOffsetToEndAddr) { + *aOffsetToEndAddr = + (origImageBase + origFuncEntry->EndAddress) - aOrigFuncAddr; + } + if (aOrigImageBase) { + *aOrigImageBase = origImageBase; + } + return reinterpret_cast<const UnwindInfo*>(origImageBase + + origFuncEntry->UnwindData) + ->Copy(); + } + + bool AddFunctionTable(uintptr_t aFunctionTable, uint32_t aEntryCount, + uintptr_t aBaseAddress) const { + return bool( + RtlAddFunctionTable(reinterpret_cast<PRUNTIME_FUNCTION>(aFunctionTable), + aEntryCount, aBaseAddress)); + } +#endif // defined(_M_X64) + + protected: + uint8_t* GetLocalView() const { return mBase; } + + uintptr_t GetRemoteView() const { + // Same as local view for in-process + return reinterpret_cast<uintptr_t>(mBase); + } + + /** + * @return the effective number of bytes reserved, or 0 on failure + */ + uint32_t Reserve(const uint32_t aSize, + const Maybe<Span<const uint8_t>>& aBounds) { + if (!aSize) { + return 0; + } + + if (mBase) { + MOZ_ASSERT(mReservationSize >= aSize); + return mReservationSize; + } + + mReservationSize = ComputeAllocationSize(aSize); + + auto reserveFn = [](HANDLE aProcess, PVOID aBase, uint32_t aSize) -> PVOID { + return ::VirtualAlloc(aBase, aSize, MEM_RESERVE, PAGE_NOACCESS); + }; + + auto reserveWithinRangeFn = + [](HANDLE aProcess, uint32_t aSize, const uint8_t* aRangeMin, + const uint8_t* aRangeMaxExcl) -> Maybe<PVOID> { + static const StaticDynamicallyLinkedFunctionPtr< + decltype(&::VirtualAlloc2)> + pVirtualAlloc2(L"kernelbase.dll", "VirtualAlloc2"); + if (!pVirtualAlloc2) { + return Nothing(); + } + + // NB: MEM_ADDRESS_REQUIREMENTS::HighestEndingAddress is *inclusive* + MEM_ADDRESS_REQUIREMENTS memReq = { + const_cast<uint8_t*>(aRangeMin), + const_cast<uint8_t*>(aRangeMaxExcl - 1)}; + + MEM_EXTENDED_PARAMETER memParam = {}; + memParam.Type = MemExtendedParameterAddressRequirements; + memParam.Pointer = &memReq; + + return Some(pVirtualAlloc2(aProcess, nullptr, aSize, MEM_RESERVE, + PAGE_NOACCESS, &memParam, 1)); + }; + + mBase = static_cast<uint8_t*>( + MMPolicyBase::Reserve(::GetCurrentProcess(), mReservationSize, + reserveFn, reserveWithinRangeFn, aBounds)); + + if (!mBase) { + return 0; + } + + return mReservationSize; + } + + bool MaybeCommitNextPage(const uint32_t aRequestedOffset, + const uint32_t aRequestedLength) { + if (!(*this)) { + return false; + } + + uint32_t limit = aRequestedOffset + aRequestedLength - 1; + if (limit < mCommitOffset) { + // No commit required + return true; + } + + MOZ_DIAGNOSTIC_ASSERT(mCommitOffset < mReservationSize); + if (mCommitOffset >= mReservationSize) { + return false; + } + + PVOID local = ::VirtualAlloc(mBase + mCommitOffset, GetPageSize(), + MEM_COMMIT, PAGE_EXECUTE_READ); + if (!local) { + return false; + } + + mCommitOffset += GetPageSize(); + return true; + } + + private: + uint8_t* mBase; + uint32_t mReservationSize; + uint32_t mCommitOffset; +}; + +// This class manages in-process memory access without using functions +// imported from kernel32.dll. Instead, it uses functions in its own +// function table that are provided from outside. +class MMPolicyInProcessEarlyStage : public MMPolicyInProcessPrimitive { + public: + struct Kernel32Exports { + decltype(&::FlushInstructionCache) mFlushInstructionCache; + decltype(&::GetModuleHandleW) mGetModuleHandleW; + decltype(&::GetSystemInfo) mGetSystemInfo; + decltype(&::VirtualProtect) mVirtualProtect; + }; + + private: + static DWORD GetPageSize(const Kernel32Exports& aK32Exports) { + SYSTEM_INFO sysInfo; + aK32Exports.mGetSystemInfo(&sysInfo); + return sysInfo.dwPageSize; + } + + const Kernel32Exports& mK32Exports; + const DWORD mPageSize; + + public: + explicit MMPolicyInProcessEarlyStage(const Kernel32Exports& aK32Exports) + : mK32Exports(aK32Exports), mPageSize(GetPageSize(mK32Exports)) {} + + // The pattern of constructing a local static variable with a lambda, + // which can be seen in MMPolicyBase, is compiled into code with the + // critical section APIs like EnterCriticalSection imported from kernel32.dll. + // Because this class needs to be able to run in a process's early stage + // when IAT is not yet resolved, we cannot use that patten, thus simply + // caching a value as a local member in the class. + DWORD GetPageSize() const { return mPageSize; } + + bool Protect(void* aVAddress, size_t aSize, uint32_t aProtFlags, + uint32_t* aPrevProtFlags) const { + return ProtectInternal(mK32Exports.mVirtualProtect, aVAddress, aSize, + aProtFlags, aPrevProtFlags); + } + + bool FlushInstructionCache() const { + const HANDLE kCurrentProcess = reinterpret_cast<HANDLE>(-1); + return !!mK32Exports.mFlushInstructionCache(kCurrentProcess, nullptr, 0); + } +}; + +class MMPolicyOutOfProcess : public MMPolicyBase { + public: + typedef MMPolicyOutOfProcess MMPolicyT; + + explicit MMPolicyOutOfProcess(HANDLE aProcess) + : mProcess(nullptr), + mMapping(nullptr), + mLocalView(nullptr), + mRemoteView(nullptr), + mReservationSize(0), + mCommitOffset(0) { + MOZ_ASSERT(aProcess); + ::DuplicateHandle(::GetCurrentProcess(), aProcess, ::GetCurrentProcess(), + &mProcess, kAccessFlags, FALSE, 0); + MOZ_ASSERT(mProcess); + } + + explicit MMPolicyOutOfProcess(DWORD aPid) + : mProcess(::OpenProcess(kAccessFlags, FALSE, aPid)), + mMapping(nullptr), + mLocalView(nullptr), + mRemoteView(nullptr), + mReservationSize(0), + mCommitOffset(0) { + MOZ_ASSERT(mProcess); + } + + ~MMPolicyOutOfProcess() { Destroy(); } + + MMPolicyOutOfProcess(MMPolicyOutOfProcess&& aOther) + : mProcess(nullptr), + mMapping(nullptr), + mLocalView(nullptr), + mRemoteView(nullptr), + mReservationSize(0), + mCommitOffset(0) { + *this = std::move(aOther); + } + + MMPolicyOutOfProcess(const MMPolicyOutOfProcess& aOther) = delete; + MMPolicyOutOfProcess& operator=(const MMPolicyOutOfProcess&) = delete; + + MMPolicyOutOfProcess& operator=(MMPolicyOutOfProcess&& aOther) { + Destroy(); + + mProcess = aOther.mProcess; + aOther.mProcess = nullptr; + + mMapping = aOther.mMapping; + aOther.mMapping = nullptr; + + mLocalView = aOther.mLocalView; + aOther.mLocalView = nullptr; + + mRemoteView = aOther.mRemoteView; + aOther.mRemoteView = nullptr; + + mReservationSize = aOther.mReservationSize; + aOther.mReservationSize = 0; + + mCommitOffset = aOther.mCommitOffset; + aOther.mCommitOffset = 0; + + return *this; + } + + explicit operator bool() const { + return mProcess && mMapping && mLocalView && mRemoteView; + } + + bool ShouldUnhookUponDestruction() const { + // We don't clean up hooks for remote processes; they are expected to + // outlive our process. + return false; + } + + // This function reads as many bytes as |aLen| from the target process and + // succeeds only when the entire area to be read is accessible. + bool Read(void* aToPtr, const void* aFromPtr, size_t aLen) const { + MOZ_ASSERT(mProcess); + if (!mProcess) { + return false; + } + + SIZE_T numBytes = 0; + BOOL ok = ::ReadProcessMemory(mProcess, aFromPtr, aToPtr, aLen, &numBytes); + return ok && numBytes == aLen; + } + + // This function reads as many bytes as possible from the target process up + // to |aLen| bytes and returns the number of bytes which was actually read. + size_t TryRead(void* aToPtr, const void* aFromPtr, size_t aLen) const { + MOZ_ASSERT(mProcess); + if (!mProcess) { + return 0; + } + + uint32_t pageSize = GetPageSize(); + uintptr_t pageMask = pageSize - 1; + + auto rangeStart = reinterpret_cast<uintptr_t>(aFromPtr); + auto rangeEnd = rangeStart + aLen; + + while (rangeStart < rangeEnd) { + SIZE_T numBytes = 0; + BOOL ok = ::ReadProcessMemory(mProcess, aFromPtr, aToPtr, + rangeEnd - rangeStart, &numBytes); + if (ok) { + return numBytes; + } + + // If ReadProcessMemory fails, try to read up to each page boundary from + // the end of the requested area one by one. + if (rangeEnd & pageMask) { + rangeEnd &= ~pageMask; + } else { + rangeEnd -= pageSize; + } + } + + return 0; + } + + bool Write(void* aToPtr, const void* aFromPtr, size_t aLen) const { + MOZ_ASSERT(mProcess); + if (!mProcess) { + return false; + } + + SIZE_T numBytes = 0; + BOOL ok = ::WriteProcessMemory(mProcess, aToPtr, aFromPtr, aLen, &numBytes); + return ok && numBytes == aLen; + } + + bool Protect(void* aVAddress, size_t aSize, uint32_t aProtFlags, + uint32_t* aPrevProtFlags) const { + MOZ_ASSERT(mProcess); + if (!mProcess) { + return false; + } + + MOZ_ASSERT(aPrevProtFlags); + BOOL ok = ::VirtualProtectEx(mProcess, aVAddress, aSize, aProtFlags, + reinterpret_cast<PDWORD>(aPrevProtFlags)); + if (!ok && aPrevProtFlags) { + // VirtualProtectEx can fail but still set valid protection flags. + // Let's clear those upon failure. + *aPrevProtFlags = 0; + } + + return !!ok; + } + + /** + * @return true if the page that hosts aVAddress is accessible. + */ + bool IsPageAccessible(uintptr_t aVAddress) const { + MEMORY_BASIC_INFORMATION mbi; + SIZE_T result = nt::VirtualQueryEx( + mProcess, reinterpret_cast<LPCVOID>(aVAddress), &mbi, sizeof(mbi)); + + return result && mbi.AllocationProtect && mbi.State == MEM_COMMIT && + mbi.Protect != PAGE_NOACCESS; + } + + bool FlushInstructionCache() const { + return !!::FlushInstructionCache(mProcess, nullptr, 0); + } + + static DWORD GetTrampWriteProtFlags() { return PAGE_READWRITE; } + +#if defined(_M_X64) + bool IsTrampolineSpaceInLowest2GB() const { + return (GetRemoteView() + mReservationSize) <= 0x0000000080000000ULL; + } + + // TODO: We should also implement unwind info for our out-of-process policy. + static constexpr bool kSupportsUnwindInfo = false; + + inline mozilla::UniquePtr<uint8_t[]> LookupUnwindInfo( + uintptr_t aOrigFuncAddr, uint32_t* aOffsetFromBeginAddr, + uint32_t* aOffsetToEndAddr, uintptr_t* aOrigImageBase) const { + return nullptr; + } + + inline bool AddFunctionTable(uintptr_t aNewTable, uint32_t aEntryCount, + uintptr_t aBaseAddress) const { + return false; + } +#endif // defined(_M_X64) + + protected: + uint8_t* GetLocalView() const { return mLocalView; } + + uintptr_t GetRemoteView() const { + return reinterpret_cast<uintptr_t>(mRemoteView); + } + + /** + * @return the effective number of bytes reserved, or 0 on failure + */ + uint32_t Reserve(const uint32_t aSize, + const Maybe<Span<const uint8_t>>& aBounds) { + if (!aSize || !mProcess) { + SetLastDetourError(MMPOLICY_RESERVE_INVALIDARG); + return 0; + } + + if (mRemoteView) { + MOZ_ASSERT(mReservationSize >= aSize); + SetLastDetourError(MMPOLICY_RESERVE_ZERO_RESERVATIONSIZE); + return mReservationSize; + } + + mReservationSize = ComputeAllocationSize(aSize); + + mMapping = ::CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, + PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, + mReservationSize, nullptr); + if (!mMapping) { + SetLastDetourError(MMPOLICY_RESERVE_CREATEFILEMAPPING, ::GetLastError()); + return 0; + } + + mLocalView = static_cast<uint8_t*>( + ::MapViewOfFile(mMapping, FILE_MAP_WRITE, 0, 0, 0)); + if (!mLocalView) { + SetLastDetourError(MMPOLICY_RESERVE_MAPVIEWOFFILE, ::GetLastError()); + return 0; + } + + auto reserveFn = [mapping = mMapping](HANDLE aProcess, PVOID aBase, + uint32_t aSize) -> PVOID { + return mozilla::MapRemoteViewOfFile(mapping, aProcess, 0ULL, aBase, 0, 0, + PAGE_EXECUTE_READ); + }; + + auto reserveWithinRangeFn = + [mapping = mMapping](HANDLE aProcess, uint32_t aSize, + const uint8_t* aRangeMin, + const uint8_t* aRangeMaxExcl) -> Maybe<PVOID> { + static const StaticDynamicallyLinkedFunctionPtr< + decltype(&::MapViewOfFile3)> + pMapViewOfFile3(L"kernelbase.dll", "MapViewOfFile3"); + if (!pMapViewOfFile3) { + return Nothing(); + } + + // NB: MEM_ADDRESS_REQUIREMENTS::HighestEndingAddress is *inclusive* + MEM_ADDRESS_REQUIREMENTS memReq = { + const_cast<uint8_t*>(aRangeMin), + const_cast<uint8_t*>(aRangeMaxExcl - 1)}; + + MEM_EXTENDED_PARAMETER memParam = {}; + memParam.Type = MemExtendedParameterAddressRequirements; + memParam.Pointer = &memReq; + + return Some(pMapViewOfFile3(mapping, aProcess, nullptr, 0, aSize, 0, + PAGE_EXECUTE_READ, &memParam, 1)); + }; + + mRemoteView = MMPolicyBase::Reserve(mProcess, mReservationSize, reserveFn, + reserveWithinRangeFn, aBounds); + if (!mRemoteView) { + return 0; + } + + return mReservationSize; + } + + bool MaybeCommitNextPage(const uint32_t aRequestedOffset, + const uint32_t aRequestedLength) { + if (!(*this)) { + return false; + } + + uint32_t limit = aRequestedOffset + aRequestedLength - 1; + if (limit < mCommitOffset) { + // No commit required + return true; + } + + MOZ_DIAGNOSTIC_ASSERT(mCommitOffset < mReservationSize); + if (mCommitOffset >= mReservationSize) { + return false; + } + + PVOID local = ::VirtualAlloc(mLocalView + mCommitOffset, GetPageSize(), + MEM_COMMIT, PAGE_READWRITE); + if (!local) { + return false; + } + + PVOID remote = ::VirtualAllocEx( + mProcess, static_cast<uint8_t*>(mRemoteView) + mCommitOffset, + GetPageSize(), MEM_COMMIT, PAGE_EXECUTE_READ); + if (!remote) { + return false; + } + + mCommitOffset += GetPageSize(); + return true; + } + + private: + void Destroy() { + // We always leak the remote view + if (mLocalView) { + ::UnmapViewOfFile(mLocalView); + mLocalView = nullptr; + } + + if (mMapping) { + ::CloseHandle(mMapping); + mMapping = nullptr; + } + + if (mProcess) { + ::CloseHandle(mProcess); + mProcess = nullptr; + } + } + + private: + HANDLE mProcess; + HANDLE mMapping; + uint8_t* mLocalView; + PVOID mRemoteView; + uint32_t mReservationSize; + uint32_t mCommitOffset; + + static const DWORD kAccessFlags = PROCESS_QUERY_INFORMATION | + PROCESS_VM_OPERATION | PROCESS_VM_READ | + PROCESS_VM_WRITE; +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_MMPolicies_h diff --git a/toolkit/xre/dllservices/mozglue/interceptor/PatcherBase.h b/toolkit/xre/dllservices/mozglue/interceptor/PatcherBase.h new file mode 100644 index 0000000000..e39a38fafd --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/PatcherBase.h @@ -0,0 +1,141 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_interceptor_PatcherBase_h +#define mozilla_interceptor_PatcherBase_h + +#include "mozilla/interceptor/TargetFunction.h" + +namespace mozilla { +namespace interceptor { + +template <typename MMPolicy> +struct GetProcAddressSelector; + +template <> +struct GetProcAddressSelector<MMPolicyOutOfProcess> { + FARPROC operator()(HMODULE aModule, const char* aName, + const MMPolicyOutOfProcess& aMMPolicy) const { + auto exportSection = + mozilla::nt::PEExportSection<MMPolicyOutOfProcess>::Get(aModule, + aMMPolicy); + return exportSection.GetProcAddress(aName); + } +}; + +template <> +struct GetProcAddressSelector<MMPolicyInProcess> { + FARPROC operator()(HMODULE aModule, const char* aName, + const MMPolicyInProcess&) const { + // PEExportSection works for MMPolicyInProcess, too, but the native + // GetProcAddress is still better because PEExportSection does not + // solve a forwarded entry. + return ::GetProcAddress(aModule, aName); + } +}; + +template <typename VMPolicy> +class WindowsDllPatcherBase { + protected: + typedef typename VMPolicy::MMPolicyT MMPolicyT; + + template <typename... Args> + explicit WindowsDllPatcherBase(Args&&... aArgs) + : mVMPolicy(std::forward<Args>(aArgs)...) {} + + ReadOnlyTargetFunction<MMPolicyT> ResolveRedirectedAddress( + FARPROC aOriginalFunction) { + uintptr_t currAddr = reinterpret_cast<uintptr_t>(aOriginalFunction); + +#if defined(_M_IX86) || defined(_M_X64) + uintptr_t prevAddr = 0; + while (prevAddr != currAddr) { + ReadOnlyTargetFunction<MMPolicyT> currFunc(mVMPolicy, currAddr); + prevAddr = currAddr; + + // If function entry is jmp rel8 stub to the internal implementation, we + // resolve redirected address from the jump target. + uintptr_t nextAddr = 0; + if (currFunc.IsRelativeShortJump(&nextAddr)) { + int8_t offset = nextAddr - currFunc.GetAddress() - 2; + +# if defined(_M_X64) + // We redirect to the target of a short jump backwards if the target + // is another jump (only 32-bit displacement is currently supported). + // This case is used by GetFileAttributesW in Win7 x64. + if ((offset < 0) && (currFunc.IsValidAtOffset(2 + offset))) { + ReadOnlyTargetFunction<MMPolicyT> redirectFn(mVMPolicy, nextAddr); + if (redirectFn.IsIndirectNearJump(&nextAddr)) { + return redirectFn; + } + } +# endif + + // We check the downstream has enough nop-space only when the offset is + // positive. Otherwise we stop chasing redirects and let the caller + // fail to hook. + if (offset > 0) { + bool isNopSpace = true; + for (int8_t i = 0; i < offset; i++) { + if (currFunc[2 + i] != 0x90) { + isNopSpace = false; + break; + } + } + + if (isNopSpace) { + currAddr = nextAddr; + } + } +# if defined(_M_X64) + } else if (currFunc.IsIndirectNearJump(&nextAddr) || + currFunc.IsRelativeNearJump(&nextAddr)) { +# else + } else if (currFunc.IsIndirectNearJump(&nextAddr)) { +# endif + // If function entry is jmp [disp32] such as used by kernel32, we + // resolve redirected address from import table. For x64, we resolve + // a relative near jump for TestDllInterceptor with --disable-optimize. + currAddr = nextAddr; + } + } +#endif // defined(_M_IX86) || defined(_M_X64) + + if (currAddr != reinterpret_cast<uintptr_t>(aOriginalFunction) && + !mVMPolicy.IsPageAccessible(currAddr)) { + currAddr = reinterpret_cast<uintptr_t>(aOriginalFunction); + } + return ReadOnlyTargetFunction<MMPolicyT>(mVMPolicy, currAddr); + } + + public: + FARPROC GetProcAddress(HMODULE aModule, const char* aName) const { + GetProcAddressSelector<MMPolicyT> selector; + return selector(aModule, aName, mVMPolicy); + } + + bool IsPageAccessible(uintptr_t aAddress) const { + return mVMPolicy.IsPageAccessible(aAddress); + } + +#if defined(NIGHTLY_BUILD) + const Maybe<DetourError>& GetLastDetourError() const { + return mVMPolicy.GetLastDetourError(); + } +#endif // defined(NIGHTLY_BUILD) + template <typename... Args> + void SetLastDetourError(Args&&... aArgs) { + mVMPolicy.SetLastDetourError(std::forward<Args>(aArgs)...); + } + + protected: + VMPolicy mVMPolicy; +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_PatcherBase_h diff --git a/toolkit/xre/dllservices/mozglue/interceptor/PatcherDetour.h b/toolkit/xre/dllservices/mozglue/interceptor/PatcherDetour.h new file mode 100644 index 0000000000..1e8afcf9bc --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/PatcherDetour.h @@ -0,0 +1,1728 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_interceptor_PatcherDetour_h +#define mozilla_interceptor_PatcherDetour_h + +#if defined(_M_ARM64) +# include "mozilla/interceptor/Arm64.h" +#endif // defined(_M_ARM64) +#include <utility> + +#include "mozilla/Maybe.h" +#include "mozilla/NativeNt.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/TypedEnumBits.h" +#include "mozilla/Types.h" +#include "mozilla/Unused.h" +#include "mozilla/interceptor/PatcherBase.h" +#include "mozilla/interceptor/Trampoline.h" +#include "mozilla/interceptor/VMSharingPolicies.h" + +#define COPY_CODES(NBYTES) \ + do { \ + tramp.CopyCodes(origBytes.GetAddress(), NBYTES); \ + origBytes += NBYTES; \ + } while (0) + +namespace mozilla { +namespace interceptor { + +enum class DetourFlags : uint32_t { + eDefault = 0, + eEnable10BytePatch = 1, // Allow 10-byte patches when conditions allow + eTestOnlyForceShortPatch = + 2, // Force short patches at all times (x86-64 and arm64 testing only) + eDontResolveRedirection = + 4, // Don't resolve the redirection of JMP (e.g. kernel32 -> kernelbase) +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(DetourFlags) + +// This class is responsible to do tasks which depend on MMPolicy, decoupled +// from VMPolicy. We already have WindowsDllPatcherBase, but it needs to +// depend on VMPolicy to hold an instance of VMPolicy as a member. +template <typename MMPolicyT> +class WindowsDllDetourPatcherPrimitive { + protected: +#if defined(_M_ARM64) + // LDR x16, .+8 + static const uint32_t kLdrX16Plus8 = 0x58000050U; +#endif // defined(_M_ARM64) + + static void ApplyDefaultPatch(WritableTargetFunction<MMPolicyT>& target, + intptr_t aDest) { +#if defined(_M_IX86) + target.WriteByte(0xe9); // jmp + target.WriteDisp32(aDest); // hook displacement +#elif defined(_M_X64) + // mov r11, address + target.WriteByte(0x49); + target.WriteByte(0xbb); + target.WritePointer(aDest); + + // jmp r11 + target.WriteByte(0x41); + target.WriteByte(0xff); + target.WriteByte(0xe3); +#elif defined(_M_ARM64) + // The default patch requires 16 bytes + // LDR x16, .+8 + target.WriteLong(kLdrX16Plus8); + // BR x16 + target.WriteLong(arm64::BuildUnconditionalBranchToRegister(16)); + target.WritePointer(aDest); +#else +# error "Unsupported processor architecture" +#endif + } + + public: + constexpr static uint32_t GetWorstCaseRequiredBytesToPatch() { +#if defined(_M_IX86) + return 5; +#elif defined(_M_X64) + return 13; +#elif defined(_M_ARM64) + return 16; +#else +# error "Unsupported processor architecture" +#endif + } + + WindowsDllDetourPatcherPrimitive() = default; + + WindowsDllDetourPatcherPrimitive(const WindowsDllDetourPatcherPrimitive&) = + delete; + WindowsDllDetourPatcherPrimitive(WindowsDllDetourPatcherPrimitive&&) = delete; + WindowsDllDetourPatcherPrimitive& operator=( + const WindowsDllDetourPatcherPrimitive&) = delete; + WindowsDllDetourPatcherPrimitive& operator=( + WindowsDllDetourPatcherPrimitive&&) = delete; + + bool AddIrreversibleHook(const MMPolicyT& aMMPolicy, FARPROC aTargetFn, + intptr_t aHookDest) { + ReadOnlyTargetFunction<MMPolicyT> targetReadOnly(aMMPolicy, aTargetFn); + + WritableTargetFunction<MMPolicyT> targetWritable( + targetReadOnly.Promote(GetWorstCaseRequiredBytesToPatch())); + if (!targetWritable) { + return false; + } + + ApplyDefaultPatch(targetWritable, aHookDest); + + return targetWritable.Commit(); + } +}; + +template <typename VMPolicy> +class WindowsDllDetourPatcher final + : public WindowsDllDetourPatcherPrimitive<typename VMPolicy::MMPolicyT>, + public WindowsDllPatcherBase<VMPolicy> { + using MMPolicyT = typename VMPolicy::MMPolicyT; + using TrampPoolT = typename VMPolicy::PoolType; + using PrimitiveT = WindowsDllDetourPatcherPrimitive<MMPolicyT>; + Maybe<DetourFlags> mFlags; + + public: + template <typename... Args> + explicit WindowsDllDetourPatcher(Args&&... aArgs) + : WindowsDllPatcherBase<VMPolicy>(std::forward<Args>(aArgs)...) {} + + ~WindowsDllDetourPatcher() { Clear(); } + + WindowsDllDetourPatcher(const WindowsDllDetourPatcher&) = delete; + WindowsDllDetourPatcher(WindowsDllDetourPatcher&&) = delete; + WindowsDllDetourPatcher& operator=(const WindowsDllDetourPatcher&) = delete; + WindowsDllDetourPatcher& operator=(WindowsDllDetourPatcher&&) = delete; + + void Clear() { + if (!this->mVMPolicy.ShouldUnhookUponDestruction()) { + return; + } + +#if defined(_M_IX86) + size_t nBytes = 1 + sizeof(intptr_t); +#elif defined(_M_X64) + size_t nBytes = 2 + sizeof(intptr_t); +#elif defined(_M_ARM64) + size_t nBytes = 2 * sizeof(uint32_t) + sizeof(uintptr_t); +#else +# error "Unknown processor type" +#endif + + const auto& tramps = this->mVMPolicy.Items(); + for (auto&& tramp : tramps) { + // First we read the pointer to the interceptor instance. + Maybe<uintptr_t> instance = tramp.ReadEncodedPointer(); + if (!instance) { + continue; + } + + if (instance.value() != reinterpret_cast<uintptr_t>(this)) { + // tramp does not belong to this interceptor instance. + continue; + } + + auto clearInstance = MakeScopeExit([&tramp]() -> void { + // Clear the instance pointer so that no future instances with the same + // |this| pointer will attempt to reset its hook. + tramp.Rewind(); + tramp.WriteEncodedPointer(nullptr); + }); + + // Now we read the pointer to the intercepted function. + Maybe<uintptr_t> interceptedFn = tramp.ReadEncodedPointer(); + if (!interceptedFn) { + continue; + } + + WritableTargetFunction<MMPolicyT> origBytes( + this->mVMPolicy, interceptedFn.value(), nBytes); + if (!origBytes) { + continue; + } + +#if defined(_M_IX86) || defined(_M_X64) + + Maybe<uint8_t> maybeOpcode1 = origBytes.ReadByte(); + if (!maybeOpcode1) { + continue; + } + + uint8_t opcode1 = maybeOpcode1.value(); + +# if defined(_M_IX86) + // Ensure the JMP from CreateTrampoline is where we expect it to be. + MOZ_ASSERT(opcode1 == 0xE9); + if (opcode1 != 0xE9) { + continue; + } + + intptr_t startOfTrampInstructions = + static_cast<intptr_t>(tramp.GetCurrentRemoteAddress()); + + origBytes.WriteDisp32(startOfTrampInstructions); + if (!origBytes) { + continue; + } + + origBytes.Commit(); +# elif defined(_M_X64) + // Note: At the moment we clear 13-byte patches by replacing the jump to + // the patched function by a jump to the stub code. The original + // bytes of the original function are *not* restored. This implies + // that the stub code outlives our cleaning, so unwind information + // remains useful and must not be removed here. + if (opcode1 == 0x49) { + if (!Clear13BytePatch(origBytes, tramp.GetCurrentRemoteAddress())) { + continue; + } + } else if (opcode1 == 0xB8) { + if (!Clear10BytePatch(origBytes)) { + continue; + } + } else if (opcode1 == 0x48) { + // The original function was just a different trampoline + if (!ClearTrampolinePatch(origBytes, tramp.GetCurrentRemoteAddress())) { + continue; + } + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized patch!"); + continue; + } +# endif + +#elif defined(_M_ARM64) + + // Ensure that we see the instruction that we expect + Maybe<uint32_t> inst1 = origBytes.ReadLong(); + if (!inst1) { + continue; + } + + if (inst1.value() == this->kLdrX16Plus8) { + if (!Clear16BytePatch(origBytes, tramp.GetCurrentRemoteAddress())) { + continue; + } + } else if (arm64::IsUnconditionalBranchImm(inst1.value())) { + if (!Clear4BytePatch(inst1.value(), origBytes)) { + continue; + } + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized patch!"); + continue; + } + +#else +# error "Unknown processor type" +#endif + } + + this->mVMPolicy.Clear(); + } + +#if defined(_M_X64) + bool Clear13BytePatch(WritableTargetFunction<MMPolicyT>& aOrigBytes, + const uintptr_t aResetToAddress) { + Maybe<uint8_t> maybeOpcode2 = aOrigBytes.ReadByte(); + if (!maybeOpcode2) { + return false; + } + + uint8_t opcode2 = maybeOpcode2.value(); + if (opcode2 != 0xBB) { + return false; + } + + aOrigBytes.WritePointer(aResetToAddress); + if (!aOrigBytes) { + return false; + } + + return aOrigBytes.Commit(); + } + + bool ClearTrampolinePatch(WritableTargetFunction<MMPolicyT>& aOrigBytes, + const uintptr_t aPtrToResetToAddress) { + // The target of the trampoline we replaced is stored at + // aPtrToResetToAddress. We simply put it back where we got it from. + Maybe<uint8_t> maybeOpcode2 = aOrigBytes.ReadByte(); + if (!maybeOpcode2) { + return false; + } + + uint8_t opcode2 = maybeOpcode2.value(); + if (opcode2 != 0xB8) { + return false; + } + + auto oldPtr = *(reinterpret_cast<const uintptr_t*>(aPtrToResetToAddress)); + + aOrigBytes.WritePointer(oldPtr); + if (!aOrigBytes) { + return false; + } + + return aOrigBytes.Commit(); + } + + bool Clear10BytePatch(WritableTargetFunction<MMPolicyT>& aOrigBytes) { + Maybe<uint32_t> maybePtr32 = aOrigBytes.ReadLong(); + if (!maybePtr32) { + return false; + } + + uint32_t ptr32 = maybePtr32.value(); + // We expect the high bit to be clear + if (ptr32 & 0x80000000) { + return false; + } + + uintptr_t trampPtr = ptr32; + + // trampPtr points to an intermediate trampoline that contains a 13-byte + // patch. We back up by sizeof(uintptr_t) so that we can access the pointer + // to the stub trampoline. + WritableTargetFunction<MMPolicyT> writableIntermediate( + this->mVMPolicy, trampPtr - sizeof(uintptr_t), 13 + sizeof(uintptr_t)); + if (!writableIntermediate) { + return false; + } + + Maybe<uintptr_t> stubTramp = writableIntermediate.ReadEncodedPtr(); + if (!stubTramp || !stubTramp.value()) { + return false; + } + + Maybe<uint8_t> maybeOpcode1 = writableIntermediate.ReadByte(); + if (!maybeOpcode1) { + return false; + } + + // We expect this opcode to be the beginning of our normal mov r11, ptr + // patch sequence. + uint8_t opcode1 = maybeOpcode1.value(); + if (opcode1 != 0x49) { + return false; + } + + // Now we can just delegate the rest to our normal 13-byte patch clearing. + return Clear13BytePatch(writableIntermediate, stubTramp.value()); + } +#endif // defined(_M_X64) + +#if defined(_M_ARM64) + bool Clear4BytePatch(const uint32_t aBranchImm, + WritableTargetFunction<MMPolicyT>& aOrigBytes) { + MOZ_ASSERT(arm64::IsUnconditionalBranchImm(aBranchImm)); + + arm64::LoadOrBranch decoded = arm64::BUncondImmDecode( + aOrigBytes.GetCurrentAddress() - sizeof(uint32_t), aBranchImm); + + uintptr_t trampPtr = decoded.mAbsAddress; + + // trampPtr points to an intermediate trampoline that contains a veneer. + // We back up by sizeof(uintptr_t) so that we can access the pointer to the + // stub trampoline. + + // We want trampLen to be the size of the veneer, plus one pointer (since + // we are backing up trampPtr by one pointer) + size_t trampLen = 16 + sizeof(uintptr_t); + + WritableTargetFunction<MMPolicyT> writableIntermediate( + this->mVMPolicy, trampPtr - sizeof(uintptr_t), trampLen); + if (!writableIntermediate) { + return false; + } + + Maybe<uintptr_t> stubTramp = writableIntermediate.ReadEncodedPtr(); + if (!stubTramp || !stubTramp.value()) { + return false; + } + + Maybe<uint32_t> inst1 = writableIntermediate.ReadLong(); + if (!inst1 || inst1.value() != this->kLdrX16Plus8) { + return false; + } + + return Clear16BytePatch(writableIntermediate, stubTramp.value()); + } + + bool Clear16BytePatch(WritableTargetFunction<MMPolicyT>& aOrigBytes, + const uintptr_t aResetToAddress) { + Maybe<uint32_t> inst2 = aOrigBytes.ReadLong(); + if (!inst2) { + return false; + } + + if (inst2.value() != arm64::BuildUnconditionalBranchToRegister(16)) { + MOZ_ASSERT_UNREACHABLE("Unrecognized patch!"); + return false; + } + + // Clobber the pointer to our hook function with a pointer to the + // start of the trampoline. + aOrigBytes.WritePointer(aResetToAddress); + aOrigBytes.Commit(); + + return true; + } +#endif // defined(_M_ARM64) + + void Init(DetourFlags aFlags = DetourFlags::eDefault) { + if (Initialized()) { + return; + } + +#if defined(_M_X64) + if (aFlags & DetourFlags::eTestOnlyForceShortPatch) { + aFlags |= DetourFlags::eEnable10BytePatch; + } +#endif // defined(_M_X64) + + mFlags = Some(aFlags); + } + + bool Initialized() const { return mFlags.isSome(); } + + bool AddHook(FARPROC aTargetFn, intptr_t aHookDest, void** aOrigFunc) { + ReadOnlyTargetFunction<MMPolicyT> target( + (mFlags.value() & DetourFlags::eDontResolveRedirection) + ? ReadOnlyTargetFunction<MMPolicyT>( + this->mVMPolicy, reinterpret_cast<uintptr_t>(aTargetFn)) + : this->ResolveRedirectedAddress(aTargetFn)); + + TrampPoolT* trampPool = nullptr; + +#if defined(_M_ARM64) + // ARM64 uses two passes to build its trampoline. The first pass uses a + // null tramp to determine how many bytes are needed. Once that is known, + // CreateTrampoline calls itself recursively with a "real" tramp. + Trampoline<MMPolicyT> tramp(nullptr); +#else + Maybe<TrampPoolT> maybeTrampPool = DoReserve(); + MOZ_ASSERT(maybeTrampPool); + if (!maybeTrampPool) { + return false; + } + + trampPool = maybeTrampPool.ptr(); + + Maybe<Trampoline<MMPolicyT>> maybeTramp(trampPool->GetNextTrampoline()); + if (!maybeTramp) { + this->SetLastDetourError( + DetourResultCode::DETOUR_PATCHER_NEXT_TRAMPOLINE_ERROR); + return false; + } + + Trampoline<MMPolicyT> tramp(std::move(maybeTramp.ref())); +#endif + + CreateTrampoline(target, trampPool, tramp, aHookDest, aOrigFunc); + if (!*aOrigFunc) { + return false; + } + + return true; + } + + private: + /** + * This function returns a maximum distance that can be reached by a single + * unconditional jump instruction. This is dependent on the processor ISA. + * Note that this distance is *exclusive* when added to the pivot, so the + * distance returned by this function is actually + * (maximum_absolute_offset + 1). + */ + static uint32_t GetDefaultPivotDistance() { +#if defined(_M_ARM64) + // Immediate unconditional branch allows for +/- 128MB + return 0x08000000U; +#elif defined(_M_IX86) || defined(_M_X64) + // For these ISAs, our distance will assume the use of an unconditional jmp + // with a 32-bit signed displacement. + return 0x80000000U; +#else +# error "Not defined for this processor arch" +#endif + } + + /** + * If we're reserving trampoline space for a specific module, we base the + * pivot off of the median address of the module's .text section. While this + * may not be precise, it should be accurate enough for our purposes: To + * ensure that the trampoline space is reachable by any executable code in the + * module. + */ + Maybe<TrampPoolT> ReserveForModule(HMODULE aModule) { + nt::PEHeaders moduleHeaders(aModule); + if (!moduleHeaders) { + this->SetLastDetourError( + DetourResultCode::DETOUR_PATCHER_RESERVE_FOR_MODULE_PE_ERROR); + return Nothing(); + } + + Maybe<Span<const uint8_t>> textSectionInfo = + moduleHeaders.GetTextSectionInfo(); + if (!textSectionInfo) { + this->SetLastDetourError( + DetourResultCode::DETOUR_PATCHER_RESERVE_FOR_MODULE_TEXT_ERROR); + return Nothing(); + } + + const uint8_t* median = textSectionInfo.value().data() + + (textSectionInfo.value().LengthBytes() / 2); + + Maybe<TrampPoolT> maybeTrampPool = this->mVMPolicy.Reserve( + reinterpret_cast<uintptr_t>(median), GetDefaultPivotDistance()); + if (!maybeTrampPool) { + this->SetLastDetourError( + DetourResultCode::DETOUR_PATCHER_RESERVE_FOR_MODULE_RESERVE_ERROR); + } + return maybeTrampPool; + } + + Maybe<TrampPoolT> DoReserve(HMODULE aModule = nullptr) { + if (aModule) { + return ReserveForModule(aModule); + } + + uintptr_t pivot = 0; + uint32_t distance = 0; + +#if defined(_M_X64) + if (mFlags.value() & DetourFlags::eEnable10BytePatch) { + // We must stay below the 2GB mark because a 10-byte patch uses movsxd + // (ie, sign extension) to expand the pointer to 64-bits, so bit 31 of any + // pointers into the reserved region must be 0. + pivot = 0x40000000U; + distance = 0x40000000U; + } +#endif // defined(_M_X64) + + Maybe<TrampPoolT> maybeTrampPool = this->mVMPolicy.Reserve(pivot, distance); +#if defined(NIGHTLY_BUILD) + if (!maybeTrampPool && this->GetLastDetourError().isNothing()) { + this->SetLastDetourError( + DetourResultCode::DETOUR_PATCHER_DO_RESERVE_ERROR); + } +#endif // defined(NIGHTLY_BUILD) + return maybeTrampPool; + } + + protected: +#if !defined(_M_ARM64) + + const static int kPageSize = 4096; + + // rex bits + static const BYTE kMaskHighNibble = 0xF0; + static const BYTE kRexOpcode = 0x40; + static const BYTE kMaskRexW = 0x08; + static const BYTE kMaskRexR = 0x04; + static const BYTE kMaskRexX = 0x02; + static const BYTE kMaskRexB = 0x01; + + // mod r/m bits + static const BYTE kRegFieldShift = 3; + static const BYTE kMaskMod = 0xC0; + static const BYTE kMaskReg = 0x38; + static const BYTE kMaskRm = 0x07; + static const BYTE kRmNeedSib = 0x04; + static const BYTE kModReg = 0xC0; + static const BYTE kModDisp32 = 0x80; + static const BYTE kModDisp8 = 0x40; + static const BYTE kModNoRegDisp = 0x00; + static const BYTE kRmNoRegDispDisp32 = 0x05; + + // sib bits + static const BYTE kMaskSibScale = 0xC0; + static const BYTE kMaskSibIndex = 0x38; + static const BYTE kMaskSibBase = 0x07; + static const BYTE kSibBaseEbp = 0x05; + + // Register bit IDs. + static const BYTE kRegAx = 0x0; + static const BYTE kRegCx = 0x1; + static const BYTE kRegDx = 0x2; + static const BYTE kRegBx = 0x3; + static const BYTE kRegSp = 0x4; + static const BYTE kRegBp = 0x5; + static const BYTE kRegSi = 0x6; + static const BYTE kRegDi = 0x7; + + // Special ModR/M codes. These indicate operands that cannot be simply + // memcpy-ed. + // Operand is a 64-bit RIP-relative address. + static const int kModOperand64 = -2; + // Operand is not yet handled by our trampoline. + static const int kModUnknown = -1; + + /** + * Returns the number of bytes taken by the ModR/M byte, SIB (if present) + * and the instruction's operand. In special cases, the special MODRM codes + * above are returned. + * aModRm points to the ModR/M byte of the instruction. + * On return, aSubOpcode (if present) is filled with the subopcode/register + * code found in the ModR/M byte. + */ + int CountModRmSib(const ReadOnlyTargetFunction<MMPolicyT>& aModRm, + BYTE* aSubOpcode = nullptr) { + int numBytes = 1; // Start with 1 for mod r/m byte itself + switch (*aModRm & kMaskMod) { + case kModReg: + return numBytes; + case kModDisp8: + numBytes += 1; + break; + case kModDisp32: + numBytes += 4; + break; + case kModNoRegDisp: + if ((*aModRm & kMaskRm) == kRmNoRegDispDisp32) { +# if defined(_M_X64) + if (aSubOpcode) { + *aSubOpcode = (*aModRm & kMaskReg) >> kRegFieldShift; + } + return kModOperand64; +# else + // On IA-32, all ModR/M instruction modes address memory relative to 0 + numBytes += 4; +# endif + } else if (((*aModRm & kMaskRm) == kRmNeedSib && + (*(aModRm + 1) & kMaskSibBase) == kSibBaseEbp)) { + numBytes += 4; + } + break; + default: + // This should not be reachable + MOZ_ASSERT_UNREACHABLE("Impossible value for modr/m byte mod bits"); + return kModUnknown; + } + if ((*aModRm & kMaskRm) == kRmNeedSib) { + // SIB byte + numBytes += 1; + } + if (aSubOpcode) { + *aSubOpcode = (*aModRm & kMaskReg) >> kRegFieldShift; + } + return numBytes; + } + +# if defined(_M_X64) + enum class JumpType{Je, Jne, Jae, Jmp, Call}; + + static bool GenerateJump(Trampoline<MMPolicyT>& aTramp, + uintptr_t aAbsTargetAddress, const JumpType aType) { + // Near call, absolute indirect, address given in r/m32 + if (aType == JumpType::Call) { + // CALL [RIP+0] + aTramp.WriteByte(0xff); + aTramp.WriteByte(0x15); + // The offset to jump destination -- 2 bytes after the current position. + aTramp.WriteInteger(2); + aTramp.WriteByte(0xeb); // JMP + 8 (jump over target address) + aTramp.WriteByte(8); + aTramp.WritePointer(aAbsTargetAddress); + return !!aTramp; + } + + // Write an opposite conditional jump because the destination branches + // are swapped. + if (aType == JumpType::Je) { + // JNE RIP+14 + aTramp.WriteByte(0x75); + aTramp.WriteByte(14); + } else if (aType == JumpType::Jne) { + // JE RIP+14 + aTramp.WriteByte(0x74); + aTramp.WriteByte(14); + } else if (aType == JumpType::Jae) { + // JB RIP+14 + aTramp.WriteByte(0x72); + aTramp.WriteByte(14); + } + + // Near jmp, absolute indirect, address given in r/m32 + // JMP [RIP+0] + aTramp.WriteByte(0xff); + aTramp.WriteByte(0x25); + // The offset to jump destination is 0 + aTramp.WriteInteger(0); + aTramp.WritePointer(aAbsTargetAddress); + + return !!aTramp; + } +# endif + + enum ePrefixGroupBits{eNoPrefixes = 0, ePrefixGroup1 = (1 << 0), + ePrefixGroup2 = (1 << 1), ePrefixGroup3 = (1 << 2), + ePrefixGroup4 = (1 << 3)}; + + int CountPrefixBytes(const ReadOnlyTargetFunction<MMPolicyT>& aBytes, + unsigned char* aOutGroupBits) { + unsigned char& groupBits = *aOutGroupBits; + groupBits = eNoPrefixes; + int index = 0; + while (true) { + switch (aBytes[index]) { + // Group 1 + case 0xF0: // LOCK + case 0xF2: // REPNZ + case 0xF3: // REP / REPZ + if (groupBits & ePrefixGroup1) { + return -1; + } + groupBits |= ePrefixGroup1; + ++index; + break; + + // Group 2 + case 0x2E: // CS override / branch not taken + case 0x36: // SS override + case 0x3E: // DS override / branch taken + case 0x64: // FS override + case 0x65: // GS override + if (groupBits & ePrefixGroup2) { + return -1; + } + groupBits |= ePrefixGroup2; + ++index; + break; + + // Group 3 + case 0x66: // operand size override + if (groupBits & ePrefixGroup3) { + return -1; + } + groupBits |= ePrefixGroup3; + ++index; + break; + + // Group 4 + case 0x67: // Address size override + if (groupBits & ePrefixGroup4) { + return -1; + } + groupBits |= ePrefixGroup4; + ++index; + break; + + default: + return index; + } + } + } + + // Return a ModR/M byte made from the 2 Mod bits, the register used for the + // reg bits and the register used for the R/M bits. + BYTE BuildModRmByte(BYTE aModBits, BYTE aReg, BYTE aRm) { + MOZ_ASSERT((aRm & kMaskRm) == aRm); + MOZ_ASSERT((aModBits & kMaskMod) == aModBits); + MOZ_ASSERT(((aReg << kRegFieldShift) & kMaskReg) == + (aReg << kRegFieldShift)); + return aModBits | (aReg << kRegFieldShift) | aRm; + } + +#endif // !defined(_M_ARM64) + + // If originalFn is a recognized trampoline then patch it to call aDest, + // set *aTramp and *aOutTramp to that trampoline's target and return true. + bool PatchIfTargetIsRecognizedTrampoline( + Trampoline<MMPolicyT>& aTramp, + ReadOnlyTargetFunction<MMPolicyT>& aOriginalFn, intptr_t aDest, + void** aOutTramp) { +#if defined(_M_X64) + // Variation 1: + // 48 b8 imm64 mov rax, imm64 + // ff e0 jmp rax + // + // Variation 2: + // 48 b8 imm64 mov rax, imm64 + // 50 push rax + // c3 ret + if ((aOriginalFn[0] == 0x48) && (aOriginalFn[1] == 0xB8) && + ((aOriginalFn[10] == 0xFF && aOriginalFn[11] == 0xE0) || + (aOriginalFn[10] == 0x50 && aOriginalFn[11] == 0xC3))) { + uintptr_t originalTarget = + (aOriginalFn + 2).template ChasePointer<uintptr_t>(); + + // Skip the first two bytes (48 b8) so that we can overwrite the imm64 + WritableTargetFunction<MMPolicyT> target(aOriginalFn.Promote(8, 2)); + if (!target) { + return false; + } + + // Write the new JMP target address. + target.WritePointer(aDest); + if (!target.Commit()) { + return false; + } + + // Store the old target address so we can restore it when we're cleared + aTramp.WritePointer(originalTarget); + if (!aTramp) { + return false; + } + + *aOutTramp = reinterpret_cast<void*>(originalTarget); + return true; + } +#endif // defined(_M_X64) + + return false; + } + +#if defined(_M_ARM64) + bool Apply4BytePatch(TrampPoolT* aTrampPool, void* aTrampPtr, + WritableTargetFunction<MMPolicyT>& target, + intptr_t aDest) { + MOZ_ASSERT(aTrampPool); + if (!aTrampPool) { + return false; + } + + uintptr_t hookDest = arm64::MakeVeneer(*aTrampPool, aTrampPtr, aDest); + if (!hookDest) { + return false; + } + + Maybe<uint32_t> branchImm = arm64::BuildUnconditionalBranchImm( + target.GetCurrentAddress(), hookDest); + if (!branchImm) { + return false; + } + + target.WriteLong(branchImm.value()); + + return true; + } +#endif // defined(_M_ARM64) + +#if defined(_M_X64) + bool Apply10BytePatch(TrampPoolT* aTrampPool, void* aTrampPtr, + WritableTargetFunction<MMPolicyT>& target, + intptr_t aDest) { + // Note: Even if the target function is also below 2GB, we still use an + // intermediary trampoline so that we consistently have a 64-bit pointer + // that we can use to reset the trampoline upon interceptor shutdown. + Maybe<Trampoline<MMPolicyT>> maybeCallTramp( + aTrampPool->GetNextTrampoline()); + if (!maybeCallTramp) { + return false; + } + + Trampoline<MMPolicyT> callTramp(std::move(maybeCallTramp.ref())); + + // Write a null instance so that Clear() does not consider this tramp to + // be a normal tramp to be torn down. + callTramp.WriteEncodedPointer(nullptr); + // Use the second pointer slot to store a pointer to the primary tramp + callTramp.WriteEncodedPointer(aTrampPtr); + callTramp.StartExecutableCode(); + + // mov r11, address + callTramp.WriteByte(0x49); + callTramp.WriteByte(0xbb); + callTramp.WritePointer(aDest); + + // jmp r11 + callTramp.WriteByte(0x41); + callTramp.WriteByte(0xff); + callTramp.WriteByte(0xe3); + + void* callTrampStart = callTramp.EndExecutableCode(); + if (!callTrampStart) { + return false; + } + + target.WriteByte(0xB8); // MOV EAX, IMM32 + + // Assert that the topmost 33 bits are 0 + MOZ_ASSERT( + !(reinterpret_cast<uintptr_t>(callTrampStart) & (~0x7FFFFFFFULL))); + + target.WriteLong(static_cast<uint32_t>( + reinterpret_cast<uintptr_t>(callTrampStart) & 0x7FFFFFFFU)); + target.WriteByte(0x48); // REX.W + target.WriteByte(0x63); // MOVSXD r64, r/m32 + // dest: rax, src: eax + target.WriteByte(BuildModRmByte(kModReg, kRegAx, kRegAx)); + target.WriteByte(0xFF); // JMP /4 + target.WriteByte(BuildModRmByte(kModReg, 4, kRegAx)); // rax + + return true; + } +#endif // defined(_M_X64) + + void CreateTrampoline(ReadOnlyTargetFunction<MMPolicyT>& origBytes, + TrampPoolT* aTrampPool, Trampoline<MMPolicyT>& aTramp, + intptr_t aDest, void** aOutTramp) { + *aOutTramp = nullptr; + + Trampoline<MMPolicyT>& tramp = aTramp; + if (!tramp) { + this->SetLastDetourError( + DetourResultCode::DETOUR_PATCHER_INVALID_TRAMPOLINE); + return; + } + + // The beginning of the trampoline contains two pointer-width slots: + // [0]: |this|, so that we know whether the trampoline belongs to us; + // [1]: Pointer to original function, so that we can reset the hooked + // function to its original behavior upon destruction. In rare cases + // where the function was already a different trampoline, this is + // just a pointer to that trampoline's target address. + tramp.WriteEncodedPointer(this); + if (!tramp) { + this->SetLastDetourError( + DetourResultCode::DETOUR_PATCHER_WRITE_POINTER_ERROR); + return; + } + + auto clearInstanceOnFailure = MakeScopeExit([this, aOutTramp, &tramp, + &origBytes]() -> void { + // *aOutTramp is not set until CreateTrampoline has completed + // successfully, so we can use that to check for success. + if (*aOutTramp) { + return; + } + + // Clear the instance pointer so that we don't try to reset a + // nonexistent hook. + tramp.Rewind(); + tramp.WriteEncodedPointer(nullptr); + +#if defined(NIGHTLY_BUILD) + origBytes.Rewind(); + this->SetLastDetourError( + DetourResultCode::DETOUR_PATCHER_CREATE_TRAMPOLINE_ERROR); + DetourError& lastError = *this->mVMPolicy.mLastError; + size_t bytesToCapture = std::min( + ArrayLength(lastError.mOrigBytes), + static_cast<size_t>(PrimitiveT::GetWorstCaseRequiredBytesToPatch())); +# if defined(_M_ARM64) + size_t numInstructionsToCapture = bytesToCapture / sizeof(uint32_t); + auto origBytesDst = reinterpret_cast<uint32_t*>(lastError.mOrigBytes); + for (size_t i = 0; i < numInstructionsToCapture; ++i) { + origBytesDst[i] = origBytes.ReadNextInstruction(); + } +# else + for (size_t i = 0; i < bytesToCapture; ++i) { + lastError.mOrigBytes[i] = origBytes[i]; + } +# endif // defined(_M_ARM64) +#else + // Silence -Wunused-lambda-capture in non-Nightly. + Unused << this; + Unused << origBytes; +#endif // defined(NIGHTLY_BUILD) + }); + + tramp.WritePointer(origBytes.AsEncodedPtr()); + if (!tramp) { + return; + } + + if (PatchIfTargetIsRecognizedTrampoline(tramp, origBytes, aDest, + aOutTramp)) { + return; + } + + tramp.StartExecutableCode(); + + constexpr uint32_t kWorstCaseBytesRequired = + PrimitiveT::GetWorstCaseRequiredBytesToPatch(); + +#if defined(_M_IX86) + int pJmp32 = -1; + while (origBytes.GetOffset() < kWorstCaseBytesRequired) { + // Understand some simple instructions that might be found in a + // prologue; we might need to extend this as necessary. + // + // Note! If we ever need to understand jump instructions, we'll + // need to rewrite the displacement argument. + unsigned char prefixGroups; + int numPrefixBytes = CountPrefixBytes(origBytes, &prefixGroups); + if (numPrefixBytes < 0 || + (prefixGroups & (ePrefixGroup3 | ePrefixGroup4))) { + // Either the prefix sequence was bad, or there are prefixes that + // we don't currently support (groups 3 and 4) + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + + origBytes += numPrefixBytes; + if (*origBytes >= 0x88 && *origBytes <= 0x8B) { + // various MOVs + ++origBytes; + int len = CountModRmSib(origBytes); + if (len < 0) { + MOZ_ASSERT_UNREACHABLE("Unrecognized MOV opcode sequence"); + return; + } + origBytes += len; + } else if (*origBytes == 0x0f && + (origBytes[1] == 0x10 || origBytes[1] == 0x11)) { + // SSE: movups xmm, xmm/m128 + // movups xmm/m128, xmm + origBytes += 2; + int len = CountModRmSib(origBytes); + if (len < 0) { + MOZ_ASSERT_UNREACHABLE("Unrecognized MOV opcode sequence"); + return; + } + origBytes += len; + } else if (*origBytes == 0xA1) { + // MOV eax, [seg:offset] + origBytes += 5; + } else if (*origBytes == 0xB8) { + // MOV 0xB8: http://ref.x86asm.net/coder32.html#xB8 + origBytes += 5; + } else if (*origBytes == 0x33 && (origBytes[1] & kMaskMod) == kModReg) { + // XOR r32, r32 + origBytes += 2; + } else if ((*origBytes & 0xf8) == 0x40) { + // INC r32 + origBytes += 1; + } else if (*origBytes == 0x83) { + uint8_t mod = static_cast<uint8_t>(origBytes[1]) & kMaskMod; + uint8_t rm = static_cast<uint8_t>(origBytes[1]) & kMaskRm; + if (mod == kModReg) { + // ADD|OR|ADC|SBB|AND|SUB|XOR|CMP r, imm8 + origBytes += 3; + } else if (mod == kModDisp8 && rm != kRmNeedSib) { + // ADD|OR|ADC|SBB|AND|SUB|XOR|CMP [r+disp8], imm8 + origBytes += 4; + } else { + // bail + MOZ_ASSERT_UNREACHABLE("Unrecognized bit opcode sequence"); + return; + } + } else if (*origBytes == 0x68) { + // PUSH with 4-byte operand + origBytes += 5; + } else if ((*origBytes & 0xf0) == 0x50) { + // 1-byte PUSH/POP + ++origBytes; + } else if (*origBytes == 0x6A) { + // PUSH imm8 + origBytes += 2; + } else if (*origBytes == 0xe9) { + pJmp32 = origBytes.GetOffset(); + // jmp 32bit offset + origBytes += 5; + } else if (*origBytes == 0xff && origBytes[1] == 0x25) { + // jmp [disp32] + origBytes += 6; + } else if (*origBytes == 0xc2) { + // ret imm16. We can't handle this but it happens. We don't ASSERT but + // we do fail to hook. +# if defined(MOZILLA_INTERNAL_API) + NS_WARNING("Cannot hook method -- RET opcode found"); +# endif + return; + } else { + // printf ("Unknown x86 instruction byte 0x%02x, aborting trampoline\n", + // *origBytes); + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } + + // The trampoline is a copy of the instructions that we just traced, + // followed by a jump that we add below. + tramp.CopyFrom(origBytes.GetBaseAddress(), origBytes.GetOffset()); + if (!tramp) { + return; + } +#elif defined(_M_X64) + bool foundJmp = false; + // |use10BytePatch| should always default to |false| in production. It is + // not set to true unless we detect that a 10-byte patch is necessary. + // OTOH, for testing purposes, if we want to force a 10-byte patch, we + // always initialize |use10BytePatch| to |true|. + bool use10BytePatch = + (mFlags.value() & DetourFlags::eTestOnlyForceShortPatch) == + DetourFlags::eTestOnlyForceShortPatch; + const uint32_t bytesRequired = + use10BytePatch ? 10 : kWorstCaseBytesRequired; + + while (origBytes.GetOffset() < bytesRequired) { + // If we found JMP 32bit offset, we require that the next bytes must + // be NOP or INT3. There is no reason to copy them. + // TODO: This used to trigger for Je as well. Now that I allow + // instructions after CALL and JE, I don't think I need that. + // The only real value of this condition is that if code follows a JMP + // then its _probably_ the target of a JMP somewhere else and we + // will be overwriting it, which would be tragic. This seems + // highly unlikely. + if (foundJmp) { + if (*origBytes == 0x90 || *origBytes == 0xcc) { + ++origBytes; + continue; + } + + // If our trampoline space is located in the lowest 2GB, we can do a ten + // byte patch instead of a thirteen byte patch. + if (aTrampPool && aTrampPool->IsInLowest2GB() && + origBytes.GetOffset() >= 10) { + use10BytePatch = true; + break; + } + + MOZ_ASSERT_UNREACHABLE("Opcode sequence includes commands after JMP"); + return; + } + if (*origBytes == 0x0f) { + COPY_CODES(1); + if (*origBytes == 0x1f) { + // nop (multibyte) + COPY_CODES(1); + if ((*origBytes & 0xc0) == 0x40 && (*origBytes & 0x7) == 0x04) { + COPY_CODES(3); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x05) { + // syscall + COPY_CODES(1); + } else if (*origBytes == 0x10 || *origBytes == 0x11) { + // SSE: movups xmm, xmm/m128 + // movups xmm/m128, xmm + COPY_CODES(1); + int nModRmSibBytes = CountModRmSib(origBytes); + if (nModRmSibBytes < 0) { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } else { + COPY_CODES(nModRmSibBytes); + } + } else if (*origBytes >= 0x83 && *origBytes <= 0x85) { + // 0f 83 cd JAE rel32 + // 0f 84 cd JE rel32 + // 0f 85 cd JNE rel32 + const JumpType kJumpTypes[] = {JumpType::Jae, JumpType::Je, + JumpType::Jne}; + auto jumpType = kJumpTypes[*origBytes - 0x83]; + ++origBytes; + --tramp; // overwrite the 0x0f we copied above + + if (!GenerateJump(tramp, origBytes.ReadDisp32AsAbsolute(), + jumpType)) { + return; + } + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes >= 0x88 && *origBytes <= 0x8B) { + // various 32-bit MOVs + COPY_CODES(1); + int len = CountModRmSib(origBytes); + if (len < 0) { + MOZ_ASSERT_UNREACHABLE("Unrecognized MOV opcode sequence"); + return; + } + COPY_CODES(len); + } else if (*origBytes == 0x40 || *origBytes == 0x41) { + // Plain REX or REX.B + COPY_CODES(1); + if ((*origBytes & 0xf0) == 0x50) { + // push/pop with Rx register + COPY_CODES(1); + } else if (*origBytes >= 0xb8 && *origBytes <= 0xbf) { + // mov r32, imm32 + COPY_CODES(5); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x44) { + // REX.R + COPY_CODES(1); + + // TODO: Combine with the "0x89" case below in the REX.W section + if (*origBytes == 0x89) { + // mov r/m32, r32 + COPY_CODES(1); + int len = CountModRmSib(origBytes); + if (len < 0) { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + COPY_CODES(len); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x45) { + // REX.R & REX.B + COPY_CODES(1); + + if (*origBytes == 0x33) { + // xor r32, r32 + COPY_CODES(2); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if ((*origBytes & 0xfa) == 0x48) { + // REX.W | REX.WR | REX.WRB | REX.WB + COPY_CODES(1); + + if (*origBytes == 0x81 && (origBytes[1] & 0xf8) == 0xe8) { + // sub r, dword + COPY_CODES(6); + } else if (*origBytes == 0x83 && (origBytes[1] & 0xf8) == 0xe8) { + // sub r, byte + COPY_CODES(3); + } else if (*origBytes == 0x83 && + (origBytes[1] & (kMaskMod | kMaskReg)) == kModReg) { + // add r, byte + COPY_CODES(3); + } else if (*origBytes == 0x83 && (origBytes[1] & 0xf8) == 0x60) { + // and [r+d], imm8 + COPY_CODES(5); + } else if (*origBytes == 0x2b && (origBytes[1] & kMaskMod) == kModReg) { + // sub r64, r64 + COPY_CODES(2); + } else if (*origBytes == 0x85) { + // 85 /r => TEST r/m32, r32 + if ((origBytes[1] & 0xc0) == 0xc0) { + COPY_CODES(2); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if ((*origBytes & 0xfd) == 0x89) { + // MOV r/m64, r64 | MOV r64, r/m64 + BYTE reg; + int len = CountModRmSib(origBytes + 1, ®); + if (len < 0) { + MOZ_ASSERT(len == kModOperand64); + if (len != kModOperand64) { + return; + } + origBytes += 2; // skip the MOV and MOD R/M bytes + + // The instruction MOVs 64-bit data from a RIP-relative memory + // address (determined with a 32-bit offset from RIP) into a + // 64-bit register. + uintptr_t absAddr = origBytes.ReadDisp32AsAbsolute(); + + if (reg == kRegAx) { + // Destination is RAX. Encode instruction as MOVABS with a + // 64-bit absolute address as its immediate operand. + tramp.WriteByte(0xa1); + tramp.WritePointer(absAddr); + } else { + // The MOV must be done in two steps. First, we MOVABS the + // absolute 64-bit address into our target register. + // Then, we MOV from that address into the register + // using register-indirect addressing. + tramp.WriteByte(0xb8 + reg); + tramp.WritePointer(absAddr); + tramp.WriteByte(0x48); + tramp.WriteByte(0x8b); + tramp.WriteByte(BuildModRmByte(kModNoRegDisp, reg, reg)); + } + } else { + COPY_CODES(len + 1); + } + } else if ((*origBytes & 0xf8) == 0xb8) { + // MOV r64, imm64 + COPY_CODES(9); + } else if (*origBytes == 0xc7) { + // MOV r/m64, imm32 + if (origBytes[1] == 0x44) { + // MOV [r64+disp8], imm32 + // ModR/W + SIB + disp8 + imm32 + COPY_CODES(8); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0xff) { + // JMP /4 + if ((origBytes[1] & 0xc0) == 0x0 && (origBytes[1] & 0x07) == 0x5) { + origBytes += 2; + --tramp; // overwrite the REX.W/REX.RW we copied above + + if (!GenerateJump(tramp, origBytes.ChasePointerFromDisp(), + JumpType::Jmp)) { + return; + } + + foundJmp = true; + } else { + // not support yet! + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x8d) { + // LEA reg, addr + if ((origBytes[1] & kMaskMod) == 0x0 && + (origBytes[1] & kMaskRm) == 0x5) { + // [rip+disp32] + // convert 32bit offset to 64bit direct and convert instruction + // to a simple 64-bit mov + BYTE reg = (origBytes[1] & kMaskReg) >> kRegFieldShift; + origBytes += 2; + uintptr_t absAddr = origBytes.ReadDisp32AsAbsolute(); + tramp.WriteByte(0xb8 + reg); // move + tramp.WritePointer(absAddr); + } else { + // Above we dealt with RIP-relative instructions. Any other + // operand form can simply be copied. + int len = CountModRmSib(origBytes + 1); + // We handled the kModOperand64 -- ie RIP-relative -- case above + MOZ_ASSERT(len > 0); + COPY_CODES(len + 1); + } + } else if (*origBytes == 0x63 && (origBytes[1] & kMaskMod) == kModReg) { + // movsxd r64, r32 (move + sign extend) + COPY_CODES(2); + } else { + // not support yet! + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x66) { + // operand override prefix + COPY_CODES(1); + // This is the same as the x86 version + if (*origBytes >= 0x88 && *origBytes <= 0x8B) { + // various MOVs + unsigned char b = origBytes[1]; + if (((b & 0xc0) == 0xc0) || + (((b & 0xc0) == 0x00) && ((b & 0x07) != 0x04) && + ((b & 0x07) != 0x05))) { + // REG=r, R/M=r or REG=r, R/M=[r] + COPY_CODES(2); + } else if ((b & 0xc0) == 0x40) { + if ((b & 0x07) == 0x04) { + // REG=r, R/M=[SIB + disp8] + COPY_CODES(4); + } else { + // REG=r, R/M=[r + disp8] + COPY_CODES(3); + } + } else { + // complex MOV, bail + MOZ_ASSERT_UNREACHABLE("Unrecognized MOV opcode sequence"); + return; + } + } else if (*origBytes == 0x44 && origBytes[1] == 0x89) { + // mov word ptr [reg+disp8], reg + COPY_CODES(2); + int len = CountModRmSib(origBytes); + if (len < 0) { + // no way to support this yet. + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + COPY_CODES(len); + } + } else if ((*origBytes & 0xf0) == 0x50) { + // 1-byte push/pop + COPY_CODES(1); + } else if (*origBytes == 0x65) { + // GS prefix + // + // The entry of GetKeyState on Windows 10 has the following code. + // 65 48 8b 04 25 30 00 00 00 mov rax,qword ptr gs:[30h] + // (GS prefix + REX + MOV (0x8b) ...) + if (origBytes[1] == 0x48 && + (origBytes[2] >= 0x88 && origBytes[2] <= 0x8b)) { + COPY_CODES(3); + int len = CountModRmSib(origBytes); + if (len < 0) { + // no way to support this yet. + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + COPY_CODES(len); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x80 && origBytes[1] == 0x3d) { + origBytes += 2; + + // cmp byte ptr [rip-relative address], imm8 + // We'll compute the absolute address and do the cmp in r11 + + // push r11 (to save the old value) + tramp.WriteByte(0x49); + tramp.WriteByte(0x53); + + uintptr_t absAddr = origBytes.ReadDisp32AsAbsolute(); + + // mov r11, absolute address + tramp.WriteByte(0x49); + tramp.WriteByte(0xbb); + tramp.WritePointer(absAddr); + + // cmp byte ptr [r11],... + tramp.WriteByte(0x41); + tramp.WriteByte(0x80); + tramp.WriteByte(0x3b); + + // ...imm8 + COPY_CODES(1); + + // pop r11 (doesn't affect the flags from the cmp) + tramp.WriteByte(0x49); + tramp.WriteByte(0x5b); + } else if (*origBytes == 0x90) { + // nop + COPY_CODES(1); + } else if ((*origBytes & 0xf8) == 0xb8) { + // MOV r32, imm32 + COPY_CODES(5); + } else if (*origBytes == 0x33) { + // xor r32, r/m32 + COPY_CODES(2); + } else if (*origBytes == 0xf6) { + // test r/m8, imm8 (used by ntdll on Windows 10 x64) + // (no flags are affected by near jmp since there is no task switch, + // so it is ok for a jmp to be written immediately after a test) + BYTE subOpcode = 0; + int nModRmSibBytes = CountModRmSib(origBytes + 1, &subOpcode); + if (nModRmSibBytes < 0 || subOpcode != 0) { + // Unsupported + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + COPY_CODES(2 + nModRmSibBytes); + } else if (*origBytes == 0x85) { + // test r/m32, r32 + int nModRmSibBytes = CountModRmSib(origBytes + 1); + if (nModRmSibBytes < 0) { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + COPY_CODES(1 + nModRmSibBytes); + } else if (*origBytes == 0xd1 && (origBytes[1] & kMaskMod) == kModReg) { + // bit shifts/rotates : (SA|SH|RO|RC)(R|L) r32 + // (e.g. 0xd1 0xe0 is SAL, 0xd1 0xc8 is ROR) + COPY_CODES(2); + } else if (*origBytes == 0x83 && (origBytes[1] & kMaskMod) == kModReg) { + // ADD|OR|ADC|SBB|AND|SUB|XOR|CMP r, imm8 + COPY_CODES(3); + } else if (*origBytes == 0xc3) { + // ret + COPY_CODES(1); + } else if (*origBytes == 0xcc) { + // int 3 + COPY_CODES(1); + } else if (*origBytes == 0xe8 || *origBytes == 0xe9) { + // CALL (0xe8) or JMP (0xe9) 32bit offset + foundJmp = *origBytes == 0xe9; + ++origBytes; + + if (!GenerateJump(tramp, origBytes.ReadDisp32AsAbsolute(), + foundJmp ? JumpType::Jmp : JumpType::Call)) { + return; + } + } else if (*origBytes >= 0x73 && *origBytes <= 0x75) { + // 73 cb JAE rel8 + // 74 cb JE rel8 + // 75 cb JNE rel8 + const JumpType kJumpTypes[] = {JumpType::Jae, JumpType::Je, + JumpType::Jne}; + auto jumpType = kJumpTypes[*origBytes - 0x73]; + uint8_t offset = origBytes[1]; + + origBytes += 2; + + if (!GenerateJump(tramp, origBytes.OffsetToAbsolute(offset), + jumpType)) { + return; + } + } else if (*origBytes == 0xff) { + uint8_t mod = origBytes[1] & kMaskMod; + uint8_t reg = (origBytes[1] & kMaskReg) >> kRegFieldShift; + uint8_t rm = origBytes[1] & kMaskRm; + if (mod == kModReg && (reg == 0 || reg == 1 || reg == 2 || reg == 6)) { + // INC|DEC|CALL|PUSH r64 + COPY_CODES(2); + } else if (mod == kModNoRegDisp && reg == 2 && + rm == kRmNoRegDispDisp32) { + // FF 15 CALL [disp32] + origBytes += 2; + if (!GenerateJump(tramp, origBytes.ChasePointerFromDisp(), + JumpType::Call)) { + return; + } + } else if (reg == 4) { + // FF /4 (Opcode=ff, REG=4): JMP r/m + if (mod == kModNoRegDisp && rm == kRmNoRegDispDisp32) { + // FF 25 JMP [disp32] + foundJmp = true; + + origBytes += 2; + + uintptr_t jmpDest = origBytes.ChasePointerFromDisp(); + + if (!GenerateJump(tramp, jmpDest, JumpType::Jmp)) { + return; + } + } else { + // JMP r/m except JMP [disp32] + int len = CountModRmSib(origBytes + 1); + if (len < 0) { + // RIP-relative not yet supported + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + + COPY_CODES(len + 1); + + foundJmp = true; + } + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x83 && (origBytes[1] & 0xf8) == 0x60) { + // and [r+d], imm8 + COPY_CODES(5); + } else if (*origBytes == 0xc6) { + // mov [r+d], imm8 + int len = CountModRmSib(origBytes + 1); + if (len < 0) { + // RIP-relative not yet supported + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + COPY_CODES(len + 2); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } +#elif defined(_M_ARM64) + + // The number of bytes required to facilitate a detour depends on the + // proximity of the hook function to the target function. In the best case, + // we can branch within +/- 128MB of the current location, requiring only + // 4 bytes. In the worst case, we need 16 bytes to load an absolute address + // into a register and then branch to it. + const uint32_t bytesRequiredFromDecode = + (mFlags.value() & DetourFlags::eTestOnlyForceShortPatch) + ? 4 + : kWorstCaseBytesRequired; + + while (origBytes.GetOffset() < bytesRequiredFromDecode) { + uintptr_t curPC = origBytes.GetCurrentAbsolute(); + uint32_t curInst = origBytes.ReadNextInstruction(); + + Result<arm64::LoadOrBranch, arm64::PCRelCheckError> pcRelInfo = + arm64::CheckForPCRel(curPC, curInst); + if (pcRelInfo.isErr()) { + if (pcRelInfo.unwrapErr() == + arm64::PCRelCheckError::InstructionNotPCRel) { + // Instruction is not PC-relative, we can just copy it verbatim + tramp.WriteInstruction(curInst); + continue; + } + + // At this point we have determined that there is no decoder available + // for the current, PC-relative, instruction. + + // origBytes is now pointing one instruction past the one that we + // need the trampoline to jump back to. + if (!origBytes.BackUpOneInstruction()) { + return; + } + + break; + } + + // We need to load an absolute address into a particular register + tramp.WriteLoadLiteral(pcRelInfo.inspect().mAbsAddress, + pcRelInfo.inspect().mDestReg); + } + +#else +# error "Unknown processor type" +#endif + + if (origBytes.GetOffset() > 100) { + // printf ("Too big!"); + return; + } + +#if defined(_M_IX86) + if (pJmp32 >= 0) { + // Jump directly to the original target of the jump instead of jumping to + // the original function. Adjust jump target displacement to jump location + // in the trampoline. + tramp.AdjustDisp32AtOffset(pJmp32 + 1, origBytes.GetBaseAddress()); + } else { + tramp.WriteByte(0xe9); // jmp + tramp.WriteDisp32(origBytes.GetAddress()); + } +#elif defined(_M_X64) + // If we found a Jmp, we don't need to add another instruction. However, + // if we found a _conditional_ jump or a CALL (or no control operations + // at all) then we still need to run the rest of aOriginalFunction. + if (!foundJmp) { + if (!GenerateJump(tramp, origBytes.GetAddress(), JumpType::Jmp)) { + return; + } + } +#elif defined(_M_ARM64) + // Let's find out how many bytes we have available to us for patching + uint32_t numBytesForPatching = tramp.GetCurrentExecutableCodeLen(); + + if (!numBytesForPatching) { + // There's nothing we can do + return; + } + + if (tramp.IsNull()) { + // Recursive case + HMODULE targetModule = nullptr; + + if (numBytesForPatching < kWorstCaseBytesRequired) { + if (!::GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast<LPCWSTR>(origBytes.GetBaseAddress()), + &targetModule)) { + return; + } + } + + Maybe<TrampPoolT> maybeTrampPool = DoReserve(targetModule); + MOZ_ASSERT(maybeTrampPool); + if (!maybeTrampPool) { + return; + } + + Maybe<Trampoline<MMPolicyT>> maybeRealTramp( + maybeTrampPool.ref().GetNextTrampoline()); + if (!maybeRealTramp) { + return; + } + + origBytes.Rewind(); + CreateTrampoline(origBytes, maybeTrampPool.ptr(), maybeRealTramp.ref(), + aDest, aOutTramp); + return; + } + + // Write the branch from the trampoline back to the original code + + tramp.WriteLoadLiteral(origBytes.GetAddress(), 16); + tramp.WriteInstruction(arm64::BuildUnconditionalBranchToRegister(16)); +#else +# error "Unsupported processor architecture" +#endif + + // The trampoline is now complete. + void* trampPtr = tramp.EndExecutableCode(); + if (!trampPtr) { + return; + } + +#ifdef _M_X64 + if constexpr (MMPolicyT::kSupportsUnwindInfo) { + DebugOnly<bool> unwindInfoAdded = tramp.AddUnwindInfo( + origBytes.GetBaseAddress(), origBytes.GetOffset()); + MOZ_ASSERT(unwindInfoAdded); + } +#endif // _M_X64 + + WritableTargetFunction<MMPolicyT> target(origBytes.Promote()); + if (!target) { + return; + } + + do { + // Now patch the original function. + // When we're instructed to apply a non-default patch, apply it and exit. + // If non-default patching fails, bail out, no fallback. + // Otherwise, we go straight to the default patch. + +#if defined(_M_X64) + if (use10BytePatch) { + if (!Apply10BytePatch(aTrampPool, trampPtr, target, aDest)) { + return; + } + break; + } +#elif defined(_M_ARM64) + if (numBytesForPatching < kWorstCaseBytesRequired) { + if (!Apply4BytePatch(aTrampPool, trampPtr, target, aDest)) { + return; + } + break; + } +#endif + + PrimitiveT::ApplyDefaultPatch(target, aDest); + } while (false); + + if (!target.Commit()) { + return; + } + + // Output the trampoline, thus signalling that this call was a success + *aOutTramp = trampPtr; + } +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_PatcherDetour_h diff --git a/toolkit/xre/dllservices/mozglue/interceptor/PatcherNopSpace.h b/toolkit/xre/dllservices/mozglue/interceptor/PatcherNopSpace.h new file mode 100644 index 0000000000..deee87e0f8 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/PatcherNopSpace.h @@ -0,0 +1,205 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_interceptor_PatcherNopSpace_h +#define mozilla_interceptor_PatcherNopSpace_h + +#if defined(_M_IX86) + +# include "mozilla/interceptor/PatcherBase.h" + +namespace mozilla { +namespace interceptor { + +template <typename VMPolicy> +class WindowsDllNopSpacePatcher final : public WindowsDllPatcherBase<VMPolicy> { + typedef typename VMPolicy::MMPolicyT MMPolicyT; + + // For remembering the addresses of functions we've patched. + mozilla::Vector<void*> mPatchedFns; + + public: + template <typename... Args> + explicit WindowsDllNopSpacePatcher(Args&&... aArgs) + : WindowsDllPatcherBase<VMPolicy>(std::forward<Args>(aArgs)...) {} + + ~WindowsDllNopSpacePatcher() { Clear(); } + + WindowsDllNopSpacePatcher(const WindowsDllNopSpacePatcher&) = delete; + WindowsDllNopSpacePatcher(WindowsDllNopSpacePatcher&&) = delete; + WindowsDllNopSpacePatcher& operator=(const WindowsDllNopSpacePatcher&) = + delete; + WindowsDllNopSpacePatcher& operator=(WindowsDllNopSpacePatcher&&) = delete; + + void Clear() { + // Restore the mov edi, edi to the beginning of each function we patched. + + for (auto&& ptr : mPatchedFns) { + WritableTargetFunction<MMPolicyT> fn( + this->mVMPolicy, reinterpret_cast<uintptr_t>(ptr), sizeof(uint16_t)); + if (!fn) { + continue; + } + + // mov edi, edi + fn.CommitAndWriteShort(0xff8b); + } + + mPatchedFns.clear(); + } + + /** + * NVIDIA Optimus drivers utilize Microsoft Detours 2.x to patch functions + * in our address space. There is a bug in Detours 2.x that causes it to + * patch at the wrong address when attempting to detour code that is already + * NOP space patched. This function is an effort to detect the presence of + * this NVIDIA code in our address space and disable NOP space patching if it + * is. We also check AppInit_DLLs since this is the mechanism that the Optimus + * drivers use to inject into our process. + */ + static bool IsCompatible() { + // These DLLs are known to have bad interactions with this style of patching + const wchar_t* kIncompatibleDLLs[] = {L"detoured.dll", L"_etoured.dll", + L"nvd3d9wrap.dll", L"nvdxgiwrap.dll"}; + // See if the infringing DLLs are already loaded + for (unsigned int i = 0; i < mozilla::ArrayLength(kIncompatibleDLLs); ++i) { + if (GetModuleHandleW(kIncompatibleDLLs[i])) { + return false; + } + } + if (GetModuleHandleW(L"user32.dll")) { + // user32 is loaded but the infringing DLLs are not, assume we're safe to + // proceed. + return true; + } + // If user32 has not loaded yet, check AppInit_DLLs to ensure that Optimus + // won't be loaded once user32 is initialized. + HKEY hkey = NULL; + if (!RegOpenKeyExW( + HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Windows", 0, + KEY_QUERY_VALUE, &hkey)) { + nsAutoRegKey key(hkey); + DWORD numBytes = 0; + const wchar_t kAppInitDLLs[] = L"AppInit_DLLs"; + // Query for required buffer size + LONG status = RegQueryValueExW(hkey, kAppInitDLLs, nullptr, nullptr, + nullptr, &numBytes); + mozilla::UniquePtr<wchar_t[]> data; + if (!status) { + // Allocate the buffer and query for the actual data + data = mozilla::MakeUnique<wchar_t[]>((numBytes + 1) / sizeof(wchar_t)); + status = RegQueryValueExW(hkey, kAppInitDLLs, nullptr, nullptr, + (LPBYTE)data.get(), &numBytes); + } + if (!status) { + // For each token, split up the filename components and then check the + // name of the file. + const wchar_t kDelimiters[] = L", "; + wchar_t* tokenContext = nullptr; + wchar_t* token = wcstok_s(data.get(), kDelimiters, &tokenContext); + while (token) { + wchar_t fname[_MAX_FNAME] = {0}; + if (!_wsplitpath_s(token, nullptr, 0, nullptr, 0, fname, + mozilla::ArrayLength(fname), nullptr, 0)) { + // nvinit.dll is responsible for bootstrapping the DLL injection, so + // that is the library that we check for here + const wchar_t kNvInitName[] = L"nvinit"; + if (!_wcsnicmp(fname, kNvInitName, + mozilla::ArrayLength(kNvInitName))) { + return false; + } + } + token = wcstok_s(nullptr, kDelimiters, &tokenContext); + } + } + } + return true; + } + + bool AddHook(FARPROC aTargetFn, intptr_t aHookDest, void** aOrigFunc) { + if (!IsCompatible()) { +# if defined(MOZILLA_INTERNAL_API) + NS_WARNING("NOP space patching is unavailable for compatibility reasons"); +# endif + return false; + } + + MOZ_ASSERT(aTargetFn); + if (!aTargetFn) { + return false; + } + + ReadOnlyTargetFunction<MMPolicyT> readOnlyTargetFn( + this->ResolveRedirectedAddress(aTargetFn)); + + if (!WriteHook(readOnlyTargetFn, aHookDest, aOrigFunc)) { + return false; + } + + return mPatchedFns.append( + reinterpret_cast<void*>(readOnlyTargetFn.GetBaseAddress())); + } + + bool WriteHook(const ReadOnlyTargetFunction<MMPolicyT>& aFn, + intptr_t aHookDest, void** aOrigFunc) { + // Ensure we can read and write starting at fn - 5 (for the long jmp we're + // going to write) and ending at fn + 2 (for the short jmp up to the long + // jmp). These bytes may span two pages with different protection. + WritableTargetFunction<MMPolicyT> writableFn(aFn.Promote(7, -5)); + if (!writableFn) { + return false; + } + + // Check that the 5 bytes before the function are NOP's or INT 3's, + const uint8_t nopOrBp[] = {0x90, 0xCC}; + if (!writableFn.template VerifyValuesAreOneOf<uint8_t, 5>(nopOrBp)) { + return false; + } + + // ... and that the first 2 bytes of the function are mov(edi, edi). + // There are two ways to encode the same thing: + // + // 0x89 0xff == mov r/m, r + // 0x8b 0xff == mov r, r/m + // + // where "r" is register and "r/m" is register or memory. + // Windows seems to use 0x8B 0xFF. We include 0x89 0xFF out of paranoia. + + // (These look backwards because little-endian) + const uint16_t possibleEncodings[] = {0xFF8B, 0xFF89}; + if (!writableFn.template VerifyValuesAreOneOf<uint16_t, 1>( + possibleEncodings, 5)) { + return false; + } + + // Write a long jump into the space above the function. + writableFn.WriteByte(0xe9); // jmp + if (!writableFn) { + return false; + } + + writableFn.WriteDisp32(aHookDest); // target + if (!writableFn) { + return false; + } + + // Set aOrigFunc here, because after this point, aHookDest might be called, + // and aHookDest might use the aOrigFunc pointer. + *aOrigFunc = reinterpret_cast<void*>(writableFn.GetCurrentAddress() + + sizeof(uint16_t)); + + // Short jump up into our long jump. + return writableFn.CommitAndWriteShort(0xF9EB); // jmp $-5 + } +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // defined(_M_IX86) + +#endif // mozilla_interceptor_PatcherNopSpace_h diff --git a/toolkit/xre/dllservices/mozglue/interceptor/RangeMap.h b/toolkit/xre/dllservices/mozglue/interceptor/RangeMap.h new file mode 100644 index 0000000000..d45d031613 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/RangeMap.h @@ -0,0 +1,142 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_interceptor_RangeMap_h +#define mozilla_interceptor_RangeMap_h + +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/mozalloc.h" +#include "mozilla/Span.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Vector.h" + +#include <algorithm> + +namespace mozilla { +namespace interceptor { + +/** + * This class maintains a vector of VMSharingPolicyUnique objects, sorted on + * the memory range that is used for reserving each object. + * + * This is used by VMSharingPolicyShared for creating and looking up VM regions + * that are within proximity of the applicable range. + * + * VMSharingPolicyUnique objects managed by this class are reused whenever + * possible. If no range is required, we just return the first available + * policy. + * + * If no range is required and no policies have yet been allocated, we create + * a new one with a null range as a default. + */ +template <typename MMPolicyT> +class RangeMap final { + private: + /** + * This class is used as the comparison key for sorting and insertion. + */ + class Range { + public: + constexpr Range() : mBase(0), mLimit(0) {} + + explicit Range(const Maybe<Span<const uint8_t>>& aBounds) + : mBase(aBounds ? reinterpret_cast<const uintptr_t>( + MMPolicyT::GetLowerBound(aBounds.ref())) + : 0), + mLimit(aBounds ? reinterpret_cast<const uintptr_t>( + MMPolicyT::GetUpperBoundIncl(aBounds.ref())) + : 0) {} + + Range& operator=(const Range&) = default; + Range(const Range&) = default; + Range(Range&&) = default; + Range& operator=(Range&&) = default; + + bool operator<(const Range& aOther) const { + return mBase < aOther.mBase || + (mBase == aOther.mBase && mLimit < aOther.mLimit); + } + + bool Contains(const Range& aOther) const { + return mBase <= aOther.mBase && mLimit >= aOther.mLimit; + } + + private: + uintptr_t mBase; + uintptr_t mLimit; + }; + + class PolicyInfo final : public Range { + public: + explicit PolicyInfo(const Range& aRange) + : Range(aRange), + mPolicy(MakeUnique<VMSharingPolicyUnique<MMPolicyT>>()) {} + + PolicyInfo(const PolicyInfo&) = delete; + PolicyInfo& operator=(const PolicyInfo&) = delete; + + PolicyInfo(PolicyInfo&& aOther) = default; + PolicyInfo& operator=(PolicyInfo&& aOther) = default; + + VMSharingPolicyUnique<MMPolicyT>* GetPolicy() { return mPolicy.get(); } + + private: + UniquePtr<VMSharingPolicyUnique<MMPolicyT>> mPolicy; + }; + + using VectorType = Vector<PolicyInfo, 0, InfallibleAllocPolicy>; + + public: + constexpr RangeMap() : mPolicies(nullptr) {} + + VMSharingPolicyUnique<MMPolicyT>* GetPolicy( + const Maybe<Span<const uint8_t>>& aBounds) { + Range testRange(aBounds); + + if (!mPolicies) { + mPolicies = new VectorType(); + } + + // If no bounds are specified, we just use the first available policy + if (!aBounds) { + if (mPolicies->empty()) { + if (!mPolicies->append(PolicyInfo(testRange))) { + return nullptr; + } + } + + return GetFirstPolicy(); + } + + // mPolicies is sorted, so we search + auto itr = + std::lower_bound(mPolicies->begin(), mPolicies->end(), testRange); + if (itr != mPolicies->end() && itr->Contains(testRange)) { + return itr->GetPolicy(); + } + + itr = mPolicies->insert(itr, PolicyInfo(testRange)); + + MOZ_ASSERT(std::is_sorted(mPolicies->begin(), mPolicies->end())); + + return itr->GetPolicy(); + } + + private: + VMSharingPolicyUnique<MMPolicyT>* GetFirstPolicy() { + MOZ_RELEASE_ASSERT(mPolicies && !mPolicies->empty()); + return mPolicies->begin()->GetPolicy(); + } + + private: + VectorType* mPolicies; +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_RangeMap_h diff --git a/toolkit/xre/dllservices/mozglue/interceptor/TargetFunction.h b/toolkit/xre/dllservices/mozglue/interceptor/TargetFunction.h new file mode 100644 index 0000000000..40be1ad08b --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/TargetFunction.h @@ -0,0 +1,1000 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_interceptor_TargetFunction_h +#define mozilla_interceptor_TargetFunction_h + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/BinarySearch.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/Maybe.h" +#include "mozilla/Tuple.h" +#include "mozilla/Types.h" +#include "mozilla/Unused.h" +#include "mozilla/Vector.h" + +#include <memory> +#include <type_traits> + +namespace mozilla { +namespace interceptor { + +#if defined(_M_IX86) + +template <typename T> +bool CommitAndWriteShortInternal(const T& aMMPolicy, void* aDest, + uint16_t aValue); + +template <> +inline bool CommitAndWriteShortInternal<MMPolicyInProcess>( + const MMPolicyInProcess& aMMPolicy, void* aDest, uint16_t aValue) { + return aMMPolicy.WriteAtomic(aDest, aValue); +} + +template <> +inline bool CommitAndWriteShortInternal<MMPolicyOutOfProcess>( + const MMPolicyOutOfProcess& aMMPolicy, void* aDest, uint16_t aValue) { + return aMMPolicy.Write(aDest, &aValue, sizeof(uint16_t)); +} + +#endif // defined(_M_IX86) + +// Forward declaration +template <typename MMPolicy> +class ReadOnlyTargetFunction; + +template <typename MMPolicy> +class MOZ_STACK_CLASS WritableTargetFunction final { + class AutoProtect final { + using ProtectParams = Tuple<uintptr_t, uint32_t>; + + public: + explicit AutoProtect(const MMPolicy& aMMPolicy) : mMMPolicy(aMMPolicy) {} + + AutoProtect(const MMPolicy& aMMPolicy, uintptr_t aAddr, size_t aNumBytes, + uint32_t aNewProt) + : mMMPolicy(aMMPolicy) { + const uint32_t pageSize = mMMPolicy.GetPageSize(); + const uintptr_t limit = aAddr + aNumBytes - 1; + const uintptr_t limitPageNum = limit / pageSize; + const uintptr_t basePageNum = aAddr / pageSize; + const uintptr_t numPagesToChange = limitPageNum - basePageNum + 1; + + // We'll use the base address of the page instead of aAddr + uintptr_t curAddr = basePageNum * pageSize; + + // Now change the protection on each page + for (uintptr_t curPage = 0; curPage < numPagesToChange; + ++curPage, curAddr += pageSize) { + uint32_t prevProt; + if (!aMMPolicy.Protect(reinterpret_cast<void*>(curAddr), pageSize, + aNewProt, &prevProt)) { + Clear(); + return; + } + + // Save the previous protection for curAddr so that we can revert this + // in the destructor. + if (!mProtects.append(MakeTuple(curAddr, prevProt))) { + Clear(); + return; + } + } + } + + AutoProtect(AutoProtect&& aOther) + : mMMPolicy(aOther.mMMPolicy), mProtects(std::move(aOther.mProtects)) { + aOther.mProtects.clear(); + } + + ~AutoProtect() { Clear(); } + + explicit operator bool() const { return !mProtects.empty(); } + + AutoProtect(const AutoProtect&) = delete; + AutoProtect& operator=(const AutoProtect&) = delete; + AutoProtect& operator=(AutoProtect&&) = delete; + + private: + void Clear() { + const uint32_t pageSize = mMMPolicy.GetPageSize(); + for (auto&& entry : mProtects) { + uint32_t prevProt; + DebugOnly<bool> ok = + mMMPolicy.Protect(reinterpret_cast<void*>(Get<0>(entry)), pageSize, + Get<1>(entry), &prevProt); + MOZ_ASSERT(ok); + } + + mProtects.clear(); + } + + private: + const MMPolicy& mMMPolicy; + // We include two entries of inline storage as that is most common in the + // worst case. + Vector<ProtectParams, 2> mProtects; + }; + + public: + /** + * Used to initialize an invalid WritableTargetFunction, thus signalling an + * error. + */ + explicit WritableTargetFunction(const MMPolicy& aMMPolicy) + : mMMPolicy(aMMPolicy), + mFunc(0), + mNumBytes(0), + mOffset(0), + mStartWriteOffset(0), + mAccumulatedStatus(false), + mProtect(aMMPolicy) {} + + WritableTargetFunction(const MMPolicy& aMMPolicy, uintptr_t aFunc, + size_t aNumBytes) + : mMMPolicy(aMMPolicy), + mFunc(aFunc), + mNumBytes(aNumBytes), + mOffset(0), + mStartWriteOffset(0), + mAccumulatedStatus(true), + mProtect(aMMPolicy, aFunc, aNumBytes, PAGE_EXECUTE_READWRITE) {} + + WritableTargetFunction(WritableTargetFunction&& aOther) + : mMMPolicy(aOther.mMMPolicy), + mFunc(aOther.mFunc), + mNumBytes(aOther.mNumBytes), + mOffset(aOther.mOffset), + mStartWriteOffset(aOther.mStartWriteOffset), + mLocalBytes(std::move(aOther.mLocalBytes)), + mAccumulatedStatus(aOther.mAccumulatedStatus), + mProtect(std::move(aOther.mProtect)) { + aOther.mAccumulatedStatus = false; + } + + ~WritableTargetFunction() { + MOZ_ASSERT(mLocalBytes.empty(), "Did you forget to call Commit?"); + } + + WritableTargetFunction(const WritableTargetFunction&) = delete; + WritableTargetFunction& operator=(const WritableTargetFunction&) = delete; + WritableTargetFunction& operator=(WritableTargetFunction&&) = delete; + + /** + * @return true if data was successfully committed. + */ + bool Commit() { + if (!(*this)) { + return false; + } + + if (mLocalBytes.empty()) { + // Nothing to commit, treat like success + return true; + } + + bool ok = + mMMPolicy.Write(reinterpret_cast<void*>(mFunc + mStartWriteOffset), + mLocalBytes.begin(), mLocalBytes.length()); + if (!ok) { + return false; + } + + mMMPolicy.FlushInstructionCache(); + + mStartWriteOffset += mLocalBytes.length(); + + mLocalBytes.clear(); + return true; + } + + explicit operator bool() const { return mProtect && mAccumulatedStatus; } + + void WriteByte(const uint8_t& aValue) { + if (!mLocalBytes.append(aValue)) { + mAccumulatedStatus = false; + return; + } + + mOffset += sizeof(uint8_t); + } + + Maybe<uint8_t> ReadByte() { + // Reading is only permitted prior to any writing + MOZ_ASSERT(mOffset == mStartWriteOffset); + if (mOffset > mStartWriteOffset) { + mAccumulatedStatus = false; + return Nothing(); + } + + uint8_t value; + if (!mMMPolicy.Read(&value, reinterpret_cast<const void*>(mFunc + mOffset), + sizeof(uint8_t))) { + mAccumulatedStatus = false; + return Nothing(); + } + + mOffset += sizeof(uint8_t); + mStartWriteOffset += sizeof(uint8_t); + return Some(value); + } + + Maybe<uintptr_t> ReadEncodedPtr() { + // Reading is only permitted prior to any writing + MOZ_ASSERT(mOffset == mStartWriteOffset); + if (mOffset > mStartWriteOffset) { + mAccumulatedStatus = false; + return Nothing(); + } + + uintptr_t value; + if (!mMMPolicy.Read(&value, reinterpret_cast<const void*>(mFunc + mOffset), + sizeof(uintptr_t))) { + mAccumulatedStatus = false; + return Nothing(); + } + + mOffset += sizeof(uintptr_t); + mStartWriteOffset += sizeof(uintptr_t); + return Some(ReadOnlyTargetFunction<MMPolicy>::DecodePtr(value)); + } + + Maybe<uint32_t> ReadLong() { + // Reading is only permitted prior to any writing + MOZ_ASSERT(mOffset == mStartWriteOffset); + if (mOffset > mStartWriteOffset) { + mAccumulatedStatus = false; + return Nothing(); + } + + uint32_t value; + if (!mMMPolicy.Read(&value, reinterpret_cast<const void*>(mFunc + mOffset), + sizeof(uint32_t))) { + mAccumulatedStatus = false; + return Nothing(); + } + + mOffset += sizeof(uint32_t); + mStartWriteOffset += sizeof(uint32_t); + return Some(value); + } + + void WriteShort(const uint16_t& aValue) { + if (!mLocalBytes.append(reinterpret_cast<const uint8_t*>(&aValue), + sizeof(uint16_t))) { + mAccumulatedStatus = false; + return; + } + + mOffset += sizeof(uint16_t); + } + +#if defined(_M_IX86) + public: + /** + * Commits any dirty writes, and then writes a short, atomically if possible. + * This call may succeed in both inproc and outproc cases, but atomicity + * is only guaranteed in the inproc case. + */ + bool CommitAndWriteShort(const uint16_t aValue) { + // First, commit everything that has been written until now + if (!Commit()) { + return false; + } + + // Now immediately write the short, atomically if inproc + bool ok = CommitAndWriteShortInternal( + mMMPolicy, reinterpret_cast<void*>(mFunc + mStartWriteOffset), aValue); + if (!ok) { + return false; + } + + mMMPolicy.FlushInstructionCache(); + mStartWriteOffset += sizeof(uint16_t); + return true; + } +#endif // defined(_M_IX86) + + void WriteDisp32(const uintptr_t aAbsTarget) { + intptr_t diff = static_cast<intptr_t>(aAbsTarget) - + static_cast<intptr_t>(mFunc + mOffset + sizeof(int32_t)); + + CheckedInt<int32_t> checkedDisp(diff); + MOZ_ASSERT(checkedDisp.isValid()); + if (!checkedDisp.isValid()) { + mAccumulatedStatus = false; + return; + } + + int32_t disp = checkedDisp.value(); + if (!mLocalBytes.append(reinterpret_cast<uint8_t*>(&disp), + sizeof(int32_t))) { + mAccumulatedStatus = false; + return; + } + + mOffset += sizeof(int32_t); + } + +#if defined(_M_X64) || defined(_M_ARM64) + void WriteLong(const uint32_t aValue) { + if (!mLocalBytes.append(reinterpret_cast<const uint8_t*>(&aValue), + sizeof(uint32_t))) { + mAccumulatedStatus = false; + return; + } + + mOffset += sizeof(uint32_t); + } +#endif // defined(_M_X64) + + void WritePointer(const uintptr_t aAbsTarget) { + if (!mLocalBytes.append(reinterpret_cast<const uint8_t*>(&aAbsTarget), + sizeof(uintptr_t))) { + mAccumulatedStatus = false; + return; + } + + mOffset += sizeof(uintptr_t); + } + + /** + * @param aValues N-sized array of type T that specifies the set of values + * that are permissible in the first M bytes of the target + * function at aOffset. + * @return true if M values of type T in the function are members of the + * set specified by aValues. + */ + template <typename T, size_t M, size_t N> + bool VerifyValuesAreOneOf(const T (&aValues)[N], const uint8_t aOffset = 0) { + T buf[M]; + if (!mMMPolicy.Read( + buf, reinterpret_cast<const void*>(mFunc + mOffset + aOffset), + M * sizeof(T))) { + return false; + } + + for (auto&& fnValue : buf) { + bool match = false; + for (auto&& testValue : aValues) { + match |= (fnValue == testValue); + } + + if (!match) { + return false; + } + } + + return true; + } + + uintptr_t GetCurrentAddress() const { return mFunc + mOffset; } + + private: + const MMPolicy& mMMPolicy; + const uintptr_t mFunc; + const size_t mNumBytes; + uint32_t mOffset; + uint32_t mStartWriteOffset; + + // In an ideal world, we'd only read 5 bytes on 32-bit and 13 bytes on 64-bit, + // to match the minimum bytes that we need to write in in order to patch the + // target function. Since the actual opcodes will often require us to pull in + // extra bytes above that minimum, we set the inline storage to be larger than + // those minima in an effort to give the Vector extra wiggle room before it + // needs to touch the heap. +#if defined(_M_IX86) + static const size_t kInlineStorage = 16; +#elif defined(_M_X64) || defined(_M_ARM64) + static const size_t kInlineStorage = 32; +#endif + Vector<uint8_t, kInlineStorage> mLocalBytes; + bool mAccumulatedStatus; + AutoProtect mProtect; +}; + +template <typename MMPolicy> +class ReadOnlyTargetBytes { + public: + ReadOnlyTargetBytes(const MMPolicy& aMMPolicy, const void* aBase) + : mMMPolicy(aMMPolicy), mBase(reinterpret_cast<const uint8_t*>(aBase)) {} + + ReadOnlyTargetBytes(ReadOnlyTargetBytes&& aOther) + : mMMPolicy(aOther.mMMPolicy), mBase(aOther.mBase) {} + + ReadOnlyTargetBytes(const ReadOnlyTargetBytes& aOther, + const uint32_t aOffsetFromOther = 0) + : mMMPolicy(aOther.mMMPolicy), mBase(aOther.mBase + aOffsetFromOther) {} + + void EnsureLimit(uint32_t aDesiredLimit) { + // In the out-proc case we use this function to read the target function's + // bytes in the other process into a local buffer. We don't need that for + // the in-process case because we already have direct access to our target + // function's bytes. + } + + uint32_t TryEnsureLimit(uint32_t aDesiredLimit) { + // Same as EnsureLimit above. We don't need to ensure for the in-process. + return aDesiredLimit; + } + + bool IsValidAtOffset(const int8_t aOffset) const { + if (!aOffset) { + return true; + } + + uintptr_t base = reinterpret_cast<uintptr_t>(mBase); + uintptr_t adjusted = base + aOffset; + uint32_t pageSize = mMMPolicy.GetPageSize(); + + // If |adjusted| is within the same page as |mBase|, we're still valid + if ((base / pageSize) == (adjusted / pageSize)) { + return true; + } + + // Otherwise, let's query |adjusted| + return mMMPolicy.IsPageAccessible(adjusted); + } + + /** + * This returns a pointer to a *potentially local copy* of the target + * function's bytes. The returned pointer should not be used for any + * pointer arithmetic relating to the target function. + */ + const uint8_t* GetLocalBytes() const { return mBase; } + + /** + * This returns a pointer to the target function's bytes. The returned pointer + * may possibly belong to another process, so while it should be used for + * pointer arithmetic, it *must not* be dereferenced. + */ + uintptr_t GetBase() const { return reinterpret_cast<uintptr_t>(mBase); } + + const MMPolicy& GetMMPolicy() const { return mMMPolicy; } + + ReadOnlyTargetBytes& operator=(const ReadOnlyTargetBytes&) = delete; + ReadOnlyTargetBytes& operator=(ReadOnlyTargetBytes&&) = delete; + + private: + const MMPolicy& mMMPolicy; + uint8_t const* const mBase; +}; + +template <> +class ReadOnlyTargetBytes<MMPolicyOutOfProcess> { + public: + ReadOnlyTargetBytes(const MMPolicyOutOfProcess& aMMPolicy, const void* aBase) + : mMMPolicy(aMMPolicy), mBase(reinterpret_cast<const uint8_t*>(aBase)) {} + + ReadOnlyTargetBytes(ReadOnlyTargetBytes&& aOther) + : mMMPolicy(aOther.mMMPolicy), + mLocalBytes(std::move(aOther.mLocalBytes)), + mBase(aOther.mBase) {} + + ReadOnlyTargetBytes(const ReadOnlyTargetBytes& aOther) + : mMMPolicy(aOther.mMMPolicy), mBase(aOther.mBase) { + Unused << mLocalBytes.appendAll(aOther.mLocalBytes); + } + + ReadOnlyTargetBytes(const ReadOnlyTargetBytes& aOther, + const uint32_t aOffsetFromOther) + : mMMPolicy(aOther.mMMPolicy), mBase(aOther.mBase + aOffsetFromOther) { + if (aOffsetFromOther >= aOther.mLocalBytes.length()) { + return; + } + + Unused << mLocalBytes.append(aOther.mLocalBytes.begin() + aOffsetFromOther, + aOther.mLocalBytes.end()); + } + + void EnsureLimit(uint32_t aDesiredLimit) { + size_t prevSize = mLocalBytes.length(); + if (aDesiredLimit < prevSize) { + return; + } + + size_t newSize = aDesiredLimit + 1; + if (newSize < kInlineStorage) { + // Always try to read as much memory as we can at once + newSize = kInlineStorage; + } + + bool resizeOk = mLocalBytes.resize(newSize); + MOZ_RELEASE_ASSERT(resizeOk); + + bool ok = mMMPolicy.Read(&mLocalBytes[prevSize], mBase + prevSize, + newSize - prevSize); + if (ok) { + return; + } + + // We couldn't pull more bytes than needed (which may happen if those extra + // bytes are not accessible). In this case, we try just to get the bare + // minimum. + newSize = aDesiredLimit + 1; + resizeOk = mLocalBytes.resize(newSize); + MOZ_RELEASE_ASSERT(resizeOk); + + ok = mMMPolicy.Read(&mLocalBytes[prevSize], mBase + prevSize, + newSize - prevSize); + MOZ_RELEASE_ASSERT(ok); + } + + // This function tries to ensure as many bytes as possible up to + // |aDesiredLimit| bytes, returning how many bytes were actually ensured. + // As EnsureLimit does, we allocate an extra byte in local to make sure + // mLocalBytes always has at least one byte even though the target memory + // was inaccessible at all. + uint32_t TryEnsureLimit(uint32_t aDesiredLimit) { + size_t prevSize = mLocalBytes.length(); + if (aDesiredLimit < prevSize) { + return aDesiredLimit; + } + + size_t newSize = aDesiredLimit; + if (newSize < kInlineStorage) { + // Always try to read as much memory as we can at once + newSize = kInlineStorage; + } + + bool resizeOk = mLocalBytes.resize(newSize); + MOZ_RELEASE_ASSERT(resizeOk); + + size_t bytesRead = mMMPolicy.TryRead(&mLocalBytes[prevSize], + mBase + prevSize, newSize - prevSize); + + newSize = prevSize + bytesRead; + + resizeOk = mLocalBytes.resize(newSize + 1); + MOZ_RELEASE_ASSERT(resizeOk); + + mLocalBytes[newSize] = 0; + return newSize; + } + + bool IsValidAtOffset(const int8_t aOffset) const { + if (!aOffset) { + return true; + } + + uintptr_t base = reinterpret_cast<uintptr_t>(mBase); + uintptr_t adjusted = base + aOffset; + uint32_t pageSize = mMMPolicy.GetPageSize(); + + // If |adjusted| is within the same page as |mBase|, we're still valid + if ((base / pageSize) == (adjusted / pageSize)) { + return true; + } + + // Otherwise, let's query |adjusted| + return mMMPolicy.IsPageAccessible(adjusted); + } + + /** + * This returns a pointer to a *potentially local copy* of the target + * function's bytes. The returned pointer should not be used for any + * pointer arithmetic relating to the target function. + */ + const uint8_t* GetLocalBytes() const { + if (mLocalBytes.empty()) { + return nullptr; + } + + return mLocalBytes.begin(); + } + + /** + * This returns a pointer to the target function's bytes. The returned pointer + * may possibly belong to another process, so while it should be used for + * pointer arithmetic, it *must not* be dereferenced. + */ + uintptr_t GetBase() const { return reinterpret_cast<uintptr_t>(mBase); } + + const MMPolicyOutOfProcess& GetMMPolicy() const { return mMMPolicy; } + + ReadOnlyTargetBytes& operator=(const ReadOnlyTargetBytes&) = delete; + ReadOnlyTargetBytes& operator=(ReadOnlyTargetBytes&&) = delete; + + private: + // In an ideal world, we'd only read 5 bytes on 32-bit and 13 bytes on 64-bit, + // to match the minimum bytes that we need to write in in order to patch the + // target function. Since the actual opcodes will often require us to pull in + // extra bytes above that minimum, we set the inline storage to be larger than + // those minima in an effort to give the Vector extra wiggle room before it + // needs to touch the heap. +#if defined(_M_IX86) + static const size_t kInlineStorage = 16; +#elif defined(_M_X64) || defined(_M_ARM64) + static const size_t kInlineStorage = 32; +#endif + + const MMPolicyOutOfProcess& mMMPolicy; + Vector<uint8_t, kInlineStorage> mLocalBytes; + uint8_t const* const mBase; +}; + +template <typename MMPolicy> +class TargetBytesPtr { + public: + typedef TargetBytesPtr<MMPolicy> Type; + + static Type Make(const MMPolicy& aMMPolicy, const void* aFunc) { + return TargetBytesPtr(aMMPolicy, aFunc); + } + + static Type CopyFromOffset(const TargetBytesPtr& aOther, + const uint32_t aOffsetFromOther) { + return TargetBytesPtr(aOther, aOffsetFromOther); + } + + ReadOnlyTargetBytes<MMPolicy>* operator->() { return &mTargetBytes; } + + TargetBytesPtr(TargetBytesPtr&& aOther) + : mTargetBytes(std::move(aOther.mTargetBytes)) {} + + TargetBytesPtr(const TargetBytesPtr& aOther) + : mTargetBytes(aOther.mTargetBytes) {} + + TargetBytesPtr& operator=(const TargetBytesPtr&) = delete; + TargetBytesPtr& operator=(TargetBytesPtr&&) = delete; + + private: + TargetBytesPtr(const MMPolicy& aMMPolicy, const void* aFunc) + : mTargetBytes(aMMPolicy, aFunc) {} + + TargetBytesPtr(const TargetBytesPtr& aOther, const uint32_t aOffsetFromOther) + : mTargetBytes(aOther.mTargetBytes, aOffsetFromOther) {} + + ReadOnlyTargetBytes<MMPolicy> mTargetBytes; +}; + +template <> +class TargetBytesPtr<MMPolicyOutOfProcess> { + public: + typedef std::shared_ptr<ReadOnlyTargetBytes<MMPolicyOutOfProcess>> Type; + + static Type Make(const MMPolicyOutOfProcess& aMMPolicy, const void* aFunc) { + return std::make_shared<ReadOnlyTargetBytes<MMPolicyOutOfProcess>>( + aMMPolicy, aFunc); + } + + static Type CopyFromOffset(const Type& aOther, + const uint32_t aOffsetFromOther) { + return std::make_shared<ReadOnlyTargetBytes<MMPolicyOutOfProcess>>( + *aOther, aOffsetFromOther); + } +}; + +template <typename MMPolicy> +class MOZ_STACK_CLASS ReadOnlyTargetFunction final { + public: + ReadOnlyTargetFunction(const MMPolicy& aMMPolicy, const void* aFunc) + : mTargetBytes(TargetBytesPtr<MMPolicy>::Make(aMMPolicy, aFunc)), + mOffset(0) {} + + ReadOnlyTargetFunction(const MMPolicy& aMMPolicy, FARPROC aFunc) + : mTargetBytes(TargetBytesPtr<MMPolicy>::Make( + aMMPolicy, reinterpret_cast<const void*>(aFunc))), + mOffset(0) {} + + ReadOnlyTargetFunction(const MMPolicy& aMMPolicy, uintptr_t aFunc) + : mTargetBytes(TargetBytesPtr<MMPolicy>::Make( + aMMPolicy, reinterpret_cast<const void*>(aFunc))), + mOffset(0) {} + + ReadOnlyTargetFunction(ReadOnlyTargetFunction&& aOther) + : mTargetBytes(std::move(aOther.mTargetBytes)), mOffset(aOther.mOffset) {} + + ReadOnlyTargetFunction& operator=(const ReadOnlyTargetFunction&) = delete; + ReadOnlyTargetFunction& operator=(ReadOnlyTargetFunction&&) = delete; + + ~ReadOnlyTargetFunction() = default; + + ReadOnlyTargetFunction operator+(const uint32_t aOffset) const { + return ReadOnlyTargetFunction(*this, mOffset + aOffset); + } + + uintptr_t GetBaseAddress() const { return mTargetBytes->GetBase(); } + + uintptr_t GetAddress() const { return mTargetBytes->GetBase() + mOffset; } + + uintptr_t AsEncodedPtr() const { + return EncodePtr( + reinterpret_cast<void*>(mTargetBytes->GetBase() + mOffset)); + } + + static uintptr_t EncodePtr(void* aPtr) { + return reinterpret_cast<uintptr_t>(::EncodePointer(aPtr)); + } + + static uintptr_t DecodePtr(uintptr_t aEncodedPtr) { + return reinterpret_cast<uintptr_t>( + ::DecodePointer(reinterpret_cast<PVOID>(aEncodedPtr))); + } + + bool IsValidAtOffset(const int8_t aOffset) const { + return mTargetBytes->IsValidAtOffset(aOffset); + } + +#if defined(_M_ARM64) + + uint32_t ReadNextInstruction() { + mTargetBytes->EnsureLimit(mOffset + sizeof(uint32_t)); + uint32_t instruction = *reinterpret_cast<const uint32_t*>( + mTargetBytes->GetLocalBytes() + mOffset); + mOffset += sizeof(uint32_t); + return instruction; + } + + bool BackUpOneInstruction() { + if (mOffset < sizeof(uint32_t)) { + return false; + } + + mOffset -= sizeof(uint32_t); + return true; + } + +#else + + uint8_t const& operator*() const { + mTargetBytes->EnsureLimit(mOffset); + return *(mTargetBytes->GetLocalBytes() + mOffset); + } + + uint8_t const& operator[](uint32_t aIndex) const { + mTargetBytes->EnsureLimit(mOffset + aIndex); + return *(mTargetBytes->GetLocalBytes() + mOffset + aIndex); + } + + ReadOnlyTargetFunction& operator++() { + ++mOffset; + return *this; + } + + ReadOnlyTargetFunction& operator+=(uint32_t aDelta) { + mOffset += aDelta; + return *this; + } + + uintptr_t ReadDisp32AsAbsolute() { + mTargetBytes->EnsureLimit(mOffset + sizeof(int32_t)); + int32_t disp = *reinterpret_cast<const int32_t*>( + mTargetBytes->GetLocalBytes() + mOffset); + uintptr_t result = + mTargetBytes->GetBase() + mOffset + sizeof(int32_t) + disp; + mOffset += sizeof(int32_t); + return result; + } + + bool IsRelativeShortJump(uintptr_t* aOutTarget) { + if ((*this)[0] == 0xeb) { + int8_t offset = static_cast<int8_t>((*this)[1]); + *aOutTarget = GetAddress() + 2 + offset; + return true; + } + return false; + } + +# if defined(_M_X64) + // Currently this function is used only in x64. + bool IsRelativeNearJump(uintptr_t* aOutTarget) { + if ((*this)[0] == 0xe9) { + *aOutTarget = (*this + 1).ReadDisp32AsAbsolute(); + return true; + } + return false; + } +# endif // defined(_M_X64) + + bool IsIndirectNearJump(uintptr_t* aOutTarget) { + if ((*this)[0] == 0xff && (*this)[1] == 0x25) { +# if defined(_M_X64) + *aOutTarget = (*this + 2).ChasePointerFromDisp(); +# else + *aOutTarget = (*this + 2).template ChasePointer<uintptr_t*>(); +# endif // defined(_M_X64) + return true; + } +# if defined(_M_X64) + else if ((*this)[0] == 0x48 && (*this)[1] == 0xff && (*this)[2] == 0x25) { + // According to Intel SDM, JMP does not have REX.W except JMP m16:64, + // but CPU can execute JMP r/m32 with REX.W. We handle it just in case. + *aOutTarget = (*this + 3).ChasePointerFromDisp(); + return true; + } +# endif // defined(_M_X64) + return false; + } + +#endif // defined(_M_ARM64) + + void Rewind() { mOffset = 0; } + + uint32_t GetOffset() const { return mOffset; } + + uintptr_t OffsetToAbsolute(const uint8_t aOffset) const { + return mTargetBytes->GetBase() + mOffset + aOffset; + } + + uintptr_t GetCurrentAbsolute() const { return OffsetToAbsolute(0); } + + /** + * This method promotes the code referenced by this object to be writable. + * + * @param aLen The length of the function's code to make writable. If set + * to zero, this object's current offset is used as the length. + * @param aOffset The result's base address will be offset from this + * object's base address by |aOffset| bytes. This value may be + * negative. + */ + WritableTargetFunction<MMPolicy> Promote(const uint32_t aLen = 0, + const int8_t aOffset = 0) const { + const uint32_t effectiveLength = aLen ? aLen : mOffset; + MOZ_RELEASE_ASSERT(effectiveLength, + "Cannot Promote a zero-length function"); + + if (!mTargetBytes->IsValidAtOffset(aOffset)) { + return WritableTargetFunction<MMPolicy>(mTargetBytes->GetMMPolicy()); + } + + WritableTargetFunction<MMPolicy> result(mTargetBytes->GetMMPolicy(), + mTargetBytes->GetBase() + aOffset, + effectiveLength); + + return result; + } + + private: + template <typename T> + struct ChasePointerHelper { + template <typename MMPolicy_> + static T Result(const MMPolicy_&, T aValue) { + return aValue; + } + }; + + template <typename T> + struct ChasePointerHelper<T*> { + template <typename MMPolicy_> + static auto Result(const MMPolicy_& aPolicy, T* aValue) { + ReadOnlyTargetFunction<MMPolicy_> ptr(aPolicy, aValue); + return ptr.template ChasePointer<T>(); + } + }; + + public: + // Keep chasing pointers until T is not a pointer type anymore + template <typename T> + auto ChasePointer() { + mTargetBytes->EnsureLimit(mOffset + sizeof(T)); + const std::remove_cv_t<T> result = + *reinterpret_cast<const std::remove_cv_t<T>*>( + mTargetBytes->GetLocalBytes() + mOffset); + return ChasePointerHelper<std::remove_cv_t<T>>::Result( + mTargetBytes->GetMMPolicy(), result); + } + + uintptr_t ChasePointerFromDisp() { + uintptr_t ptrFromDisp = ReadDisp32AsAbsolute(); + ReadOnlyTargetFunction<MMPolicy> ptr( + mTargetBytes->GetMMPolicy(), + reinterpret_cast<const void*>(ptrFromDisp)); + return ptr.template ChasePointer<uintptr_t>(); + } + + private: + ReadOnlyTargetFunction(const ReadOnlyTargetFunction& aOther) + : mTargetBytes(aOther.mTargetBytes), mOffset(aOther.mOffset) {} + + ReadOnlyTargetFunction(const ReadOnlyTargetFunction& aOther, + const uint32_t aOffsetFromOther) + : mTargetBytes(TargetBytesPtr<MMPolicy>::CopyFromOffset( + aOther.mTargetBytes, aOffsetFromOther)), + mOffset(0) {} + + private: + mutable typename TargetBytesPtr<MMPolicy>::Type mTargetBytes; + uint32_t mOffset; +}; + +template <typename MMPolicy, typename T> +class MOZ_STACK_CLASS TargetObject { + mutable typename TargetBytesPtr<MMPolicy>::Type mTargetBytes; + + TargetObject(const MMPolicy& aMMPolicy, const void* aBaseAddress) + : mTargetBytes(TargetBytesPtr<MMPolicy>::Make(aMMPolicy, aBaseAddress)) { + mTargetBytes->EnsureLimit(sizeof(T)); + } + + public: + explicit TargetObject(const MMPolicy& aMMPolicy) + : mTargetBytes(TargetBytesPtr<MMPolicy>::Make(aMMPolicy, nullptr)) {} + + TargetObject(const MMPolicy& aMMPolicy, uintptr_t aBaseAddress) + : TargetObject(aMMPolicy, reinterpret_cast<const void*>(aBaseAddress)) {} + + TargetObject(const TargetObject&) = delete; + TargetObject(TargetObject&&) = delete; + TargetObject& operator=(const TargetObject&) = delete; + TargetObject& operator=(TargetObject&&) = delete; + + explicit operator bool() const { + return mTargetBytes->GetBase() && mTargetBytes->GetLocalBytes(); + } + + const T* operator->() const { + return reinterpret_cast<const T*>(mTargetBytes->GetLocalBytes()); + } + + const T* GetLocalBase() const { + return reinterpret_cast<const T*>(mTargetBytes->GetLocalBytes()); + } +}; + +template <typename MMPolicy, typename T> +class MOZ_STACK_CLASS TargetObjectArray { + mutable typename TargetBytesPtr<MMPolicy>::Type mTargetBytes; + size_t mNumOfItems; + + TargetObjectArray(const MMPolicy& aMMPolicy, const void* aBaseAddress, + size_t aNumOfItems) + : mTargetBytes(TargetBytesPtr<MMPolicy>::Make(aMMPolicy, aBaseAddress)), + mNumOfItems(aNumOfItems) { + uint32_t itemsRead = + mTargetBytes->TryEnsureLimit(sizeof(T) * mNumOfItems) / sizeof(T); + // itemsRead may be bigger than the requested amount because of buffering, + // but mNumOfItems should not include extra bytes of buffering. + if (itemsRead < mNumOfItems) { + mNumOfItems = itemsRead; + } + } + + const T* GetLocalBase() const { + return reinterpret_cast<const T*>(mTargetBytes->GetLocalBytes()); + } + + public: + explicit TargetObjectArray(const MMPolicy& aMMPolicy) + : mTargetBytes(TargetBytesPtr<MMPolicy>::Make(aMMPolicy, nullptr)), + mNumOfItems(0) {} + + TargetObjectArray(const MMPolicy& aMMPolicy, uintptr_t aBaseAddress, + size_t aNumOfItems) + : TargetObjectArray(aMMPolicy, + reinterpret_cast<const void*>(aBaseAddress), + aNumOfItems) {} + + TargetObjectArray(const TargetObjectArray&) = delete; + TargetObjectArray(TargetObjectArray&&) = delete; + TargetObjectArray& operator=(const TargetObjectArray&) = delete; + TargetObjectArray& operator=(TargetObjectArray&&) = delete; + + explicit operator bool() const { + return mTargetBytes->GetBase() && mNumOfItems; + } + + const T* operator[](size_t aIndex) const { + if (aIndex >= mNumOfItems) { + return nullptr; + } + + return &GetLocalBase()[aIndex]; + } + + template <typename Comparator> + bool BinarySearchIf(const Comparator& aCompare, + size_t* aMatchOrInsertionPoint) const { + return mozilla::BinarySearchIf(GetLocalBase(), 0, mNumOfItems, aCompare, + aMatchOrInsertionPoint); + } +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_TargetFunction_h diff --git a/toolkit/xre/dllservices/mozglue/interceptor/Trampoline.h b/toolkit/xre/dllservices/mozglue/interceptor/Trampoline.h new file mode 100644 index 0000000000..befbd47215 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/Trampoline.h @@ -0,0 +1,773 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_interceptor_Trampoline_h +#define mozilla_interceptor_Trampoline_h + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/Maybe.h" +#include "mozilla/Types.h" +#include "mozilla/WindowsProcessMitigations.h" +#include "mozilla/WindowsUnwindInfo.h" + +namespace mozilla { +namespace interceptor { + +template <typename MMPolicy> +class MOZ_STACK_CLASS Trampoline final { + public: + Trampoline(const MMPolicy* aMMPolicy, uint8_t* const aLocalBase, + const uintptr_t aRemoteBase, const uint32_t aChunkSize) + : mMMPolicy(aMMPolicy), + mPrevLocalProt(0), + mLocalBase(aLocalBase), + mRemoteBase(aRemoteBase), + mOffset(0), + mExeOffset(0), +#ifdef _M_X64 + mCopyCodesEndOffset(0), + mExeEndOffset(0), +#endif // _M_X64 + mMaxOffset(aChunkSize), + mAccumulatedStatus(true) { + if (!::VirtualProtect(aLocalBase, aChunkSize, + MMPolicy::GetTrampWriteProtFlags(), + &mPrevLocalProt)) { + mPrevLocalProt = 0; + } + } + + Trampoline(Trampoline&& aOther) + : mMMPolicy(aOther.mMMPolicy), + mPrevLocalProt(aOther.mPrevLocalProt), + mLocalBase(aOther.mLocalBase), + mRemoteBase(aOther.mRemoteBase), + mOffset(aOther.mOffset), + mExeOffset(aOther.mExeOffset), +#ifdef _M_X64 + mCopyCodesEndOffset(aOther.mCopyCodesEndOffset), + mExeEndOffset(aOther.mExeEndOffset), +#endif // _M_X64 + mMaxOffset(aOther.mMaxOffset), + mAccumulatedStatus(aOther.mAccumulatedStatus) { + aOther.mPrevLocalProt = 0; + aOther.mAccumulatedStatus = false; + } + + MOZ_IMPLICIT Trampoline(decltype(nullptr)) + : mMMPolicy(nullptr), + mPrevLocalProt(0), + mLocalBase(nullptr), + mRemoteBase(0), + mOffset(0), + mExeOffset(0), +#ifdef _M_X64 + mCopyCodesEndOffset(0), + mExeEndOffset(0), +#endif // _M_X64 + mMaxOffset(0), + mAccumulatedStatus(false) { + } + + Trampoline(const Trampoline&) = delete; + Trampoline& operator=(const Trampoline&) = delete; + + Trampoline& operator=(Trampoline&& aOther) { + Clear(); + + mMMPolicy = aOther.mMMPolicy; + mPrevLocalProt = aOther.mPrevLocalProt; + mLocalBase = aOther.mLocalBase; + mRemoteBase = aOther.mRemoteBase; + mOffset = aOther.mOffset; + mExeOffset = aOther.mExeOffset; +#ifdef _M_X64 + mCopyCodesEndOffset = aOther.mCopyCodesEndOffset; + mExeEndOffset = aOther.mExeEndOffset; +#endif // _M_X64 + mMaxOffset = aOther.mMaxOffset; + mAccumulatedStatus = aOther.mAccumulatedStatus; + + aOther.mPrevLocalProt = 0; + aOther.mAccumulatedStatus = false; + + return *this; + } + + ~Trampoline() { Clear(); } + + explicit operator bool() const { + return IsNull() || + (mLocalBase && mRemoteBase && mPrevLocalProt && mAccumulatedStatus); + } + + bool IsNull() const { return !mMMPolicy; } + +#if defined(_M_ARM64) + + void WriteInstruction(uint32_t aInstruction) { + const uint32_t kDelta = sizeof(uint32_t); + + if (!mMMPolicy) { + // Null tramp, just track offset + mOffset += kDelta; + return; + } + + if (mOffset + kDelta > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + *reinterpret_cast<uint32_t*>(mLocalBase + mOffset) = aInstruction; + mOffset += kDelta; + } + + void WriteLoadLiteral(const uintptr_t aAddress, const uint8_t aReg) { + const uint32_t kDelta = sizeof(uint32_t) + sizeof(uintptr_t); + + if (!mMMPolicy) { + // Null tramp, just track offset + mOffset += kDelta; + return; + } + + // We grow the literal pool from the *end* of the tramp, + // so we need to ensure that there is enough room for both an instruction + // and a pointer + if (mOffset + kDelta > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + mMaxOffset -= sizeof(uintptr_t); + *reinterpret_cast<uintptr_t*>(mLocalBase + mMaxOffset) = aAddress; + + CheckedInt<intptr_t> pc(GetCurrentRemoteAddress()); + if (!pc.isValid()) { + mAccumulatedStatus = false; + return; + } + + CheckedInt<intptr_t> literal(reinterpret_cast<uintptr_t>(mLocalBase) + + mMaxOffset); + if (!literal.isValid()) { + mAccumulatedStatus = false; + return; + } + + CheckedInt<intptr_t> ptrOffset = (literal - pc); + if (!ptrOffset.isValid()) { + mAccumulatedStatus = false; + return; + } + + // ptrOffset must be properly aligned + MOZ_ASSERT((ptrOffset.value() % 4) == 0); + ptrOffset /= 4; + + CheckedInt<int32_t> offset(ptrOffset.value()); + if (!offset.isValid()) { + mAccumulatedStatus = false; + return; + } + + // Ensure that offset falls within the range of a signed 19-bit value + if (offset.value() < -0x40000 || offset.value() > 0x3FFFF) { + mAccumulatedStatus = false; + return; + } + + const int32_t kimm19Mask = 0x7FFFF; + int32_t masked = offset.value() & kimm19Mask; + + MOZ_ASSERT(aReg < 32); + uint32_t loadInstr = 0x58000000 | (masked << 5) | aReg; + WriteInstruction(loadInstr); + } + +#else + + void WriteByte(uint8_t aValue) { + const uint32_t kDelta = sizeof(uint8_t); + + if (!mMMPolicy) { + // Null tramp, just track offset + mOffset += kDelta; + return; + } + + if (mOffset >= mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + *(mLocalBase + mOffset) = aValue; + ++mOffset; + } + + void WriteInteger(int32_t aValue) { + const uint32_t kDelta = sizeof(int32_t); + + if (!mMMPolicy) { + // Null tramp, just track offset + mOffset += kDelta; + return; + } + + if (mOffset + kDelta > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + *reinterpret_cast<int32_t*>(mLocalBase + mOffset) = aValue; + mOffset += kDelta; + } + + void WriteDisp32(uintptr_t aAbsTarget) { + const uint32_t kDelta = sizeof(int32_t); + + if (!mMMPolicy) { + // Null tramp, just track offset + mOffset += kDelta; + return; + } + + if (mOffset + kDelta > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + // This needs to be computed from the remote location + intptr_t remoteTrampPosition = static_cast<intptr_t>(mRemoteBase + mOffset); + + intptr_t diff = + static_cast<intptr_t>(aAbsTarget) - (remoteTrampPosition + kDelta); + + CheckedInt<int32_t> checkedDisp(diff); + MOZ_ASSERT(checkedDisp.isValid()); + if (!checkedDisp.isValid()) { + mAccumulatedStatus = false; + return; + } + + int32_t disp = checkedDisp.value(); + *reinterpret_cast<int32_t*>(mLocalBase + mOffset) = disp; + mOffset += kDelta; + } + + void WriteBytes(void* aAddr, size_t aSize) { + if (!mMMPolicy) { + // Null tramp, just track offset + mOffset += aSize; + return; + } + + if (mOffset + aSize > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + std::memcpy(reinterpret_cast<void*>(mLocalBase + mOffset), aAddr, aSize); + mOffset += aSize; + } + +#endif + + void WritePointer(uintptr_t aValue) { + const uint32_t kDelta = sizeof(uintptr_t); + + if (!mMMPolicy) { + // Null tramp, just track offset + mOffset += kDelta; + return; + } + + if (mOffset + kDelta > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + *reinterpret_cast<uintptr_t*>(mLocalBase + mOffset) = aValue; + mOffset += kDelta; + } + + void WriteEncodedPointer(void* aValue) { + uintptr_t encoded = ReadOnlyTargetFunction<MMPolicy>::EncodePtr(aValue); + WritePointer(encoded); + } + + Maybe<uintptr_t> ReadPointer() { + if (mOffset + sizeof(uintptr_t) > mMaxOffset) { + mAccumulatedStatus = false; + return Nothing(); + } + + auto result = Some(*reinterpret_cast<uintptr_t*>(mLocalBase + mOffset)); + mOffset += sizeof(uintptr_t); + return std::move(result); + } + + Maybe<uintptr_t> ReadEncodedPointer() { + Maybe<uintptr_t> encoded(ReadPointer()); + if (!encoded) { + return encoded; + } + + return Some(ReadOnlyTargetFunction<MMPolicy>::DecodePtr(encoded.value())); + } + +#if defined(_M_IX86) + // 32-bit only + void AdjustDisp32AtOffset(uint32_t aOffset, uintptr_t aAbsTarget) { + uint32_t effectiveOffset = mExeOffset + aOffset; + + if (effectiveOffset + sizeof(int32_t) > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + intptr_t diff = static_cast<intptr_t>(aAbsTarget) - + static_cast<intptr_t>(mRemoteBase + mExeOffset); + *reinterpret_cast<int32_t*>(mLocalBase + effectiveOffset) += diff; + } +#endif // defined(_M_IX86) + + void CopyFrom(uintptr_t aOrigBytes, uint32_t aNumBytes) { + if (!mMMPolicy) { + // Null tramp, just track offset + mOffset += aNumBytes; + return; + } + + if (!mMMPolicy || mOffset + aNumBytes > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + if (!mMMPolicy->Read(mLocalBase + mOffset, + reinterpret_cast<void*>(aOrigBytes), aNumBytes)) { + mAccumulatedStatus = false; + return; + } + + mOffset += aNumBytes; + } + + void CopyCodes(uintptr_t aOrigBytes, uint32_t aNumBytes) { +#ifdef _M_X64 + if (mOffset == mCopyCodesEndOffset) { + mCopyCodesEndOffset += aNumBytes; + } +#endif // _M_X64 + CopyFrom(aOrigBytes, aNumBytes); + } + + void Rewind() { mOffset = 0; } + + uintptr_t GetCurrentRemoteAddress() const { return mRemoteBase + mOffset; } + + void StartExecutableCode() { + MOZ_ASSERT(!mExeOffset); + mExeOffset = mOffset; +#ifdef _M_X64 + mCopyCodesEndOffset = mOffset; +#endif // _M_X64 + } + + void* EndExecutableCode() { + if (!mAccumulatedStatus || !mMMPolicy) { + return nullptr; + } + +#ifdef _M_X64 + mExeEndOffset = mOffset; +#endif // _M_X64 + + // This must always return the start address the executable code + // *in the target process* + return reinterpret_cast<void*>(mRemoteBase + mExeOffset); + } + + uint32_t GetCurrentExecutableCodeLen() const { return mOffset - mExeOffset; } + +#ifdef _M_X64 + + void Align(uint32_t aAlignment) { + // aAlignment should be a power of 2 + MOZ_ASSERT(!(aAlignment & (aAlignment - 1))); + + uint32_t alignedOffset = (mOffset + aAlignment - 1) & ~(aAlignment - 1); + if (alignedOffset > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + mOffset = alignedOffset; + } + + // We assume that all instructions that are part of the prologue are left + // intact by detouring code, i.e. that they are copied using CopyCodes. This + // is not true for calls and jumps for example, but calls and jumps cannot be + // part of the prologue. This assumption allows us to copy unwind information + // as-is, because unwind information only refers to instructions within the + // prologue. + bool AddUnwindInfo(uintptr_t aOrigFuncAddr, uintptr_t aOrigFuncStopOffset) { + if constexpr (!MMPolicy::kSupportsUnwindInfo) { + return false; + } + + if (!mMMPolicy) { + return false; + } + + uint32_t origFuncOffsetFromBeginAddr = 0; + uint32_t origFuncOffsetToEndAddr = 0; + uintptr_t origImageBase = 0; + auto unwindInfoData = + mMMPolicy->LookupUnwindInfo(aOrigFuncAddr, &origFuncOffsetFromBeginAddr, + &origFuncOffsetToEndAddr, &origImageBase); + if (!unwindInfoData) { + // If the original function does not have unwind info, there is nothing + // more to do. + return true; + } + + // We do not support hooking at a location that isn't the beginning of a + // function. + MOZ_ASSERT(origFuncOffsetFromBeginAddr == 0); + if (origFuncOffsetFromBeginAddr != 0) { + return false; + } + + IterableUnwindInfo unwindInfoIt(unwindInfoData.get()); + auto& unwindInfo = unwindInfoIt.Info(); + + // The prologue should contain only instructions that we detour using + // CopyCodes. If not, there is most likely a mismatch between the unwind + // information and the actual code we are detouring, so we stop here. This + // is a best-effort safeguard intended to detect situations where e.g. + // third-party injected code would have altered the function we are + // detouring. + if (mCopyCodesEndOffset < aOrigFuncStopOffset && + unwindInfo.size_of_prolog > mCopyCodesEndOffset) { + return false; + } + + // According to the documentation, the array is sorted by descending order + // of offset in the prologue. Let's double check this assumption if in + // debug. This also checks that the full unwind information isn't + // ill-formed, thanks to all the MOZ_ASSERT in iteration code. +# ifdef DEBUG + uint8_t previousOffset = 0xFF; + for (const auto& unwindCode : unwindInfoIt) { + MOZ_ASSERT(unwindCode.offset_in_prolog <= previousOffset); + previousOffset = unwindCode.offset_in_prolog; + } +# endif // DEBUG + + // We skip entries that are not part of the code we have detoured. + // This code relies on the array being sorted by descending order of offset + // in the prolog. + uint8_t firstRelevantCode = 0; + uint8_t countOfCodes = 0; + auto it = unwindInfoIt.begin(); + for (; it != unwindInfoIt.end(); ++it) { + const auto& unwindCode = *it; + if (unwindCode.offset_in_prolog <= aOrigFuncStopOffset) { + // Found a relevant entry + firstRelevantCode = it.Index(); + countOfCodes = unwindInfo.count_of_codes - firstRelevantCode; + break; + } + } + + // Check that we encountered no ill-formed unwind codes. + if (!it.IsValid() && !it.IsAtEnd()) { + return false; + } + + // We do not support chained unwind info. We should add support for chained + // unwind info if we ever reach this assert. Since we hook functions at + // their start address, this should not happen. + if (unwindInfo.flags & UNW_FLAG_CHAININFO) { + MOZ_ASSERT( + false, + "Tried to detour at a location with chained unwind information"); + return false; + } + + // We do not support exception handler info either. This could be a problem + // if we detour code that does not belong to the prologue and contains a + // call instruction, as this handler would then not be found if unwinding + // from callees. The following assert checks that this does not happen. + // + // Our current assumption is that all the functions we hook either have no + // associated exception handlers, or it is __GSHandlerCheck. This handler + // is the most commonly found, for example it is present in LdrLoadDll, + // SendMessageTimeoutW, GetWindowInfo. It is added to functions that use + // stack buffers, in order to mitigate stack buffer overflows. We explain + // below why it is not a problem that we do not preserve __GSHandlerCheck + // information when we detour code. + // + // Preserving exception handler information would raise two challenges: + // + // (1) if the exception handler was not written in a generic way, it may + // behave differently when called for our detoured code compared to + // what it would do if called from the original location of the code; + // (2) the exception handler can be followed by handler-specific data, + // which we cannot copy because we do not know its size. + // + // __GSHandlerCheck checks that the stack cookie value wasn't overwritten + // before continuing to unwind and call further handlers. That is a + // security feature that we want to preserve. However, since these + // functions allocate stack space and write the stack cookie as part of + // their prologue, the 13 bytes that we detour are necessarily part of + // their prologue, which must contain at least the following instructions: + // + // 48 81 ec XX XX XX XX sub rsp, 0xXXXXXXXX + // 48 8b 05 XX XX XX XX mov rax, qword ptr [rip+__security_cookie] + // 48 33 c4 xor rax, rsp + // 48 89 84 24 XX XX XX XX mov qword ptr [RSP + 0xXXXXXXXX],RAX + // + // As a consequence, code associated with __GSHandlerCheck will necessarily + // satisfy (aOrigFuncStopOffset <= unwindInfo.size_of_prolog), and it is OK + // to not preserve handler info in that case. +# ifdef DEBUG + if (aOrigFuncStopOffset > unwindInfo.size_of_prolog) { + MOZ_ASSERT(!(unwindInfo.flags & (UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER))); + } +# endif // DEBUG + + // The unwind info must be DWORD-aligned + Align(sizeof(uint32_t)); + if (!mAccumulatedStatus) { + return false; + } + uintptr_t unwindInfoOffset = mOffset; + + unwindInfo.flags &= + ~(UNW_FLAG_CHAININFO | UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER); + unwindInfo.count_of_codes = countOfCodes; + if (aOrigFuncStopOffset < unwindInfo.size_of_prolog) { + unwindInfo.size_of_prolog = aOrigFuncStopOffset; + } + + WriteBytes(reinterpret_cast<void*>(&unwindInfo), + offsetof(UnwindInfo, unwind_code)); + if (!mAccumulatedStatus) { + return false; + } + + WriteBytes( + reinterpret_cast<void*>(&unwindInfo.unwind_code[firstRelevantCode]), + countOfCodes * sizeof(UnwindCode)); + if (!mAccumulatedStatus) { + return false; + } + + // The function table must be DWORD-aligned + Align(sizeof(uint32_t)); + if (!mAccumulatedStatus) { + return false; + } + uintptr_t functionTableOffset = mOffset; + + WriteInteger(mExeOffset); + if (!mAccumulatedStatus) { + return false; + } + + WriteInteger(mExeEndOffset); + if (!mAccumulatedStatus) { + return false; + } + + WriteInteger(unwindInfoOffset); + if (!mAccumulatedStatus) { + return false; + } + + return mMMPolicy->AddFunctionTable(mRemoteBase + functionTableOffset, 1, + mRemoteBase); + } + +#endif // _M_X64 + + Trampoline<MMPolicy>& operator--() { + MOZ_ASSERT(mOffset); + --mOffset; + return *this; + } + + private: + void Clear() { + if (!mLocalBase || !mPrevLocalProt) { + return; + } + + DebugOnly<bool> ok = !!::VirtualProtect(mLocalBase, mMaxOffset, + mPrevLocalProt, &mPrevLocalProt); + MOZ_ASSERT(ok); + + mLocalBase = nullptr; + mRemoteBase = 0; + mPrevLocalProt = 0; + mAccumulatedStatus = false; + } + + private: + const MMPolicy* mMMPolicy; + DWORD mPrevLocalProt; + uint8_t* mLocalBase; + uintptr_t mRemoteBase; + uint32_t mOffset; + uint32_t mExeOffset; +#ifdef _M_X64 + uint32_t mCopyCodesEndOffset; + uint32_t mExeEndOffset; +#endif // _M_X64 + uint32_t mMaxOffset; + bool mAccumulatedStatus; +}; + +template <typename MMPolicy> +class MOZ_STACK_CLASS TrampolineCollection final { + public: + class MOZ_STACK_CLASS TrampolineIterator final { + public: + Trampoline<MMPolicy> operator*() { + uint32_t offset = mCurTramp * mCollection.mTrampSize; + return Trampoline<MMPolicy>( + &mCollection.mMMPolicy, mCollection.mLocalBase + offset, + mCollection.mRemoteBase + offset, mCollection.mTrampSize); + } + + TrampolineIterator& operator++() { + ++mCurTramp; + return *this; + } + + bool operator!=(const TrampolineIterator& aOther) const { + return mCurTramp != aOther.mCurTramp; + } + + private: + explicit TrampolineIterator( + const TrampolineCollection<MMPolicy>& aCollection, + const uint32_t aCurTramp = 0) + : mCollection(aCollection), mCurTramp(aCurTramp) {} + + const TrampolineCollection<MMPolicy>& mCollection; + uint32_t mCurTramp; + + friend class TrampolineCollection<MMPolicy>; + }; + + explicit TrampolineCollection(const MMPolicy& aMMPolicy) + : mMMPolicy(aMMPolicy), + mLocalBase(0), + mRemoteBase(0), + mTrampSize(0), + mNumTramps(0), + mPrevProt(0), + mCS(nullptr) {} + + TrampolineCollection(const MMPolicy& aMMPolicy, uint8_t* const aLocalBase, + const uintptr_t aRemoteBase, const uint32_t aTrampSize, + const uint32_t aNumTramps) + : mMMPolicy(aMMPolicy), + mLocalBase(aLocalBase), + mRemoteBase(aRemoteBase), + mTrampSize(aTrampSize), + mNumTramps(aNumTramps), + mPrevProt(0), + mCS(nullptr) { + if (!aNumTramps) { + return; + } + + BOOL ok = mMMPolicy.Protect(aLocalBase, aNumTramps * aTrampSize, + PAGE_EXECUTE_READWRITE, &mPrevProt); + if (!ok) { + // When destroying a sandboxed process that uses + // MITIGATION_DYNAMIC_CODE_DISABLE, we won't be allowed to write to our + // executable memory so we just do nothing. If we fail to get access + // to memory for any other reason, we still don't want to crash but we + // do assert. + MOZ_ASSERT(IsDynamicCodeDisabled()); + mNumTramps = 0; + mPrevProt = 0; + } + } + + ~TrampolineCollection() { + if (!mPrevProt) { + return; + } + + mMMPolicy.Protect(mLocalBase, mNumTramps * mTrampSize, mPrevProt, + &mPrevProt); + + if (mCS) { + ::LeaveCriticalSection(mCS); + } + } + + void Lock(CRITICAL_SECTION& aCS) { + if (!mPrevProt || mCS) { + return; + } + + mCS = &aCS; + ::EnterCriticalSection(&aCS); + } + + TrampolineIterator begin() const { + if (!mPrevProt) { + return end(); + } + + return TrampolineIterator(*this); + } + + TrampolineIterator end() const { + return TrampolineIterator(*this, mNumTramps); + } + + TrampolineCollection(const TrampolineCollection&) = delete; + TrampolineCollection& operator=(const TrampolineCollection&) = delete; + TrampolineCollection& operator=(TrampolineCollection&&) = delete; + + TrampolineCollection(TrampolineCollection&& aOther) + : mMMPolicy(aOther.mMMPolicy), + mLocalBase(aOther.mLocalBase), + mRemoteBase(aOther.mRemoteBase), + mTrampSize(aOther.mTrampSize), + mNumTramps(aOther.mNumTramps), + mPrevProt(aOther.mPrevProt), + mCS(aOther.mCS) { + aOther.mPrevProt = 0; + aOther.mCS = nullptr; + } + + private: + const MMPolicy& mMMPolicy; + uint8_t* const mLocalBase; + const uintptr_t mRemoteBase; + const uint32_t mTrampSize; + uint32_t mNumTramps; + uint32_t mPrevProt; + CRITICAL_SECTION* mCS; + + friend class TrampolineIterator; +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_Trampoline_h diff --git a/toolkit/xre/dllservices/mozglue/interceptor/VMSharingPolicies.h b/toolkit/xre/dllservices/mozglue/interceptor/VMSharingPolicies.h new file mode 100644 index 0000000000..8f93f5c1ad --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/VMSharingPolicies.h @@ -0,0 +1,285 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_interceptor_VMSharingPolicies_h +#define mozilla_interceptor_VMSharingPolicies_h + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/Types.h" + +namespace mozilla { +namespace interceptor { + +/** + * This class is an abstraction of a reservation of virtual address space that + * has been obtained from a VMSharingPolicy via the policy's |Reserve| method. + * + * TrampolinePool allows us to obtain a trampoline without needing to concern + * ourselves with the underlying implementation of the VM sharing policy. + * + * For example, VMSharingPolicyShared delegates to VMSharingPolicyUnique, but + * also requires taking a lock before doing so. By invoking |GetNextTrampoline| + * on a TrampolinePool, the caller does not need to concern themselves with + * that detail. + * + * We accompolish this with a recursive implementation that provides an inner + * TrampolinePool that is provided by the delegated VMSharingPolicy. + */ +template <typename VMPolicyT, typename InnerT> +class MOZ_STACK_CLASS TrampolinePool final { + public: + TrampolinePool(TrampolinePool&& aOther) = default; + + TrampolinePool(VMPolicyT& aVMPolicy, InnerT&& aInner) + : mVMPolicy(aVMPolicy), mInner(std::move(aInner)) {} + + TrampolinePool& operator=(TrampolinePool&& aOther) = delete; + TrampolinePool(const TrampolinePool&) = delete; + TrampolinePool& operator=(const TrampolinePool&) = delete; + + using MMPolicyT = typename VMPolicyT::MMPolicyT; + + Maybe<Trampoline<MMPolicyT>> GetNextTrampoline() { + return mVMPolicy.GetNextTrampoline(mInner); + } + +#if defined(_M_X64) + bool IsInLowest2GB() const { + return mVMPolicy.IsTrampolineSpaceInLowest2GB(mInner); + } +#endif // defined(_M_X64) + + private: + VMPolicyT& mVMPolicy; + InnerT mInner; +}; + +/** + * This specialization is the base case for TrampolinePool, and is used by + * VMSharingPolicyUnique (since that policy does not delegate anything). + */ +template <typename VMPolicyT> +class MOZ_STACK_CLASS TrampolinePool<VMPolicyT, decltype(nullptr)> final { + public: + explicit TrampolinePool(VMPolicyT& aVMPolicy) : mVMPolicy(aVMPolicy) {} + + TrampolinePool(TrampolinePool&& aOther) = default; + + TrampolinePool& operator=(TrampolinePool&& aOther) = delete; + TrampolinePool(const TrampolinePool&) = delete; + TrampolinePool& operator=(const TrampolinePool&) = delete; + + using MMPolicyT = typename VMPolicyT::MMPolicyT; + + Maybe<Trampoline<MMPolicyT>> GetNextTrampoline() { + return mVMPolicy.GetNextTrampoline(); + } + +#if defined(_M_X64) + bool IsInLowest2GB() const { + return mVMPolicy.IsTrampolineSpaceInLowest2GB(); + } +#endif // defined(_M_X64) + + private: + VMPolicyT& mVMPolicy; +}; + +template <typename MMPolicy> +class VMSharingPolicyUnique : public MMPolicy { + using ThisType = VMSharingPolicyUnique<MMPolicy>; + + public: + using PoolType = TrampolinePool<ThisType, decltype(nullptr)>; + + template <typename... Args> + explicit VMSharingPolicyUnique(Args&&... aArgs) + : MMPolicy(std::forward<Args>(aArgs)...), mNextChunkIndex(0) {} + + Maybe<PoolType> Reserve(const uintptr_t aPivotAddr, + const uint32_t aMaxDistanceFromPivot) { + // Win32 allocates VM addresses at a 64KiB granularity, so we might as well + // utilize that entire 64KiB reservation. + uint32_t len = MMPolicy::GetAllocGranularity(); + + Maybe<Span<const uint8_t>> maybeBounds = MMPolicy::SpanFromPivotAndDistance( + len, aPivotAddr, aMaxDistanceFromPivot); + + return Reserve(len, maybeBounds); + } + + Maybe<PoolType> Reserve(const uint32_t aSize, + const Maybe<Span<const uint8_t>>& aBounds) { + uint32_t bytesReserved = MMPolicy::Reserve(aSize, aBounds); + if (!bytesReserved) { + return Nothing(); + } + + return Some(PoolType(*this)); + } + + TrampolineCollection<MMPolicy> Items() const { + return TrampolineCollection<MMPolicy>(*this, this->GetLocalView(), + this->GetRemoteView(), kChunkSize, + mNextChunkIndex); + } + + void Clear() { mNextChunkIndex = 0; } + + ~VMSharingPolicyUnique() = default; + + VMSharingPolicyUnique(const VMSharingPolicyUnique&) = delete; + VMSharingPolicyUnique& operator=(const VMSharingPolicyUnique&) = delete; + + VMSharingPolicyUnique(VMSharingPolicyUnique&& aOther) + : MMPolicy(std::move(aOther)), mNextChunkIndex(aOther.mNextChunkIndex) { + aOther.mNextChunkIndex = 0; + } + + VMSharingPolicyUnique& operator=(VMSharingPolicyUnique&& aOther) { + static_cast<MMPolicy&>(*this) = std::move(aOther); + mNextChunkIndex = aOther.mNextChunkIndex; + aOther.mNextChunkIndex = 0; + return *this; + } + + protected: + // In VMSharingPolicyUnique we do not implement the overload that accepts + // an inner trampoline pool, as this policy is expected to be the + // implementation of the base case. + Maybe<Trampoline<MMPolicy>> GetNextTrampoline() { + uint32_t offset = mNextChunkIndex * kChunkSize; + if (!this->MaybeCommitNextPage(offset, kChunkSize)) { + return Nothing(); + } + + Trampoline<MMPolicy> result(this, this->GetLocalView() + offset, + this->GetRemoteView() + offset, kChunkSize); + if (!!result) { + ++mNextChunkIndex; + } + + return Some(std::move(result)); + } + + private: + uint32_t mNextChunkIndex; + static const uint32_t kChunkSize = 128; + + template <typename VMPolicyT, typename FriendT> + friend class TrampolinePool; +}; + +} // namespace interceptor +} // namespace mozilla + +// We don't include RangeMap.h until this point because it depends on the +// TrampolinePool definitions from above. +#include "mozilla/interceptor/RangeMap.h" + +namespace mozilla { +namespace interceptor { + +// We only support this policy for in-proc MMPolicy. +class MOZ_TRIVIAL_CTOR_DTOR VMSharingPolicyShared : public MMPolicyInProcess { + typedef VMSharingPolicyUnique<MMPolicyInProcess> UniquePolicyT; + typedef VMSharingPolicyShared ThisType; + + public: + using PoolType = TrampolinePool<ThisType, UniquePolicyT::PoolType>; + using MMPolicyT = MMPolicyInProcess; + + constexpr VMSharingPolicyShared() {} + + bool ShouldUnhookUponDestruction() const { return false; } + + Maybe<PoolType> Reserve(const uintptr_t aPivotAddr, + const uint32_t aMaxDistanceFromPivot) { + // Win32 allocates VM addresses at a 64KiB granularity, so we might as well + // utilize that entire 64KiB reservation. + uint32_t len = this->GetAllocGranularity(); + + Maybe<Span<const uint8_t>> maybeBounds = + MMPolicyInProcess::SpanFromPivotAndDistance(len, aPivotAddr, + aMaxDistanceFromPivot); + + AutoCriticalSection lock(GetCS()); + VMSharingPolicyUnique<MMPolicyT>* uniquePol = sVMMap.GetPolicy(maybeBounds); + MOZ_ASSERT(uniquePol); + if (!uniquePol) { + return Nothing(); + } + + Maybe<UniquePolicyT::PoolType> maybeUnique = + uniquePol->Reserve(len, maybeBounds); + if (!maybeUnique) { + return Nothing(); + } + + return Some(PoolType(*this, std::move(maybeUnique.ref()))); + } + + TrampolineCollection<MMPolicyInProcess> Items() const { + // Since ShouldUnhookUponDestruction returns false, this can be empty + return TrampolineCollection<MMPolicyInProcess>(*this); + } + + void Clear() { + // This must be a no-op for shared VM policy; we can't have one interceptor + // wiping out trampolines for all interceptors in the process. + } + + VMSharingPolicyShared(const VMSharingPolicyShared&) = delete; + VMSharingPolicyShared(VMSharingPolicyShared&&) = delete; + VMSharingPolicyShared& operator=(const VMSharingPolicyShared&) = delete; + VMSharingPolicyShared& operator=(VMSharingPolicyShared&&) = delete; + + private: + static CRITICAL_SECTION* GetCS() { + static const bool isAlloc = []() -> bool { + DWORD flags = 0; +#if defined(RELEASE_OR_BETA) + flags |= CRITICAL_SECTION_NO_DEBUG_INFO; +#endif // defined(RELEASE_OR_BETA) + ::InitializeCriticalSectionEx(&sCS, 4000, flags); + return true; + }(); + Unused << isAlloc; + + return &sCS; + } + + // In VMSharingPolicyShared, we only implement the overload that accepts + // a VMSharingPolicyUnique trampoline pool as |aInner|, since we require the + // former policy to wrap the latter. + Maybe<Trampoline<MMPolicyInProcess>> GetNextTrampoline( + UniquePolicyT::PoolType& aInner) { + AutoCriticalSection lock(GetCS()); + return aInner.GetNextTrampoline(); + } + +#if defined(_M_X64) + bool IsTrampolineSpaceInLowest2GB( + const UniquePolicyT::PoolType& aInner) const { + AutoCriticalSection lock(GetCS()); + return aInner.IsInLowest2GB(); + } +#endif // defined(_M_X64) + + private: + template <typename VMPolicyT, typename InnerT> + friend class TrampolinePool; + + inline static RangeMap<MMPolicyInProcess> sVMMap; + inline static CRITICAL_SECTION sCS; +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_VMSharingPolicies_h diff --git a/toolkit/xre/dllservices/mozglue/interceptor/moz.build b/toolkit/xre/dllservices/mozglue/interceptor/moz.build new file mode 100644 index 0000000000..561e33b147 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/interceptor/moz.build @@ -0,0 +1,26 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla.interceptor += [ + "Arm64.h", + "MMPolicies.h", + "PatcherBase.h", + "PatcherDetour.h", + "PatcherNopSpace.h", + "RangeMap.h", + "TargetFunction.h", + "Trampoline.h", + "VMSharingPolicies.h", +] + +if CONFIG["CPU_ARCH"] == "aarch64": + Library("interceptor") + + FINAL_LIBRARY = "mozglue" + + UNIFIED_SOURCES += [ + "Arm64.cpp", + ] diff --git a/toolkit/xre/dllservices/mozglue/moz.build b/toolkit/xre/dllservices/mozglue/moz.build new file mode 100644 index 0000000000..ea7b0eb7d5 --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/moz.build @@ -0,0 +1,80 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Library("dllservices_mozglue") + +if CONFIG["MOZ_WIDGET_TOOLKIT"]: + SOURCES += [ + # This file contains a |using namespace mozilla;| statement + "WindowsDllBlocklist.cpp", + ] + + UNIFIED_SOURCES += [ + "Authenticode.cpp", + "LoaderObserver.cpp", + "ModuleLoadFrame.cpp", + "WindowsFallbackLoaderAPI.cpp", + ] + +if not CONFIG["JS_STANDALONE"]: + UNIFIED_SOURCES += [ + "CacheNtDllThunk.cpp", + ] + +OS_LIBS += [ + "crypt32", + "ntdll", + "version", + "wintrust", +] + +DELAYLOAD_DLLS += [ + "crypt32.dll", + "wintrust.dll", +] + +EXPORTS += [ + "nsWindowsDllInterceptor.h", +] + +EXPORTS.mozilla += [ + "Authenticode.h", + "CacheNtDllThunk.h", + "LoaderAPIInterfaces.h", + "ModuleLoadInfo.h", + "WindowsDllBlocklist.h", + "WindowsDllBlocklistCommon.h", + "WindowsDllBlocklistInfo.h", +] + +EXPORTS.mozilla.glue += [ + "SharedSection.h", + "WindowsDllServices.h", +] + +# Generate DLL Blocklists +blocklist_header_types = ["A11y", "Launcher", "Legacy", "Test"] +blocklist_file_leaf_tpl = "WindowsDllBlocklist{0}Defs.h" +blocklist_files = tuple( + [blocklist_file_leaf_tpl.format(type) for type in blocklist_header_types] +) + +GeneratedFile( + *blocklist_files, + script="gen_dll_blocklist_defs.py", + entry_point="gen_blocklists", + inputs=["WindowsDllBlocklistDefs.in"] +) + +EXPORTS.mozilla += ["!" + hdr for hdr in blocklist_files] + +DIRS += [ + "interceptor", +] + +FINAL_LIBRARY = "mozglue" + +REQUIRES_UNIFIED_BUILD = True diff --git a/toolkit/xre/dllservices/mozglue/nsWindowsDllInterceptor.h b/toolkit/xre/dllservices/mozglue/nsWindowsDllInterceptor.h new file mode 100644 index 0000000000..d6afc9bb7f --- /dev/null +++ b/toolkit/xre/dllservices/mozglue/nsWindowsDllInterceptor.h @@ -0,0 +1,819 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_WINDOWS_DLL_INTERCEPTOR_H_ +#define NS_WINDOWS_DLL_INTERCEPTOR_H_ + +#include <wchar.h> +#include <windows.h> +#include <winternl.h> + +#include <utility> + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/NativeNt.h" +#include "mozilla/Tuple.h" +#include "mozilla/Types.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Vector.h" +#include "mozilla/interceptor/MMPolicies.h" +#include "mozilla/interceptor/PatcherDetour.h" +#include "mozilla/interceptor/PatcherNopSpace.h" +#include "mozilla/interceptor/VMSharingPolicies.h" +#include "nsWindowsHelpers.h" + +/* + * Simple function interception. + * + * We have two separate mechanisms for intercepting a function: We can use the + * built-in nop space, if it exists, or we can create a detour. + * + * Using the built-in nop space works as follows: On x86-32, DLL functions + * begin with a two-byte nop (mov edi, edi) and are preceeded by five bytes of + * NOP instructions. + * + * When we detect a function with this prelude, we do the following: + * + * 1. Write a long jump to our interceptor function into the five bytes of NOPs + * before the function. + * + * 2. Write a short jump -5 into the two-byte nop at the beginning of the + * function. + * + * This mechanism is nice because it's thread-safe. It's even safe to do if + * another thread is currently running the function we're modifying! + * + * When the WindowsDllNopSpacePatcher is destroyed, we overwrite the short jump + * but not the long jump, so re-intercepting the same function won't work, + * because its prelude won't match. + * + * + * Unfortunately nop space patching doesn't work on functions which don't have + * this magic prelude (and in particular, x86-64 never has the prelude). So + * when we can't use the built-in nop space, we fall back to using a detour, + * which works as follows: + * + * 1. Save first N bytes of OrigFunction to trampoline, where N is a + * number of bytes >= 5 that are instruction aligned. + * + * 2. Replace first 5 bytes of OrigFunction with a jump to the Hook + * function. + * + * 3. After N bytes of the trampoline, add a jump to OrigFunction+N to + * continue original program flow. + * + * 4. Hook function needs to call the trampoline during its execution, + * to invoke the original function (so address of trampoline is + * returned). + * + * When the WindowsDllDetourPatcher object is destructed, OrigFunction is + * patched again to jump directly to the trampoline instead of going through + * the hook function. As such, re-intercepting the same function won't work, as + * jump instructions are not supported. + * + * Note that this is not thread-safe. Sad day. + * + */ + +#if defined(_M_IX86) && defined(__clang__) && __has_declspec_attribute(guard) +// On x86, nop-space patches return to the second instruction of their target. +// This is a deliberate violation of Control Flow Guard, so disable the check. +# define INTERCEPTOR_DISABLE_CFGUARD __declspec(guard(nocf)) +#else +# define INTERCEPTOR_DISABLE_CFGUARD /* nothing */ +#endif + +namespace mozilla { +namespace interceptor { + +template <typename T> +struct OriginalFunctionPtrTraits; + +template <typename R, typename... Args> +struct OriginalFunctionPtrTraits<R (*)(Args...)> { + using ReturnType = R; +}; + +#if defined(_M_IX86) +template <typename R, typename... Args> +struct OriginalFunctionPtrTraits<R(__stdcall*)(Args...)> { + using ReturnType = R; +}; + +template <typename R, typename... Args> +struct OriginalFunctionPtrTraits<R(__fastcall*)(Args...)> { + using ReturnType = R; +}; +#endif // defined(_M_IX86) + +template <typename InterceptorT, typename FuncPtrT> +class FuncHook final { + public: + using ThisType = FuncHook<InterceptorT, FuncPtrT>; + using ReturnType = typename OriginalFunctionPtrTraits<FuncPtrT>::ReturnType; + + constexpr FuncHook() : mOrigFunc(nullptr), mInitOnce(INIT_ONCE_STATIC_INIT) {} + + ~FuncHook() = default; + + bool Set(InterceptorT& aInterceptor, const char* aName, FuncPtrT aHookDest) { + LPVOID addHookOk = nullptr; + InitOnceContext ctx(this, &aInterceptor, aName, aHookDest, false); + + return ::InitOnceExecuteOnce(&mInitOnce, &InitOnceCallback, &ctx, + &addHookOk) && + addHookOk; + } + + bool SetDetour(InterceptorT& aInterceptor, const char* aName, + FuncPtrT aHookDest) { + LPVOID addHookOk = nullptr; + InitOnceContext ctx(this, &aInterceptor, aName, aHookDest, true); + + return ::InitOnceExecuteOnce(&mInitOnce, &InitOnceCallback, &ctx, + &addHookOk) && + addHookOk; + } + + explicit operator bool() const { return !!mOrigFunc; } + + template <typename... ArgsType> + INTERCEPTOR_DISABLE_CFGUARD ReturnType operator()(ArgsType&&... aArgs) const { + return mOrigFunc(std::forward<ArgsType>(aArgs)...); + } + + FuncPtrT GetStub() const { return mOrigFunc; } + + // One-time init stuff cannot be moved or copied + FuncHook(const FuncHook&) = delete; + FuncHook(FuncHook&&) = delete; + FuncHook& operator=(const FuncHook&) = delete; + FuncHook& operator=(FuncHook&& aOther) = delete; + + private: + struct MOZ_RAII InitOnceContext final { + InitOnceContext(ThisType* aHook, InterceptorT* aInterceptor, + const char* aName, FuncPtrT aHookDest, bool aForceDetour) + : mHook(aHook), + mInterceptor(aInterceptor), + mName(aName), + mHookDest(reinterpret_cast<void*>(aHookDest)), + mForceDetour(aForceDetour) {} + + ThisType* mHook; + InterceptorT* mInterceptor; + const char* mName; + void* mHookDest; + bool mForceDetour; + }; + + private: + bool Apply(InterceptorT* aInterceptor, const char* aName, void* aHookDest) { + return aInterceptor->AddHook(aName, reinterpret_cast<intptr_t>(aHookDest), + reinterpret_cast<void**>(&mOrigFunc)); + } + + bool ApplyDetour(InterceptorT* aInterceptor, const char* aName, + void* aHookDest) { + return aInterceptor->AddDetour(aName, reinterpret_cast<intptr_t>(aHookDest), + reinterpret_cast<void**>(&mOrigFunc)); + } + + static BOOL CALLBACK InitOnceCallback(PINIT_ONCE aInitOnce, PVOID aParam, + PVOID* aOutContext) { + MOZ_ASSERT(aOutContext); + + bool result; + auto ctx = reinterpret_cast<InitOnceContext*>(aParam); + if (ctx->mForceDetour) { + result = ctx->mHook->ApplyDetour(ctx->mInterceptor, ctx->mName, + ctx->mHookDest); + } else { + result = ctx->mHook->Apply(ctx->mInterceptor, ctx->mName, ctx->mHookDest); + } + + *aOutContext = + result ? reinterpret_cast<PVOID>(1U << INIT_ONCE_CTX_RESERVED_BITS) + : nullptr; + return TRUE; + } + + private: + FuncPtrT mOrigFunc; + INIT_ONCE mInitOnce; +}; + +template <typename InterceptorT, typename FuncPtrT> +class MOZ_ONLY_USED_TO_AVOID_STATIC_CONSTRUCTORS FuncHookCrossProcess final { + public: + using ThisType = FuncHookCrossProcess<InterceptorT, FuncPtrT>; + using ReturnType = typename OriginalFunctionPtrTraits<FuncPtrT>::ReturnType; + +#if defined(DEBUG) + FuncHookCrossProcess() {} +#endif // defined(DEBUG) + + bool Set(nt::CrossExecTransferManager& aTransferMgr, + InterceptorT& aInterceptor, const char* aName, FuncPtrT aHookDest) { + FuncPtrT origFunc; + if (!aInterceptor.AddHook(aName, reinterpret_cast<intptr_t>(aHookDest), + reinterpret_cast<void**>(&origFunc))) { + return false; + } + + return CopyStubToChildProcess(aTransferMgr, aInterceptor, origFunc); + } + + bool SetDetour(nt::CrossExecTransferManager& aTransferMgr, + InterceptorT& aInterceptor, const char* aName, + FuncPtrT aHookDest) { + FuncPtrT origFunc; + if (!aInterceptor.AddDetour(aName, reinterpret_cast<intptr_t>(aHookDest), + reinterpret_cast<void**>(&origFunc))) { + return false; + } + + return CopyStubToChildProcess(aTransferMgr, aInterceptor, origFunc); + } + + explicit operator bool() const { return !!mOrigFunc; } + + /** + * NB: This operator is only meaningful when invoked in the target process! + */ + template <typename... ArgsType> + ReturnType operator()(ArgsType&&... aArgs) const { + return mOrigFunc(std::forward<ArgsType>(aArgs)...); + } + +#if defined(DEBUG) + FuncHookCrossProcess(const FuncHookCrossProcess&) = delete; + FuncHookCrossProcess(FuncHookCrossProcess&&) = delete; + FuncHookCrossProcess& operator=(const FuncHookCrossProcess&) = delete; + FuncHookCrossProcess& operator=(FuncHookCrossProcess&& aOther) = delete; +#endif // defined(DEBUG) + + private: + bool CopyStubToChildProcess(nt::CrossExecTransferManager& aTransferMgr, + InterceptorT& aInterceptor, FuncPtrT aStub) { + LauncherVoidResult writeResult = + aTransferMgr.Transfer(&mOrigFunc, &aStub, sizeof(FuncPtrT)); + if (writeResult.isErr()) { +#ifdef MOZ_USE_LAUNCHER_ERROR + const mozilla::WindowsError& err = writeResult.inspectErr().mError; +#else + const mozilla::WindowsError& err = writeResult.inspectErr(); +#endif + aInterceptor.SetLastDetourError(FUNCHOOKCROSSPROCESS_COPYSTUB_ERROR, + err.AsHResult()); + return false; + } + return true; + } + + private: + FuncPtrT mOrigFunc; +}; + +template <typename MMPolicyT, typename InterceptorT> +struct TypeResolver; + +template <typename InterceptorT> +struct TypeResolver<mozilla::interceptor::MMPolicyInProcess, InterceptorT> { + template <typename FuncPtrT> + using FuncHookType = FuncHook<InterceptorT, FuncPtrT>; +}; + +template <typename InterceptorT> +struct TypeResolver<mozilla::interceptor::MMPolicyOutOfProcess, InterceptorT> { + template <typename FuncPtrT> + using FuncHookType = FuncHookCrossProcess<InterceptorT, FuncPtrT>; +}; + +template <typename VMPolicy = mozilla::interceptor::VMSharingPolicyShared> +class WindowsDllInterceptor final + : public TypeResolver<typename VMPolicy::MMPolicyT, + WindowsDllInterceptor<VMPolicy>> { + typedef WindowsDllInterceptor<VMPolicy> ThisType; + + interceptor::WindowsDllDetourPatcher<VMPolicy> mDetourPatcher; +#if defined(_M_IX86) + interceptor::WindowsDllNopSpacePatcher<typename VMPolicy::MMPolicyT> + mNopSpacePatcher; +#endif // defined(_M_IX86) + + HMODULE mModule; + + public: + template <typename... Args> + explicit WindowsDllInterceptor(Args&&... aArgs) + : mDetourPatcher(std::forward<Args>(aArgs)...) +#if defined(_M_IX86) + , + mNopSpacePatcher(std::forward<Args>(aArgs)...) +#endif // defined(_M_IX86) + , + mModule(nullptr) { + } + + WindowsDllInterceptor(const WindowsDllInterceptor&) = delete; + WindowsDllInterceptor(WindowsDllInterceptor&&) = delete; + WindowsDllInterceptor& operator=(const WindowsDllInterceptor&) = delete; + WindowsDllInterceptor& operator=(WindowsDllInterceptor&&) = delete; + + ~WindowsDllInterceptor() { Clear(); } + + template <size_t N> + void Init(const char (&aModuleName)[N]) { + wchar_t moduleName[N]; + + for (size_t i = 0; i < N; ++i) { + MOZ_ASSERT(!(aModuleName[i] & 0x80), + "Use wide-character overload for non-ASCII module names"); + moduleName[i] = aModuleName[i]; + } + + Init(moduleName); + } + + void Init(const wchar_t* aModuleName) { + if (mModule) { + return; + } + + mModule = ::LoadLibraryW(aModuleName); + } + + /** Force a specific configuration for testing purposes. NOT to be used in + production code! **/ + void TestOnlyDetourInit(const wchar_t* aModuleName, DetourFlags aFlags) { + Init(aModuleName); + mDetourPatcher.Init(aFlags); + } + + void Clear() { + if (!mModule) { + return; + } + +#if defined(_M_IX86) + mNopSpacePatcher.Clear(); +#endif // defined(_M_IX86) + mDetourPatcher.Clear(); + + // NB: We intentionally leak mModule + } + +#if defined(NIGHTLY_BUILD) + const Maybe<DetourError>& GetLastDetourError() const { + return mDetourPatcher.GetLastDetourError(); + } +#endif // defined(NIGHTLY_BUILD) + template <typename... Args> + void SetLastDetourError(Args&&... aArgs) { + return mDetourPatcher.SetLastDetourError(std::forward<Args>(aArgs)...); + } + + constexpr static uint32_t GetWorstCaseRequiredBytesToPatch() { + return WindowsDllDetourPatcherPrimitive< + typename VMPolicy::MMPolicyT>::GetWorstCaseRequiredBytesToPatch(); + } + + private: + /** + * Hook/detour the method aName from the DLL we set in Init so that it calls + * aHookDest instead. Returns the original method pointer in aOrigFunc + * and returns true if successful. + * + * IMPORTANT: If you use this method, please add your case to the + * TestDllInterceptor in order to detect future failures. Even if this + * succeeds now, updates to the hooked DLL could cause it to fail in + * the future. + */ + bool AddHook(const char* aName, intptr_t aHookDest, void** aOrigFunc) { + // Use a nop space patch if possible, otherwise fall back to a detour. + // This should be the preferred method for adding hooks. + if (!mModule) { + mDetourPatcher.SetLastDetourError(DetourResultCode::INTERCEPTOR_MOD_NULL); + return false; + } + + if (!mDetourPatcher.IsPageAccessible( + nt::PEHeaders::HModuleToBaseAddr<uintptr_t>(mModule))) { + mDetourPatcher.SetLastDetourError( + DetourResultCode::INTERCEPTOR_MOD_INACCESSIBLE); + return false; + } + + FARPROC proc = mDetourPatcher.GetProcAddress(mModule, aName); + if (!proc) { + mDetourPatcher.SetLastDetourError( + DetourResultCode::INTERCEPTOR_PROC_NULL); + return false; + } + + if (!mDetourPatcher.IsPageAccessible(reinterpret_cast<uintptr_t>(proc))) { + mDetourPatcher.SetLastDetourError( + DetourResultCode::INTERCEPTOR_PROC_INACCESSIBLE); + return false; + } + +#if defined(_M_IX86) + if (mNopSpacePatcher.AddHook(proc, aHookDest, aOrigFunc)) { + return true; + } +#endif // defined(_M_IX86) + + return AddDetour(proc, aHookDest, aOrigFunc); + } + + /** + * Detour the method aName from the DLL we set in Init so that it calls + * aHookDest instead. Returns the original method pointer in aOrigFunc + * and returns true if successful. + * + * IMPORTANT: If you use this method, please add your case to the + * TestDllInterceptor in order to detect future failures. Even if this + * succeeds now, updates to the detoured DLL could cause it to fail in + * the future. + */ + bool AddDetour(const char* aName, intptr_t aHookDest, void** aOrigFunc) { + // Generally, code should not call this method directly. Use AddHook unless + // there is a specific need to avoid nop space patches. + if (!mModule) { + mDetourPatcher.SetLastDetourError(DetourResultCode::INTERCEPTOR_MOD_NULL); + return false; + } + + if (!mDetourPatcher.IsPageAccessible( + nt::PEHeaders::HModuleToBaseAddr<uintptr_t>(mModule))) { + mDetourPatcher.SetLastDetourError( + DetourResultCode::INTERCEPTOR_MOD_INACCESSIBLE); + return false; + } + + FARPROC proc = mDetourPatcher.GetProcAddress(mModule, aName); + if (!proc) { + mDetourPatcher.SetLastDetourError( + DetourResultCode::INTERCEPTOR_PROC_NULL); + return false; + } + + if (!mDetourPatcher.IsPageAccessible(reinterpret_cast<uintptr_t>(proc))) { + mDetourPatcher.SetLastDetourError( + DetourResultCode::INTERCEPTOR_PROC_INACCESSIBLE); + return false; + } + + return AddDetour(proc, aHookDest, aOrigFunc); + } + + bool AddDetour(FARPROC aProc, intptr_t aHookDest, void** aOrigFunc) { + MOZ_ASSERT(mModule && aProc); + + if (!mDetourPatcher.Initialized()) { + DetourFlags flags = DetourFlags::eDefault; +#if defined(_M_X64) + // NTDLL hooks should attempt to use a 10-byte patch because some + // injected DLLs do the same and interfere with our stuff. + bool needs10BytePatch = (mModule == ::GetModuleHandleW(L"ntdll.dll")); + + bool isWin8Or81 = IsWin8OrLater() && (!IsWin10OrLater()); + bool isWin8 = IsWin8OrLater() && (!IsWin8Point1OrLater()); + + bool isKernel32Dll = (mModule == ::GetModuleHandleW(L"kernel32.dll")); + + bool isDuplicateHandle = (reinterpret_cast<void*>(aProc) == + reinterpret_cast<void*>(&::DuplicateHandle)); + + // CloseHandle on Windows 8/8.1 only accomodates 10-byte patches. + needs10BytePatch |= isWin8Or81 && isKernel32Dll && + (reinterpret_cast<void*>(aProc) == + reinterpret_cast<void*>(&CloseHandle)); + + // CreateFileA and DuplicateHandle on Windows 8 require 10-byte patches. + needs10BytePatch |= isWin8 && isKernel32Dll && + ((reinterpret_cast<void*>(aProc) == + reinterpret_cast<void*>(&::CreateFileA)) || + isDuplicateHandle); + + if (needs10BytePatch) { + flags |= DetourFlags::eEnable10BytePatch; + } + + if (isWin8 && isDuplicateHandle) { + // Because we can't detour Win8's KERNELBASE!DuplicateHandle, + // we detour kernel32!DuplicateHandle (See bug 1659398). + flags |= DetourFlags::eDontResolveRedirection; + } +#endif // defined(_M_X64) + + mDetourPatcher.Init(flags); + } + + return mDetourPatcher.AddHook(aProc, aHookDest, aOrigFunc); + } + + private: + template <typename InterceptorT, typename FuncPtrT> + friend class FuncHook; + + template <typename InterceptorT, typename FuncPtrT> + friend class FuncHookCrossProcess; +}; + +/** + * IAT patching is intended for use when we only want to intercept a function + * call originating from a specific module. + */ +class WindowsIATPatcher final { + public: + template <typename FuncPtrT> + using FuncHookType = FuncHook<WindowsIATPatcher, FuncPtrT>; + + private: + static bool CheckASCII(const char* aInStr) { + while (*aInStr) { + if (*aInStr & 0x80) { + return false; + } + ++aInStr; + } + return true; + } + + static bool AddHook(HMODULE aFromModule, const char* aToModuleName, + const char* aTargetFnName, void* aHookDest, + Atomic<void*>* aOutOrigFunc) { + if (!aFromModule || !aToModuleName || !aTargetFnName || !aOutOrigFunc) { + return false; + } + + // PE Spec requires ASCII names for imported module names + const bool isModuleNameAscii = CheckASCII(aToModuleName); + MOZ_ASSERT(isModuleNameAscii); + if (!isModuleNameAscii) { + return false; + } + + // PE Spec requires ASCII names for imported function names + const bool isTargetFnNameAscii = CheckASCII(aTargetFnName); + MOZ_ASSERT(isTargetFnNameAscii); + if (!isTargetFnNameAscii) { + return false; + } + + nt::PEHeaders headers(aFromModule); + if (!headers) { + return false; + } + + PIMAGE_IMPORT_DESCRIPTOR impDesc = + headers.GetImportDescriptor(aToModuleName); + if (!nt::PEHeaders::IsValid(impDesc)) { + // Either aFromModule does not import aToModuleName at load-time, or + // aToModuleName is a (currently unsupported) delay-load import. + return false; + } + + // Resolve the import name table (INT). + auto firstINTThunk = headers.template RVAToPtr<PIMAGE_THUNK_DATA>( + impDesc->OriginalFirstThunk); + if (!nt::PEHeaders::IsValid(firstINTThunk)) { + return false; + } + + Maybe<ptrdiff_t> thunkIndex; + + // Scan the INT for the location of the thunk for the function named + // 'aTargetFnName'. + for (PIMAGE_THUNK_DATA curINTThunk = firstINTThunk; + nt::PEHeaders::IsValid(curINTThunk); ++curINTThunk) { + if (IMAGE_SNAP_BY_ORDINAL(curINTThunk->u1.Ordinal)) { + // Currently not supporting import by ordinal; this isn't hard to add, + // but we won't bother unless necessary. + continue; + } + + PIMAGE_IMPORT_BY_NAME curThunkFnName = + headers.template RVAToPtr<PIMAGE_IMPORT_BY_NAME>( + curINTThunk->u1.AddressOfData); + MOZ_ASSERT(curThunkFnName); + if (!curThunkFnName) { + // Looks like we have a bad name descriptor. Try to continue. + continue; + } + + // Function name checks MUST be case-sensitive! + if (!strcmp(aTargetFnName, curThunkFnName->Name)) { + // We found the thunk. Save the index of this thunk, as the IAT thunk + // is located at the same index in that table as in the INT. + thunkIndex = Some(curINTThunk - firstINTThunk); + break; + } + } + + if (thunkIndex.isNothing()) { + // We never found a thunk for that function. Perhaps it's not imported? + return false; + } + + if (thunkIndex.value() < 0) { + // That's just wrong. + return false; + } + + auto firstIATThunk = + headers.template RVAToPtr<PIMAGE_THUNK_DATA>(impDesc->FirstThunk); + if (!nt::PEHeaders::IsValid(firstIATThunk)) { + return false; + } + + // Resolve the IAT thunk for the function we want + PIMAGE_THUNK_DATA targetThunk = &firstIATThunk[thunkIndex.value()]; + if (!nt::PEHeaders::IsValid(targetThunk)) { + return false; + } + + auto fnPtr = reinterpret_cast<Atomic<void*>*>(&targetThunk->u1.Function); + + // Now we can just change out its pointer with our hook function. + AutoVirtualProtect prot(fnPtr, sizeof(void*), PAGE_EXECUTE_READWRITE); + if (!prot) { + return false; + } + + // We do the exchange this way to ensure that *aOutOrigFunc is always valid + // once the atomic exchange has taken place. + void* tmp; + + do { + tmp = *fnPtr; + *aOutOrigFunc = tmp; + } while (!fnPtr->compareExchange(tmp, aHookDest)); + + return true; + } + + template <typename InterceptorT, typename FuncPtrT> + friend class FuncHook; +}; + +template <typename FuncPtrT> +class MOZ_ONLY_USED_TO_AVOID_STATIC_CONSTRUCTORS + FuncHook<WindowsIATPatcher, FuncPtrT> + final { + public: + using ThisType = FuncHook<WindowsIATPatcher, FuncPtrT>; + using ReturnType = typename OriginalFunctionPtrTraits<FuncPtrT>::ReturnType; + + constexpr FuncHook() + : mInitOnce(INIT_ONCE_STATIC_INIT), + mFromModule(nullptr), + mOrigFunc(nullptr) {} + +#if defined(DEBUG) + ~FuncHook() = default; +#endif // defined(DEBUG) + + bool Set(const wchar_t* aFromModuleName, const char* aToModuleName, + const char* aFnName, FuncPtrT aHookDest) { + nsModuleHandle fromModule(::LoadLibraryW(aFromModuleName)); + if (!fromModule) { + return false; + } + + return Set(fromModule, aToModuleName, aFnName, aHookDest); + } + + // We offer this overload in case the client wants finer-grained control over + // loading aFromModule. + bool Set(nsModuleHandle& aFromModule, const char* aToModuleName, + const char* aFnName, FuncPtrT aHookDest) { + LPVOID addHookOk = nullptr; + InitOnceContext ctx(this, aFromModule, aToModuleName, aFnName, aHookDest); + + bool result = ::InitOnceExecuteOnce(&mInitOnce, &InitOnceCallback, &ctx, + &addHookOk) && + addHookOk; + if (!result) { + return result; + } + + // If we successfully set the hook then we must retain a strong reference + // to the module that we modified. + mFromModule = aFromModule.disown(); + return result; + } + + explicit operator bool() const { return !!mOrigFunc; } + + template <typename... ArgsType> + ReturnType operator()(ArgsType&&... aArgs) const { + return mOrigFunc(std::forward<ArgsType>(aArgs)...); + } + + FuncPtrT GetStub() const { return mOrigFunc; } + +#if defined(DEBUG) + // One-time init stuff cannot be moved or copied + FuncHook(const FuncHook&) = delete; + FuncHook(FuncHook&&) = delete; + FuncHook& operator=(const FuncHook&) = delete; + FuncHook& operator=(FuncHook&& aOther) = delete; +#endif // defined(DEBUG) + + private: + struct MOZ_RAII InitOnceContext final { + InitOnceContext(ThisType* aHook, const nsModuleHandle& aFromModule, + const char* aToModuleName, const char* aFnName, + FuncPtrT aHookDest) + : mHook(aHook), + mFromModule(aFromModule), + mToModuleName(aToModuleName), + mFnName(aFnName), + mHookDest(reinterpret_cast<void*>(aHookDest)) {} + + ThisType* mHook; + const nsModuleHandle& mFromModule; + const char* mToModuleName; + const char* mFnName; + void* mHookDest; + }; + + private: + bool Apply(const nsModuleHandle& aFromModule, const char* aToModuleName, + const char* aFnName, void* aHookDest) { + return WindowsIATPatcher::AddHook( + aFromModule, aToModuleName, aFnName, aHookDest, + reinterpret_cast<Atomic<void*>*>(&mOrigFunc)); + } + + static BOOL CALLBACK InitOnceCallback(PINIT_ONCE aInitOnce, PVOID aParam, + PVOID* aOutContext) { + MOZ_ASSERT(aOutContext); + + auto ctx = reinterpret_cast<InitOnceContext*>(aParam); + bool result = ctx->mHook->Apply(ctx->mFromModule, ctx->mToModuleName, + ctx->mFnName, ctx->mHookDest); + + *aOutContext = + result ? reinterpret_cast<PVOID>(1U << INIT_ONCE_CTX_RESERVED_BITS) + : nullptr; + return TRUE; + } + + private: + INIT_ONCE mInitOnce; + HMODULE mFromModule; // never freed + FuncPtrT mOrigFunc; +}; + +/** + * This class applies an irreversible patch to jump to a target function + * without backing up the original function. + */ +class WindowsDllEntryPointInterceptor final { + using DllMainFn = BOOL(WINAPI*)(HINSTANCE, DWORD, LPVOID); + using MMPolicyT = MMPolicyInProcessEarlyStage; + + MMPolicyT mMMPolicy; + + public: + explicit WindowsDllEntryPointInterceptor( + const MMPolicyT::Kernel32Exports& aK32Exports) + : mMMPolicy(aK32Exports) {} + + bool Set(const nt::PEHeaders& aHeaders, DllMainFn aDestination) { + if (!aHeaders) { + return false; + } + + WindowsDllDetourPatcherPrimitive<MMPolicyT> patcher; + return patcher.AddIrreversibleHook( + mMMPolicy, aHeaders.GetEntryPoint(), + reinterpret_cast<uintptr_t>(aDestination)); + } +}; + +} // namespace interceptor + +using WindowsDllInterceptor = interceptor::WindowsDllInterceptor<>; + +using CrossProcessDllInterceptor = interceptor::WindowsDllInterceptor< + mozilla::interceptor::VMSharingPolicyUnique< + mozilla::interceptor::MMPolicyOutOfProcess>>; + +using WindowsIATPatcher = interceptor::WindowsIATPatcher; + +} // namespace mozilla + +#endif /* NS_WINDOWS_DLL_INTERCEPTOR_H_ */ diff --git a/toolkit/xre/dllservices/tests/AssemblyPayloads.h b/toolkit/xre/dllservices/tests/AssemblyPayloads.h new file mode 100644 index 0000000000..811bf88412 --- /dev/null +++ b/toolkit/xre/dllservices/tests/AssemblyPayloads.h @@ -0,0 +1,241 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +/* These assembly functions represent patterns that were already hooked by + * another application before our detour. + */ + +#ifndef mozilla_AssemblyPayloads_h +#define mozilla_AssemblyPayloads_h + +#include <cstdint> + +#define PADDING_256_NOP \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" + +extern "C" { + +#if defined(__clang__) +# if defined(_M_X64) +constexpr uintptr_t JumpDestination = 0x7fff00000000; + +__declspec(dllexport) __attribute__((naked)) void MovPushRet() { + asm volatile( + "mov %0, %%rax;" + "push %%rax;" + "ret;" + : + : "i"(JumpDestination)); +} + +__declspec(dllexport) __attribute__((naked)) void MovRaxJump() { + asm volatile( + "mov %0, %%rax;" + "jmpq *%%rax;" + : + : "i"(JumpDestination)); +} + +__declspec(dllexport) __attribute__((naked)) void DoubleJump() { + asm volatile( + "jmp label1;" + + "label2:" + "mov %0, %%rax;" + "jmpq *%%rax;" + + // 0x100 bytes padding to generate jmp rel32 instead of jmp rel8 + PADDING_256_NOP + + "label1:" + "jmp label2;" + : + : "i"(JumpDestination)); +} + +__declspec(dllexport) __attribute__((naked)) void NearJump() { + asm volatile( + "jae label3;" + "je label3;" + "jne label3;" + + "label4:" + "mov %0, %%rax;" + "jmpq *%%rax;" + + // 0x100 bytes padding to generate jae rel32 instead of jae rel8 + PADDING_256_NOP + + "label3:" + "jmp label4;" + : + : "i"(JumpDestination)); +} + +__declspec(dllexport) __attribute__((naked)) void OpcodeFF() { + // Skip PUSH (FF /6) because clang prefers Opcode 50+rd + // to translate PUSH r64 rather than Opcode FF. + asm volatile( + "incl %eax;" + "decl %ebx;" + "call *%rcx;" + "jmp *(%rip);" // Indirect jump to 0xcccccccc`cccccccc + "int $3;int $3;int $3;int $3;" + "int $3;int $3;int $3;int $3;"); +} + +__declspec(dllexport) __attribute__((naked)) void IndirectCall() { + asm volatile( + "call *(%rip);" // Indirect call to 0x90909090`90909090 + "nop;nop;nop;nop;nop;nop;nop;nop;" + "ret;"); +} + +__declspec(dllexport) __attribute__((naked)) void MovImm64() { + asm volatile( + "mov $0x1234567812345678, %r10;" + "nop;nop;nop"); +} + +# if !defined(MOZ_CODE_COVERAGE) +// This code reproduces bug 1798787: it uses the same prologue, the same unwind +// info, and it has a call instruction that starts within the 13 first bytes. +__attribute__((naked)) void DetouredCallCode(uintptr_t aCallee) { + asm volatile( + "subq $0x28, %rsp;" + "testq %rcx, %rcx;" + "jz exit;" + "callq *%rcx;" + "exit:" + "addq $0x28, %rsp;" + "retq;"); +} +constexpr uint8_t gDetouredCallCodeSize = 16; // size of function in bytes +alignas(uint32_t) uint8_t gDetouredCallUnwindInfo[] = { + 0x01, // Version (1), Flags (0) + 0x04, // SizeOfProlog (4) + 0x01, // CountOfUnwindCodes (1) + 0x00, // FrameRegister (0), FrameOffset (0) + // UnwindCodes[0] + 0x04, // .OffsetInProlog (4) + 0x42, // .UnwindOpCode(UWOP_ALLOC_SMALL=2), .UnwindInfo (4) +}; + +// This points to the same code as DetouredCallCode, but dynamically generated +// so that it can have custom unwinding info. See TestDllInterceptor.cpp. +extern decltype(&DetouredCallCode) gDetouredCall; + +// This is just a jumper: our hooking code will thus detour the jump target +// DetouredCall -- it will not detour DetouredCallJumper. We need to do this to +// point our hooking code to the dynamic code, because our hooking API needs an +// exported function name. +// +// guard(nocf) ensures that our generated code is a recognized jumper: +// jmp qword ptr [rip+offset DetouredCall] +// rather than: +// mov rax, qword ptr [rip+offset DetouredCall] +// mov rdx, qword ptr [rip+offset __guard_dispatch_icall_fptr] +// jmp rdx +__declspec(dllexport noinline guard(nocf)) void DetouredCallJumper( + uintptr_t aCallee) { + gDetouredCall(aCallee); +} +# endif // !defined(MOZ_CODE_COVERAGE) + +# elif defined(_M_IX86) +constexpr uintptr_t JumpDestination = 0x7fff0000; + +__declspec(dllexport) __attribute__((naked)) void PushRet() { + asm volatile( + "push %0;" + "ret;" + : + : "i"(JumpDestination)); +} + +__declspec(dllexport) __attribute__((naked)) void MovEaxJump() { + asm volatile( + "mov %0, %%eax;" + "jmp *%%eax;" + : + : "i"(JumpDestination)); +} + +__declspec(dllexport) __attribute__((naked)) void Opcode83() { + asm volatile( + "xor $0x42, %eax;" + "cmpl $1, 0xc(%ebp);"); +} + +__declspec(dllexport) __attribute__((naked)) void LockPrefix() { + // Test an instruction with a LOCK prefix (0xf0) at a non-zero offset + asm volatile( + "push $0x7c;" + "lock push $0x7c;"); +} + +__declspec(dllexport) __attribute__((naked)) void LooksLikeLockPrefix() { + // This is for a regression scenario of bug 1625452, where we double-counted + // the offset in CountPrefixBytes. When we count prefix bytes in front of + // the 2nd PUSH located at offset 2, we mistakenly started counting from + // the byte 0xf0 at offset 4, which is considered as LOCK, thus we try to + // detour the next byte 0xcc and it fails. + // + // 0: 6a7c push 7Ch + // 2: 68ccf00000 push 0F0CCh + // + asm volatile( + "push $0x7c;" + "push $0x0000f0cc;"); +} + +__declspec(dllexport) __attribute__((naked)) void DoubleJump() { + asm volatile( + "jmp label1;" + + "label2:" + "mov %0, %%eax;" + "jmp *%%eax;" + + // 0x100 bytes padding to generate jmp rel32 instead of jmp rel8 + PADDING_256_NOP + + "label1:" + "jmp label2;" + : + : "i"(JumpDestination)); +} +# endif + +# if !defined(_M_ARM64) +__declspec(dllexport) __attribute__((naked)) void UnsupportedOp() { + asm volatile( + "ud2;" + "nop;nop;nop;nop;nop;nop;nop;nop;" + "nop;nop;nop;nop;nop;nop;nop;nop;"); +} +# endif // !defined(_M_ARM64) + +#endif // defined(__clang__) + +} // extern "C" + +#endif // mozilla_AssemblyPayloads_h diff --git a/toolkit/xre/dllservices/tests/TestDllInterceptor.cpp b/toolkit/xre/dllservices/tests/TestDllInterceptor.cpp new file mode 100644 index 0000000000..3c58132a34 --- /dev/null +++ b/toolkit/xre/dllservices/tests/TestDllInterceptor.cpp @@ -0,0 +1,1416 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <shlobj.h> +#include <stdio.h> +#include <commdlg.h> +#define SECURITY_WIN32 +#include <security.h> +#include <wininet.h> +#include <schnlsp.h> +#include <winternl.h> +#include <processthreadsapi.h> + +#include "AssemblyPayloads.h" +#include "mozilla/DynamicallyLinkedFunctionPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WindowsVersion.h" +#include "nsWindowsDllInterceptor.h" +#include "nsWindowsHelpers.h" + +#include "mozilla/MozProcessMitigationDynamicCodePolicy.h" + +NTSTATUS NTAPI NtFlushBuffersFile(HANDLE, PIO_STATUS_BLOCK); +NTSTATUS NTAPI NtReadFile(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, + PIO_STATUS_BLOCK, PVOID, ULONG, PLARGE_INTEGER, + PULONG); +NTSTATUS NTAPI NtReadFileScatter(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, + PIO_STATUS_BLOCK, PFILE_SEGMENT_ELEMENT, ULONG, + PLARGE_INTEGER, PULONG); +NTSTATUS NTAPI NtWriteFile(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, + PIO_STATUS_BLOCK, PVOID, ULONG, PLARGE_INTEGER, + PULONG); +NTSTATUS NTAPI NtWriteFileGather(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, + PIO_STATUS_BLOCK, PFILE_SEGMENT_ELEMENT, ULONG, + PLARGE_INTEGER, PULONG); +NTSTATUS NTAPI NtQueryFullAttributesFile(POBJECT_ATTRIBUTES, PVOID); +NTSTATUS NTAPI LdrLoadDll(PWCHAR filePath, PULONG flags, + PUNICODE_STRING moduleFileName, PHANDLE handle); +NTSTATUS NTAPI LdrUnloadDll(HMODULE); + +NTSTATUS NTAPI NtMapViewOfSection( + HANDLE aSection, HANDLE aProcess, PVOID* aBaseAddress, ULONG_PTR aZeroBits, + SIZE_T aCommitSize, PLARGE_INTEGER aSectionOffset, PSIZE_T aViewSize, + SECTION_INHERIT aInheritDisposition, ULONG aAllocationType, + ULONG aProtectionFlags); + +// These pointers are disguised as PVOID to avoid pulling in obscure headers +PVOID NTAPI LdrResolveDelayLoadedAPI(PVOID, PVOID, PVOID, PVOID, PVOID, ULONG); +void CALLBACK ProcessCaretEvents(HWINEVENTHOOK, DWORD, HWND, LONG, LONG, DWORD, + DWORD); +void __fastcall BaseThreadInitThunk(BOOL aIsInitialThread, void* aStartAddress, + void* aThreadParam); + +BOOL WINAPI ApiSetQueryApiSetPresence(PCUNICODE_STRING, PBOOLEAN); + +#if (_WIN32_WINNT < 0x0602) +BOOL WINAPI +SetProcessMitigationPolicy(PROCESS_MITIGATION_POLICY aMitigationPolicy, + PVOID aBuffer, SIZE_T aBufferLen); +#endif // (_WIN32_WINNT < 0x0602) + +using namespace mozilla; + +struct payload { + UINT64 a; + UINT64 b; + UINT64 c; + + bool operator==(const payload& other) const { + return (a == other.a && b == other.b && c == other.c); + } +}; + +extern "C" __declspec(dllexport) __declspec(noinline) payload + rotatePayload(payload p) { + UINT64 tmp = p.a; + p.a = p.b; + p.b = p.c; + p.c = tmp; + return p; +} + +// payloadNotHooked is a target function for a test to expect a negative result. +// We cannot use rotatePayload for that purpose because our detour cannot hook +// a function detoured already. Please keep this function always unhooked. +extern "C" __declspec(dllexport) __declspec(noinline) payload + payloadNotHooked(payload p) { + // Do something different from rotatePayload to avoid ICF. + p.a ^= p.b; + p.b ^= p.c; + p.c ^= p.a; + return p; +} + +// Declared as volatile to prevent optimizers from incorrectly eliding accesses +// to it. (See bug 1769001 for a motivating example.) +static volatile bool patched_func_called = false; + +static WindowsDllInterceptor::FuncHookType<decltype(&rotatePayload)> + orig_rotatePayload; + +static WindowsDllInterceptor::FuncHookType<decltype(&payloadNotHooked)> + orig_payloadNotHooked; + +static payload patched_rotatePayload(payload p) { + patched_func_called = true; + return orig_rotatePayload(p); +} + +// Invoke aFunc by taking aArg's contents and using them as aFunc's arguments +template <typename OrigFuncT, typename... Args, + typename ArgTuple = Tuple<Args...>, size_t... Indices> +decltype(auto) Apply(OrigFuncT& aFunc, ArgTuple&& aArgs, + std::index_sequence<Indices...>) { + return aFunc(Get<Indices>(std::forward<ArgTuple>(aArgs))...); +} + +#define DEFINE_TEST_FUNCTION(calling_convention) \ + template <typename R, typename... Args, typename... TestArgs> \ + bool TestFunction(R(calling_convention* aFunc)(Args...), bool (*aPred)(R), \ + TestArgs&&... aArgs) { \ + using ArgTuple = Tuple<Args...>; \ + using Indices = std::index_sequence_for<Args...>; \ + ArgTuple fakeArgs{std::forward<TestArgs>(aArgs)...}; \ + patched_func_called = false; \ + return aPred(Apply(aFunc, std::forward<ArgTuple>(fakeArgs), Indices())) && \ + patched_func_called; \ + } \ + \ + /* Specialization for functions returning void */ \ + template <typename PredT, typename... Args, typename... TestArgs> \ + bool TestFunction(void(calling_convention * aFunc)(Args...), PredT, \ + TestArgs&&... aArgs) { \ + using ArgTuple = Tuple<Args...>; \ + using Indices = std::index_sequence_for<Args...>; \ + ArgTuple fakeArgs{std::forward<TestArgs>(aArgs)...}; \ + patched_func_called = false; \ + Apply(aFunc, std::forward<ArgTuple>(fakeArgs), Indices()); \ + return patched_func_called; \ + } + +// C++11 allows empty arguments to macros. clang works just fine. MSVC does the +// right thing, but it also throws up warning C4003. +#if defined(_MSC_VER) && !defined(__clang__) +DEFINE_TEST_FUNCTION(__cdecl) +#else +DEFINE_TEST_FUNCTION() +#endif + +#ifdef _M_IX86 +DEFINE_TEST_FUNCTION(__stdcall) +DEFINE_TEST_FUNCTION(__fastcall) +#endif // _M_IX86 + +// Test the hooked function against the supplied predicate +template <typename OrigFuncT, typename PredicateT, typename... Args> +bool CheckHook(OrigFuncT& aOrigFunc, const char* aDllName, + const char* aFuncName, PredicateT&& aPred, Args&&... aArgs) { + if (TestFunction(aOrigFunc, std::forward<PredicateT>(aPred), + std::forward<Args>(aArgs)...)) { + printf( + "TEST-PASS | WindowsDllInterceptor | " + "Executed hooked function %s from %s\n", + aFuncName, aDllName); + fflush(stdout); + return true; + } + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Failed to execute hooked function %s from %s\n", + aFuncName, aDllName); + return false; +} + +struct InterceptorFunction { + static const size_t EXEC_MEMBLOCK_SIZE = 64 * 1024; // 64K + + static InterceptorFunction& Create() { + // Make sure the executable memory is allocated + if (!sBlock) { + Init(); + } + MOZ_ASSERT(sBlock); + + // Make sure we aren't making more functions than we allocated room for + MOZ_RELEASE_ASSERT((sNumInstances + 1) * sizeof(InterceptorFunction) <= + EXEC_MEMBLOCK_SIZE); + + // Grab the next InterceptorFunction from executable memory + InterceptorFunction& ret = *reinterpret_cast<InterceptorFunction*>( + sBlock + (sNumInstances++ * sizeof(InterceptorFunction))); + + // Set the InterceptorFunction to the code template. + auto funcCode = &ret[0]; + memcpy(funcCode, sInterceptorTemplate, TemplateLength); + + // Fill in the patched_func_called pointer in the template. + auto pfPtr = + reinterpret_cast<volatile bool**>(&ret[PatchedFuncCalledIndex]); + *pfPtr = &patched_func_called; + return ret; + } + + uint8_t& operator[](size_t i) { return mFuncCode[i]; } + + uint8_t* GetFunction() { return mFuncCode; } + + void SetStub(uintptr_t aStub) { + auto pfPtr = reinterpret_cast<uintptr_t*>(&mFuncCode[StubFuncIndex]); + *pfPtr = aStub; + } + + private: + // We intercept functions with short machine-code functions that set a boolean + // and run the stub that launches the original function. Each entry in the + // array is the code for one of those interceptor functions. We cannot + // free this memory until the test shuts down. + // The templates have spots for the address of patched_func_called + // and for the address of the stub function. Their indices in the byte + // array are given as constants below and they appear as blocks of + // 0xff bytes in the templates. +#if defined(_M_X64) + // 0: 48 b8 ff ff ff ff ff ff ff ff movabs rax, &patched_func_called + // a: c6 00 01 mov BYTE PTR [rax],0x1 + // d: 48 b8 ff ff ff ff ff ff ff ff movabs rax, &stub_func_ptr + // 17: ff e0 jmp rax + static constexpr uint8_t sInterceptorTemplate[] = { + 0x48, 0xB8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xC6, 0x00, 0x01, 0x48, 0xB8, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0}; + static const size_t PatchedFuncCalledIndex = 0x2; + static const size_t StubFuncIndex = 0xf; +#elif defined(_M_IX86) + // 0: c6 05 ff ff ff ff 01 mov BYTE PTR &patched_func_called, 0x1 + // 7: 68 ff ff ff ff push &stub_func_ptr + // c: c3 ret + static constexpr uint8_t sInterceptorTemplate[] = { + 0xC6, 0x05, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, + 0x68, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3}; + static const size_t PatchedFuncCalledIndex = 0x2; + static const size_t StubFuncIndex = 0x8; +#elif defined(_M_ARM64) + // 0: 31 00 80 52 movz w17, #0x1 + // 4: 90 00 00 58 ldr x16, #16 + // 8: 11 02 00 39 strb w17, [x16] + // c: 90 00 00 58 ldr x16, #16 + // 10: 00 02 1F D6 br x16 + // 14: &patched_func_called + // 1c: &stub_func_ptr + static constexpr uint8_t sInterceptorTemplate[] = { + 0x31, 0x00, 0x80, 0x52, 0x90, 0x00, 0x00, 0x58, 0x11, 0x02, 0x00, 0x39, + 0x90, 0x00, 0x00, 0x58, 0x00, 0x02, 0x1F, 0xD6, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + static const size_t PatchedFuncCalledIndex = 0x14; + static const size_t StubFuncIndex = 0x1c; +#else +# error "Missing template for architecture" +#endif + + static const size_t TemplateLength = sizeof(sInterceptorTemplate); + uint8_t mFuncCode[TemplateLength]; + + InterceptorFunction() = delete; + InterceptorFunction(const InterceptorFunction&) = delete; + InterceptorFunction& operator=(const InterceptorFunction&) = delete; + + static void Init() { + MOZ_ASSERT(!sBlock); + sBlock = reinterpret_cast<uint8_t*>( + ::VirtualAlloc(nullptr, EXEC_MEMBLOCK_SIZE, MEM_RESERVE | MEM_COMMIT, + PAGE_EXECUTE_READWRITE)); + } + + static uint8_t* sBlock; + static size_t sNumInstances; +}; + +uint8_t* InterceptorFunction::sBlock = nullptr; +size_t InterceptorFunction::sNumInstances = 0; + +constexpr uint8_t InterceptorFunction::sInterceptorTemplate[]; + +// Hook the function and optionally attempt calling it +template <typename OrigFuncT, size_t N, typename PredicateT, typename... Args> +bool TestHook(const char (&dll)[N], const char* func, PredicateT&& aPred, + Args&&... aArgs) { + auto orig_func( + mozilla::MakeUnique<WindowsDllInterceptor::FuncHookType<OrigFuncT>>()); + wchar_t dllW[N]; + std::copy(std::begin(dll), std::end(dll), std::begin(dllW)); + + bool successful = false; + WindowsDllInterceptor TestIntercept; + TestIntercept.Init(dll); + + InterceptorFunction& interceptorFunc = InterceptorFunction::Create(); + successful = orig_func->Set( + TestIntercept, func, + reinterpret_cast<OrigFuncT>(interceptorFunc.GetFunction())); + + if (successful) { + auto stub = reinterpret_cast<uintptr_t>(orig_func->GetStub()); + interceptorFunc.SetStub(stub); + printf("TEST-PASS | WindowsDllInterceptor | Could hook %s from %s\n", func, + dll); + fflush(stdout); + + // Test the DLL function we just hooked. + HMODULE module = ::LoadLibraryW(dllW); + FARPROC funcAddr = ::GetProcAddress(module, func); + if (!funcAddr) { + return false; + } + +// Check that unwind information has been added if and only if it was present +// for the original function. +#ifdef _M_X64 + + auto funcBytes = reinterpret_cast<uint8_t*>(funcAddr); + + // If the function we are hooking is a jumper, we need to lookup the + // destination of the jump to find the unwind information. + auto realFuncAddr = reinterpret_cast<uintptr_t>(funcAddr); + // jmp qword ptr[rip+offset] + if (funcBytes[0] == 0xff && funcBytes[1] == 0x25) { + realFuncAddr = *reinterpret_cast<uintptr_t*>( + realFuncAddr + 6 + *reinterpret_cast<int32_t*>(realFuncAddr + 2)); + } + // rex.jmp qword ptr[rip+offset] + else if (funcBytes[0] == 0x48 && funcBytes[1] == 0xff && + funcBytes[2] == 0x25) { + realFuncAddr = *reinterpret_cast<uintptr_t*>( + realFuncAddr + 7 + *reinterpret_cast<int32_t*>(realFuncAddr + 3)); + } + + uintptr_t funcImageBase = 0; + auto funcEntry = + RtlLookupFunctionEntry(realFuncAddr, &funcImageBase, nullptr); + bool funcHasUnwindInfo = bool(funcEntry); + + uintptr_t stubImageBase = 0; + auto stubEntry = RtlLookupFunctionEntry(stub, &stubImageBase, nullptr); + bool stubHasUnwindInfo = bool(stubEntry); + + if (funcHasUnwindInfo == stubHasUnwindInfo) { + printf( + "TEST-PASS | WindowsDllInterceptor | The hook for %s from %s and " + "the original function are coherent with respect to unwind info: " + "funcHasUnwindInfo (%d) == stubHasUnwindInfo (%d).\n", + func, dll, funcHasUnwindInfo, stubHasUnwindInfo); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Hook for %s from %s " + "and the original function are not coherent with respect to unwind " + "info: " + "funcHasUnwindInfo (%d) != stubHasUnwindInfo (%d).\n", + func, dll, funcHasUnwindInfo, stubHasUnwindInfo); + fflush(stdout); + return false; + } + + if (stubHasUnwindInfo) { + if (stub == (stubImageBase + stubEntry->BeginAddress)) { + printf( + "TEST-PASS | WindowsDllInterceptor | The hook for %s from %s has " + "coherent unwind info.\n", + func, dll); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | The hook for %s " + " from %s has incoherent unwind info.\n", + func, dll); + fflush(stdout); + return false; + } + } + +#endif // _M_X64 + + if (!aPred) { + printf( + "TEST-SKIPPED | WindowsDllInterceptor | " + "Will not attempt to execute patched %s.\n", + func); + fflush(stdout); + return true; + } + + return CheckHook(reinterpret_cast<OrigFuncT&>(funcAddr), dll, func, + std::forward<PredicateT>(aPred), + std::forward<Args>(aArgs)...); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Failed to hook %s from " + "%s\n", + func, dll); + fflush(stdout); + + // Print out the function's bytes so that we can easily analyze the error. + nsModuleHandle mod(::LoadLibraryW(dllW)); + FARPROC funcAddr = ::GetProcAddress(mod, func); + if (funcAddr) { + const uint32_t kNumBytesToDump = + WindowsDllInterceptor::GetWorstCaseRequiredBytesToPatch(); + + printf("\tFirst %u bytes of function:\n\t", kNumBytesToDump); + + auto code = reinterpret_cast<const uint8_t*>(funcAddr); + for (uint32_t i = 0; i < kNumBytesToDump; ++i) { + char suffix = (i < (kNumBytesToDump - 1)) ? ' ' : '\n'; + printf("%02hhX%c", code[i], suffix); + } + + fflush(stdout); + } + return false; + } +} + +// Detour the function and optionally attempt calling it +template <typename OrigFuncT, size_t N, typename PredicateT> +bool TestDetour(const char (&dll)[N], const char* func, PredicateT&& aPred) { + auto orig_func( + mozilla::MakeUnique<WindowsDllInterceptor::FuncHookType<OrigFuncT>>()); + wchar_t dllW[N]; + std::copy(std::begin(dll), std::end(dll), std::begin(dllW)); + + bool successful = false; + WindowsDllInterceptor TestIntercept; + TestIntercept.Init(dll); + + InterceptorFunction& interceptorFunc = InterceptorFunction::Create(); + successful = orig_func->Set( + TestIntercept, func, + reinterpret_cast<OrigFuncT>(interceptorFunc.GetFunction())); + + if (successful) { + interceptorFunc.SetStub(reinterpret_cast<uintptr_t>(orig_func->GetStub())); + printf("TEST-PASS | WindowsDllInterceptor | Could detour %s from %s\n", + func, dll); + fflush(stdout); + if (!aPred) { + printf( + "TEST-SKIPPED | WindowsDllInterceptor | " + "Will not attempt to execute patched %s.\n", + func); + fflush(stdout); + return true; + } + + // Test the DLL function we just hooked. + HMODULE module = ::LoadLibraryW(dllW); + FARPROC funcAddr = ::GetProcAddress(module, func); + if (!funcAddr) { + return false; + } + + return CheckHook(reinterpret_cast<OrigFuncT&>(funcAddr), dll, func, + std::forward<PredicateT>(aPred)); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Failed to detour %s " + "from %s\n", + func, dll); + fflush(stdout); + return false; + } +} + +// If a function pointer's type returns void*, this template converts that type +// to return uintptr_t instead, for the purposes of predicates. +template <typename FuncT> +struct SubstituteForVoidPtr { + using Type = FuncT; +}; + +template <typename... Args> +struct SubstituteForVoidPtr<void* (*)(Args...)> { + using Type = uintptr_t (*)(Args...); +}; + +#ifdef _M_IX86 +template <typename... Args> +struct SubstituteForVoidPtr<void*(__stdcall*)(Args...)> { + using Type = uintptr_t(__stdcall*)(Args...); +}; + +template <typename... Args> +struct SubstituteForVoidPtr<void*(__fastcall*)(Args...)> { + using Type = uintptr_t(__fastcall*)(Args...); +}; +#endif // _M_IX86 + +// Determines the function's return type +template <typename FuncT> +struct ReturnType; + +template <typename R, typename... Args> +struct ReturnType<R (*)(Args...)> { + using Type = R; +}; + +#ifdef _M_IX86 +template <typename R, typename... Args> +struct ReturnType<R(__stdcall*)(Args...)> { + using Type = R; +}; + +template <typename R, typename... Args> +struct ReturnType<R(__fastcall*)(Args...)> { + using Type = R; +}; +#endif // _M_IX86 + +// Predicates that may be supplied during tests +template <typename FuncT> +struct Predicates { + using ArgType = typename ReturnType<FuncT>::Type; + + template <ArgType CompVal> + static bool Equals(ArgType aValue) { + return CompVal == aValue; + } + + template <ArgType CompVal> + static bool NotEquals(ArgType aValue) { + return CompVal != aValue; + } + + template <ArgType CompVal> + static bool Ignore(ArgType aValue) { + return true; + } +}; + +// Functions that return void should be ignored, so we specialize the +// Ignore predicate for that case. Use nullptr as the value to compare against. +template <typename... Args> +struct Predicates<void (*)(Args...)> { + template <nullptr_t DummyVal> + static bool Ignore() { + return true; + } +}; + +#ifdef _M_IX86 +template <typename... Args> +struct Predicates<void(__stdcall*)(Args...)> { + template <nullptr_t DummyVal> + static bool Ignore() { + return true; + } +}; + +template <typename... Args> +struct Predicates<void(__fastcall*)(Args...)> { + template <nullptr_t DummyVal> + static bool Ignore() { + return true; + } +}; +#endif // _M_IX86 + +// The standard test. Hook |func|, and then try executing it with all zero +// arguments, using |pred| and |comp| to determine whether the call successfully +// executed. In general, you want set pred and comp such that they return true +// when the function is returning whatever value is expected with all-zero +// arguments. +// +// Note: When |func| returns void, you must supply |Ignore| and |nullptr| as the +// |pred| and |comp| arguments, respectively. +#define TEST_HOOK(dll, func, pred, comp) \ + TestHook<decltype(&func)>(dll, #func, \ + &Predicates<decltype(&func)>::pred<comp>) + +// We need to special-case functions that return INVALID_HANDLE_VALUE +// (ie, CreateFile). Our template machinery for comparing values doesn't work +// with integer constants passed as pointers (well, it works on MSVC, but not +// clang, because that is not standard-compliant). +#define TEST_HOOK_FOR_INVALID_HANDLE_VALUE(dll, func) \ + TestHook<SubstituteForVoidPtr<decltype(&func)>::Type>( \ + dll, #func, \ + &Predicates<SubstituteForVoidPtr<decltype(&func)>::Type>::Equals< \ + uintptr_t(-1)>) + +// This variant allows you to explicitly supply arguments to the hooked function +// during testing. You want to provide arguments that produce the conditions +// that induce the function to return a value that is accepted by your +// predicate. +#define TEST_HOOK_PARAMS(dll, func, pred, comp, ...) \ + TestHook<decltype(&func)>( \ + dll, #func, &Predicates<decltype(&func)>::pred<comp>, __VA_ARGS__) + +// This is for cases when we want to hook |func|, but it is unsafe to attempt +// to execute the function in the context of a test. +#define TEST_HOOK_SKIP_EXEC(dll, func) \ + TestHook<decltype(&func)>( \ + dll, #func, \ + reinterpret_cast<bool (*)(typename ReturnType<decltype(&func)>::Type)>( \ + NULL)) + +// The following three variants are identical to the previous macros, +// however the forcibly use a Detour on 32-bit Windows. On 64-bit Windows, +// these macros are identical to their TEST_HOOK variants. +#define TEST_DETOUR(dll, func, pred, comp) \ + TestDetour<decltype(&func)>(dll, #func, \ + &Predicates<decltype(&func)>::pred<comp>) + +#define TEST_DETOUR_PARAMS(dll, func, pred, comp, ...) \ + TestDetour<decltype(&func)>( \ + dll, #func, &Predicates<decltype(&func)>::pred<comp>, __VA_ARGS__) + +#define TEST_DETOUR_SKIP_EXEC(dll, func) \ + TestDetour<decltype(&func)>( \ + dll, #func, \ + reinterpret_cast<bool (*)(typename ReturnType<decltype(&func)>::Type)>( \ + NULL)) + +template <typename OrigFuncT, size_t N, typename PredicateT, typename... Args> +bool MaybeTestHook(const bool cond, const char (&dll)[N], const char* func, + PredicateT&& aPred, Args&&... aArgs) { + if (!cond) { + printf( + "TEST-SKIPPED | WindowsDllInterceptor | Skipped hook test for %s from " + "%s\n", + func, dll); + fflush(stdout); + return true; + } + + return TestHook<OrigFuncT>(dll, func, std::forward<PredicateT>(aPred), + std::forward<Args>(aArgs)...); +} + +// Like TEST_HOOK, but the test is only executed when cond is true. +#define MAYBE_TEST_HOOK(cond, dll, func, pred, comp) \ + MaybeTestHook<decltype(&func)>(cond, dll, #func, \ + &Predicates<decltype(&func)>::pred<comp>) + +#define MAYBE_TEST_HOOK_PARAMS(cond, dll, func, pred, comp, ...) \ + MaybeTestHook<decltype(&func)>( \ + cond, dll, #func, &Predicates<decltype(&func)>::pred<comp>, __VA_ARGS__) + +#define MAYBE_TEST_HOOK_SKIP_EXEC(cond, dll, func) \ + MaybeTestHook<decltype(&func)>( \ + cond, dll, #func, \ + reinterpret_cast<bool (*)(typename ReturnType<decltype(&func)>::Type)>( \ + NULL)) + +bool ShouldTestTipTsf() { + if (!IsWin8OrLater()) { + return false; + } + + mozilla::DynamicallyLinkedFunctionPtr<decltype(&SHGetKnownFolderPath)> + pSHGetKnownFolderPath(L"shell32.dll", "SHGetKnownFolderPath"); + if (!pSHGetKnownFolderPath) { + return false; + } + + PWSTR commonFilesPath = nullptr; + if (FAILED(pSHGetKnownFolderPath(FOLDERID_ProgramFilesCommon, 0, nullptr, + &commonFilesPath))) { + return false; + } + + wchar_t fullPath[MAX_PATH + 1] = {}; + wcscpy(fullPath, commonFilesPath); + wcscat(fullPath, L"\\Microsoft Shared\\Ink\\tiptsf.dll"); + CoTaskMemFree(commonFilesPath); + + if (!LoadLibraryW(fullPath)) { + return false; + } + + // Leak the module so that it's loaded for the interceptor test + return true; +} + +static const wchar_t gEmptyUnicodeStringLiteral[] = L""; +static UNICODE_STRING gEmptyUnicodeString; +static BOOLEAN gIsPresent; + +bool HasApiSetQueryApiSetPresence() { + mozilla::DynamicallyLinkedFunctionPtr<decltype(&ApiSetQueryApiSetPresence)> + func(L"Api-ms-win-core-apiquery-l1-1-0.dll", "ApiSetQueryApiSetPresence"); + if (!func) { + return false; + } + + // Prepare gEmptyUnicodeString for the test + ::RtlInitUnicodeString(&gEmptyUnicodeString, gEmptyUnicodeStringLiteral); + + return true; +} + +// Set this to true to test function unhooking (currently broken). +const bool ShouldTestUnhookFunction = false; + +#if defined(_M_X64) || defined(_M_ARM64) + +// Use VMSharingPolicyUnique for the ShortInterceptor, as it needs to +// reserve its trampoline memory in a special location. +using ShortInterceptor = mozilla::interceptor::WindowsDllInterceptor< + mozilla::interceptor::VMSharingPolicyUnique< + mozilla::interceptor::MMPolicyInProcess>>; + +static ShortInterceptor::FuncHookType<decltype(&::NtMapViewOfSection)> + orig_NtMapViewOfSection; + +#endif // defined(_M_X64) || defined(_M_ARM64) + +bool TestShortDetour() { +#if defined(_M_X64) || defined(_M_ARM64) + auto pNtMapViewOfSection = reinterpret_cast<decltype(&::NtMapViewOfSection)>( + ::GetProcAddress(::GetModuleHandleW(L"ntdll.dll"), "NtMapViewOfSection")); + if (!pNtMapViewOfSection) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Failed to resolve ntdll!NtMapViewOfSection\n"); + fflush(stdout); + return false; + } + + { // Scope for shortInterceptor + ShortInterceptor shortInterceptor; + shortInterceptor.TestOnlyDetourInit( + L"ntdll.dll", + mozilla::interceptor::DetourFlags::eTestOnlyForceShortPatch); + + InterceptorFunction& interceptorFunc = InterceptorFunction::Create(); + if (!orig_NtMapViewOfSection.SetDetour( + shortInterceptor, "NtMapViewOfSection", + reinterpret_cast<decltype(&::NtMapViewOfSection)>( + interceptorFunc.GetFunction()))) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Failed to hook ntdll!NtMapViewOfSection via 10-byte patch\n"); + fflush(stdout); + return false; + } + + interceptorFunc.SetStub( + reinterpret_cast<uintptr_t>(orig_NtMapViewOfSection.GetStub())); + + auto pred = + &Predicates<decltype(&::NtMapViewOfSection)>::Ignore<((NTSTATUS)0)>; + + if (!CheckHook(pNtMapViewOfSection, "ntdll.dll", "NtMapViewOfSection", + pred)) { + // CheckHook has already printed the error message for us + return false; + } + } + + // Now ensure that our hook cleanup worked + if (ShouldTestUnhookFunction) { + NTSTATUS status = + pNtMapViewOfSection(nullptr, nullptr, nullptr, 0, 0, nullptr, nullptr, + ((SECTION_INHERIT)0), 0, 0); + if (NT_SUCCESS(status)) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Unexpected successful call to ntdll!NtMapViewOfSection after " + "removing short-patched hook\n"); + fflush(stdout); + return false; + } + + printf( + "TEST-PASS | WindowsDllInterceptor | " + "Successfully unhooked ntdll!NtMapViewOfSection via short patch\n"); + fflush(stdout); + } + + return true; +#else + return true; +#endif +} + +constexpr uintptr_t NoStubAddressCheck = 0; +constexpr uintptr_t ExpectedFail = 1; +struct TestCase { + const char* mFunctionName; + uintptr_t mExpectedStub; + bool mPatchedOnce; + explicit TestCase(const char* aFunctionName, uintptr_t aExpectedStub) + : mFunctionName(aFunctionName), + mExpectedStub(aExpectedStub), + mPatchedOnce(false) {} +} g_AssemblyTestCases[] = { +#if defined(__clang__) +// We disable these testcases because the code coverage instrumentation injects +// code in a way that WindowsDllInterceptor doesn't understand. +# ifndef MOZ_CODE_COVERAGE +# if defined(_M_X64) + // Since we have PatchIfTargetIsRecognizedTrampoline for x64, we expect the + // original jump destination is returned as a stub. + TestCase("MovPushRet", JumpDestination), + TestCase("MovRaxJump", JumpDestination), + TestCase("DoubleJump", JumpDestination), + + // Passing NoStubAddressCheck as the following testcases return + // a trampoline address instead of the original destination. + TestCase("NearJump", NoStubAddressCheck), + TestCase("OpcodeFF", NoStubAddressCheck), + TestCase("IndirectCall", NoStubAddressCheck), + TestCase("MovImm64", NoStubAddressCheck), +# elif defined(_M_IX86) + // Skip the stub address check as we always generate a trampoline for x86. + TestCase("PushRet", NoStubAddressCheck), + TestCase("MovEaxJump", NoStubAddressCheck), + TestCase("DoubleJump", NoStubAddressCheck), + TestCase("Opcode83", NoStubAddressCheck), + TestCase("LockPrefix", NoStubAddressCheck), + TestCase("LooksLikeLockPrefix", NoStubAddressCheck), +# endif +# if !defined(DEBUG) + // Skip on Debug build because it hits MOZ_ASSERT_UNREACHABLE. + TestCase("UnsupportedOp", ExpectedFail), +# endif // !defined(DEBUG) +# endif // MOZ_CODE_COVERAGE +#endif // defined(__clang__) +}; + +template <typename InterceptorType> +bool TestAssemblyFunctions() { + static const auto patchedFunction = []() { patched_func_called = true; }; + + InterceptorType interceptor; + interceptor.Init("TestDllInterceptor.exe"); + + for (auto& testCase : g_AssemblyTestCases) { + if (testCase.mExpectedStub == NoStubAddressCheck && testCase.mPatchedOnce) { + // For the testcases with NoStubAddressCheck, we revert a hook by + // jumping into the original stub, which is not detourable again. + continue; + } + + typename InterceptorType::template FuncHookType<void (*)()> hook; + bool result = + hook.Set(interceptor, testCase.mFunctionName, patchedFunction); + if (testCase.mExpectedStub == ExpectedFail) { + if (result) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Unexpectedly succeeded to detour %s.\n", + testCase.mFunctionName); + return false; + } +#if defined(NIGHTLY_BUILD) + const Maybe<DetourError>& maybeError = interceptor.GetLastDetourError(); + if (maybeError.isNothing()) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "DetourError was not set on detour error.\n"); + return false; + } + if (maybeError.ref().mErrorCode != + DetourResultCode::DETOUR_PATCHER_CREATE_TRAMPOLINE_ERROR) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "A wrong detour errorcode was set on detour error.\n"); + return false; + } +#endif // defined(NIGHTLY_BUILD) + printf("TEST-PASS | WindowsDllInterceptor | %s\n", + testCase.mFunctionName); + continue; + } + + if (!result) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Failed to detour %s.\n", + testCase.mFunctionName); + return false; + } + + testCase.mPatchedOnce = true; + + const auto actualStub = reinterpret_cast<uintptr_t>(hook.GetStub()); + if (testCase.mExpectedStub != NoStubAddressCheck && + actualStub != testCase.mExpectedStub) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Wrong stub was backed up for %s: %zx\n", + testCase.mFunctionName, actualStub); + return false; + } + + patched_func_called = false; + + auto originalFunction = reinterpret_cast<void (*)()>( + GetProcAddress(GetModuleHandleW(nullptr), testCase.mFunctionName)); + originalFunction(); + + if (!patched_func_called) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Hook from %s was not called\n", + testCase.mFunctionName); + return false; + } + + printf("TEST-PASS | WindowsDllInterceptor | %s\n", testCase.mFunctionName); + } + + return true; +} + +#if defined(_M_X64) && !defined(MOZ_CODE_COVERAGE) +// We want to test hooking and unhooking with unwind information, so we need: +// - a VMSharingPolicy such that ShouldUnhookUponDestruction() is true and +// Items() is implemented; +// - a MMPolicy such that ShouldUnhookUponDestruction() is true and +// kSupportsUnwindInfo is true. +using DetouredCallInterceptor = mozilla::interceptor::WindowsDllInterceptor< + mozilla::interceptor::VMSharingPolicyUnique< + mozilla::interceptor::MMPolicyInProcess>>; + +struct DetouredCallChunk { + alignas(uint32_t) RUNTIME_FUNCTION functionTable[1]; + alignas(uint32_t) uint8_t unwindInfo[sizeof(gDetouredCallUnwindInfo)]; + uint8_t code[gDetouredCallCodeSize]; +}; + +// Unfortunately using RtlAddFunctionTable for static code that lives within +// a module doesn't seem to work. Presumably it conflicts with the static +// function tables. So we recreate gDetouredCall as dynamic code to be able to +// associate it with unwind information. +decltype(&DetouredCallCode) gDetouredCall = + []() -> decltype(&DetouredCallCode) { + auto detouredCallChunk = reinterpret_cast<DetouredCallChunk*>( + VirtualAlloc(nullptr, sizeof(DetouredCallChunk), MEM_RESERVE | MEM_COMMIT, + PAGE_READWRITE)); + if (!detouredCallChunk) { + return nullptr; + } + + detouredCallChunk->functionTable[0].BeginAddress = + offsetof(DetouredCallChunk, code); + detouredCallChunk->functionTable[0].EndAddress = + offsetof(DetouredCallChunk, code) + gDetouredCallCodeSize; + detouredCallChunk->functionTable[0].UnwindData = + offsetof(DetouredCallChunk, unwindInfo); + memcpy(reinterpret_cast<void*>(&detouredCallChunk->unwindInfo), + reinterpret_cast<void*>(gDetouredCallUnwindInfo), + sizeof(detouredCallChunk->unwindInfo)); + memcpy(reinterpret_cast<void*>(&detouredCallChunk->code[0]), + reinterpret_cast<void*>(DetouredCallCode), + sizeof(detouredCallChunk->code)); + + DWORD oldProtect = 0; + if (!VirtualProtect(reinterpret_cast<void*>(detouredCallChunk), + sizeof(DetouredCallChunk), PAGE_EXECUTE_READ, + &oldProtect)) { + VirtualFree(detouredCallChunk, 0, MEM_RELEASE); + return nullptr; + } + + if (!RtlAddFunctionTable(detouredCallChunk->functionTable, 1, + reinterpret_cast<uintptr_t>(detouredCallChunk))) { + VirtualFree(detouredCallChunk, 0, MEM_RELEASE); + return nullptr; + } + + return reinterpret_cast<decltype(&DetouredCallCode)>(detouredCallChunk->code); +}(); + +// We use our own variable instead of patched_func_called because the callee of +// gDetouredCall could end up calling other, already hooked functions could +// change patched_func_called. +static volatile bool sCalledPatchedDetouredCall = false; +static volatile bool sCalledDetouredCallCallee = false; +static volatile bool sCouldUnwindFromDetouredCallCallee = false; +void DetouredCallCallee() { + sCalledDetouredCallCallee = true; + + // Check that we can fully unwind the stack + CONTEXT contextRecord{}; + RtlCaptureContext(&contextRecord); + if (!contextRecord.Rip) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "DetouredCallCallee was unable to get an initial context to work " + "with\n"); + fflush(stdout); + return; + } + while (contextRecord.Rip) { + DWORD64 imageBase = 0; + auto FunctionEntry = + RtlLookupFunctionEntry(contextRecord.Rip, &imageBase, nullptr); + if (!FunctionEntry) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "DetouredCallCallee was unable to get unwind info for ControlPc=%p\n", + reinterpret_cast<void*>(contextRecord.Rip)); + fflush(stdout); + return; + } + printf( + "TEST-PASS | WindowsDllInterceptor | " + "DetouredCallCallee was able to get unwind info for ControlPc=%p\n", + reinterpret_cast<void*>(contextRecord.Rip)); + fflush(stdout); + void* handlerData = nullptr; + DWORD64 establisherFrame = 0; + RtlVirtualUnwind(UNW_FLAG_NHANDLER, imageBase, contextRecord.Rip, + FunctionEntry, &contextRecord, &handlerData, + &establisherFrame, nullptr); + } + sCouldUnwindFromDetouredCallCallee = true; +} + +static DetouredCallInterceptor::FuncHookType<decltype(&DetouredCallCode)> + orig_DetouredCall; + +static void patched_DetouredCall(uintptr_t aCallee) { + sCalledPatchedDetouredCall = true; + return orig_DetouredCall(aCallee); +} + +bool TestCallingDetouredCall(const char* aTestDescription, + bool aExpectCalledPatchedDetouredCall) { + sCalledPatchedDetouredCall = false; + sCalledDetouredCallCallee = false; + sCouldUnwindFromDetouredCallCallee = false; + DetouredCallJumper(reinterpret_cast<uintptr_t>(DetouredCallCallee)); + + if (aExpectCalledPatchedDetouredCall != sCalledPatchedDetouredCall) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "%s: expectCalledPatchedDetouredCall (%d) differs from " + "sCalledPatchedDetouredCall (%d)\n", + aTestDescription, aExpectCalledPatchedDetouredCall, + sCalledPatchedDetouredCall); + fflush(stdout); + return false; + } + + printf( + "TEST-PASS | WindowsDllInterceptor | " + "%s: expectCalledPatchedDetouredCall (%d) matches with " + "sCalledPatchedDetouredCall (%d)\n", + aTestDescription, aExpectCalledPatchedDetouredCall, + sCalledPatchedDetouredCall); + fflush(stdout); + + if (!sCalledDetouredCallCallee) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "%s: gDetouredCall failed to call its callee\n", + aTestDescription); + fflush(stdout); + return false; + } + + printf( + "TEST-PASS | WindowsDllInterceptor | " + "%s: gDetouredCall successfully called its callee\n", + aTestDescription); + fflush(stdout); + + if (!sCouldUnwindFromDetouredCallCallee) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "%s: the callee of gDetouredCall failed to unwind\n", + aTestDescription); + fflush(stdout); + return false; + } + + printf( + "TEST-PASS | WindowsDllInterceptor | " + "%s: the callee of gDetouredCall successfully unwinded\n", + aTestDescription); + fflush(stdout); + return true; +} + +// Test that detouring a call preserves unwind information (bug 1798787). +bool TestDetouredCallUnwindInfo() { + if (!gDetouredCall) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "Failed to generate dynamic gDetouredCall code\n"); + fflush(stdout); + return false; + } + + uintptr_t imageBase = 0; + if (!RtlLookupFunctionEntry(reinterpret_cast<uintptr_t>(gDetouredCall), + &imageBase, nullptr)) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "Failed to find unwind information for dynamic gDetouredCall code\n"); + fflush(stdout); + return false; + } + + // We first double check that we manage to unwind when we *do not* detour + if (!TestCallingDetouredCall("Before hooking", false)) { + return false; + } + + uintptr_t StubAddress = 0; + + // The real test starts here: let's detour and check if we can still unwind + { + DetouredCallInterceptor ExeIntercept; + ExeIntercept.Init("TestDllInterceptor.exe"); + if (!orig_DetouredCall.Set(ExeIntercept, "DetouredCallJumper", + &patched_DetouredCall)) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "Failed to hook the detoured call jumper.\n"); + fflush(stdout); + return false; + } + + printf( + "TEST-PASS | WindowsDllInterceptor | " + "Successfully hooked the detoured call jumper.\n"); + fflush(stdout); + + StubAddress = reinterpret_cast<uintptr_t>(orig_DetouredCall.GetStub()); + if (!RtlLookupFunctionEntry(StubAddress, &imageBase, nullptr)) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "Failed to find unwind information for detoured code of " + "gDetouredCall\n"); + fflush(stdout); + return false; + } + + TestCallingDetouredCall("After hooking", true); + } + + // Check that we can still unwind after clearing the hook. + return TestCallingDetouredCall("After unhooking", false); +} +#endif // defined(_M_X64) && !defined(MOZ_CODE_COVERAGE) + +bool TestDynamicCodePolicy() { + if (!IsWin8Point1OrLater()) { + // Skip if a platform does not support this policy. + return true; + } + + MOZ_PROCESS_MITIGATION_DYNAMIC_CODE_POLICY policy = {}; + policy.ProhibitDynamicCode = true; + + mozilla::DynamicallyLinkedFunctionPtr<decltype(&SetProcessMitigationPolicy)> + pSetProcessMitigationPolicy(L"kernel32.dll", + "SetProcessMitigationPolicy"); + if (!pSetProcessMitigationPolicy) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "SetProcessMitigationPolicy does not exist.\n"); + fflush(stdout); + return false; + } + + if (!pSetProcessMitigationPolicy(ProcessDynamicCodePolicy, &policy, + sizeof(policy))) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "Fail to enable ProcessDynamicCodePolicy.\n"); + fflush(stdout); + return false; + } + + WindowsDllInterceptor ExeIntercept; + ExeIntercept.Init("TestDllInterceptor.exe"); + + // Make sure we fail to hook a function if ProcessDynamicCodePolicy is on + // because we cannot create an executable trampoline region. + if (orig_payloadNotHooked.Set(ExeIntercept, "payloadNotHooked", + &patched_rotatePayload)) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "ProcessDynamicCodePolicy is not working.\n"); + fflush(stdout); + return false; + } + + printf( + "TEST-PASS | WindowsDllInterceptor | " + "Successfully passed TestDynamicCodePolicy.\n"); + fflush(stdout); + return true; +} + +extern "C" int wmain(int argc, wchar_t* argv[]) { + LARGE_INTEGER start; + QueryPerformanceCounter(&start); + + // We disable this part of the test because the code coverage instrumentation + // injects code in rotatePayload in a way that WindowsDllInterceptor doesn't + // understand. +#ifndef MOZ_CODE_COVERAGE + payload initial = {0x12345678, 0xfc4e9d31, 0x87654321}; + payload p0, p1; + ZeroMemory(&p0, sizeof(p0)); + ZeroMemory(&p1, sizeof(p1)); + + p0 = rotatePayload(initial); + + { + WindowsDllInterceptor ExeIntercept; + ExeIntercept.Init("TestDllInterceptor.exe"); + if (orig_rotatePayload.Set(ExeIntercept, "rotatePayload", + &patched_rotatePayload)) { + printf("TEST-PASS | WindowsDllInterceptor | Hook added\n"); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Failed to add " + "hook\n"); + fflush(stdout); + return 1; + } + + p1 = rotatePayload(initial); + + if (patched_func_called) { + printf("TEST-PASS | WindowsDllInterceptor | Hook called\n"); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Hook was not " + "called\n"); + fflush(stdout); + return 1; + } + + if (p0 == p1) { + printf("TEST-PASS | WindowsDllInterceptor | Hook works properly\n"); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Hook didn't return " + "the right information\n"); + fflush(stdout); + return 1; + } + } + + patched_func_called = false; + ZeroMemory(&p1, sizeof(p1)); + + p1 = rotatePayload(initial); + + if (ShouldTestUnhookFunction != patched_func_called) { + printf( + "TEST-PASS | WindowsDllInterceptor | Hook was %scalled after " + "unregistration\n", + ShouldTestUnhookFunction ? "not " : ""); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Hook was %scalled " + "after unregistration\n", + ShouldTestUnhookFunction ? "" : "not "); + fflush(stdout); + return 1; + } + + if (p0 == p1) { + printf( + "TEST-PASS | WindowsDllInterceptor | Original function worked " + "properly\n"); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Original function " + "didn't return the right information\n"); + fflush(stdout); + return 1; + } +#endif + + CredHandle credHandle; + memset(&credHandle, 0, sizeof(CredHandle)); + OBJECT_ATTRIBUTES attributes = {}; + + // NB: These tests should be ordered such that lower-level APIs are tested + // before higher-level APIs. + if (TestShortDetour() && + // Run <ShortInterceptor> first because <WindowsDllInterceptor> + // does not clean up hooks. +#if defined(_M_X64) + TestAssemblyFunctions<ShortInterceptor>() && +#endif + TestAssemblyFunctions<WindowsDllInterceptor>() && +#ifdef _M_IX86 + // We keep this test to hook complex code on x86. (Bug 850957) + TEST_HOOK("ntdll.dll", NtFlushBuffersFile, NotEquals, 0) && +#endif + TEST_HOOK("ntdll.dll", NtCreateFile, NotEquals, 0) && + TEST_HOOK("ntdll.dll", NtReadFile, NotEquals, 0) && + TEST_HOOK("ntdll.dll", NtReadFileScatter, NotEquals, 0) && + TEST_HOOK("ntdll.dll", NtWriteFile, NotEquals, 0) && + TEST_HOOK("ntdll.dll", NtWriteFileGather, NotEquals, 0) && + TEST_HOOK_PARAMS("ntdll.dll", NtQueryFullAttributesFile, NotEquals, 0, + &attributes, nullptr) && + TEST_DETOUR_SKIP_EXEC("ntdll.dll", LdrLoadDll) && + TEST_HOOK("ntdll.dll", LdrUnloadDll, NotEquals, 0) && + MAYBE_TEST_HOOK_SKIP_EXEC(IsWin8OrLater(), "ntdll.dll", + LdrResolveDelayLoadedAPI) && + MAYBE_TEST_HOOK_PARAMS(HasApiSetQueryApiSetPresence(), + "Api-ms-win-core-apiquery-l1-1-0.dll", + ApiSetQueryApiSetPresence, Equals, FALSE, + &gEmptyUnicodeString, &gIsPresent) && + TEST_HOOK("kernelbase.dll", QueryDosDeviceW, Equals, 0) && + TEST_HOOK("kernel32.dll", GetFileAttributesW, Equals, + INVALID_FILE_ATTRIBUTES) && +#if !defined(_M_ARM64) +# ifndef MOZ_ASAN + // Bug 733892: toolkit/crashreporter/nsExceptionHandler.cpp + // This fails on ASan because the ASan runtime already hooked this + // function + TEST_HOOK("kernel32.dll", SetUnhandledExceptionFilter, Ignore, nullptr) && +# endif +#endif // !defined(_M_ARM64) +#ifdef _M_IX86 + TEST_HOOK_FOR_INVALID_HANDLE_VALUE("kernel32.dll", CreateFileW) && +#endif +#if !defined(_M_ARM64) + TEST_HOOK_FOR_INVALID_HANDLE_VALUE("kernel32.dll", CreateFileA) && +#endif // !defined(_M_ARM64) +#if !defined(_M_ARM64) + TEST_HOOK("kernel32.dll", TlsAlloc, NotEquals, TLS_OUT_OF_INDEXES) && + TEST_HOOK_PARAMS("kernel32.dll", TlsFree, Equals, FALSE, + TLS_OUT_OF_INDEXES) && + TEST_HOOK("kernel32.dll", CloseHandle, Equals, FALSE) && + TEST_HOOK("kernel32.dll", DuplicateHandle, Equals, FALSE) && +#endif // !defined(_M_ARM64) + TEST_DETOUR_SKIP_EXEC("kernel32.dll", BaseThreadInitThunk) && +#if defined(_M_X64) || defined(_M_ARM64) + MAYBE_TEST_HOOK(!IsWin8OrLater(), "kernel32.dll", + RtlInstallFunctionTableCallback, Equals, FALSE) && + TEST_HOOK("user32.dll", GetKeyState, Ignore, 0) && // see Bug 1316415 +#endif + TEST_HOOK("user32.dll", GetWindowInfo, Equals, FALSE) && + TEST_HOOK("user32.dll", TrackPopupMenu, Equals, FALSE) && + TEST_DETOUR("user32.dll", CreateWindowExW, Equals, nullptr) && + TEST_HOOK("user32.dll", InSendMessageEx, Equals, ISMEX_NOSEND) && + TEST_HOOK("user32.dll", SendMessageTimeoutW, Equals, 0) && + TEST_HOOK("user32.dll", SetCursorPos, NotEquals, FALSE) && +#if !defined(_M_ARM64) + TEST_HOOK("imm32.dll", ImmGetContext, Equals, nullptr) && +#endif // !defined(_M_ARM64) + TEST_HOOK("imm32.dll", ImmGetCompositionStringW, Ignore, 0) && + TEST_HOOK_SKIP_EXEC("imm32.dll", ImmSetCandidateWindow) && + TEST_HOOK("imm32.dll", ImmNotifyIME, Equals, 0) && + TEST_HOOK("comdlg32.dll", GetSaveFileNameW, Ignore, FALSE) && + TEST_HOOK("comdlg32.dll", GetOpenFileNameW, Ignore, FALSE) && +#if defined(_M_X64) + TEST_HOOK("comdlg32.dll", PrintDlgW, Ignore, 0) && +#endif + MAYBE_TEST_HOOK(ShouldTestTipTsf(), "tiptsf.dll", ProcessCaretEvents, + Ignore, nullptr) && + TEST_HOOK("wininet.dll", InternetOpenA, NotEquals, nullptr) && + TEST_HOOK("wininet.dll", InternetCloseHandle, Equals, FALSE) && + TEST_HOOK("wininet.dll", InternetConnectA, Equals, nullptr) && + TEST_HOOK("wininet.dll", InternetQueryDataAvailable, Equals, FALSE) && + TEST_HOOK("wininet.dll", InternetReadFile, Equals, FALSE) && + TEST_HOOK("wininet.dll", InternetWriteFile, Equals, FALSE) && + TEST_HOOK("wininet.dll", InternetSetOptionA, Equals, FALSE) && + TEST_HOOK("wininet.dll", HttpAddRequestHeadersA, Equals, FALSE) && + TEST_HOOK("wininet.dll", HttpOpenRequestA, Equals, nullptr) && + TEST_HOOK("wininet.dll", HttpQueryInfoA, Equals, FALSE) && + TEST_HOOK("wininet.dll", HttpSendRequestA, Equals, FALSE) && + TEST_HOOK("wininet.dll", HttpSendRequestExA, Equals, FALSE) && + TEST_HOOK("wininet.dll", HttpEndRequestA, Equals, FALSE) && + TEST_HOOK("wininet.dll", InternetQueryOptionA, Equals, FALSE) && + TEST_HOOK("sspicli.dll", AcquireCredentialsHandleA, NotEquals, + SEC_E_OK) && + TEST_HOOK_PARAMS("sspicli.dll", QueryCredentialsAttributesA, Equals, + SEC_E_INVALID_HANDLE, &credHandle, 0, nullptr) && + TEST_HOOK_PARAMS("sspicli.dll", FreeCredentialsHandle, Equals, + SEC_E_INVALID_HANDLE, &credHandle) && +#if defined(_M_X64) && !defined(MOZ_CODE_COVERAGE) + TestDetouredCallUnwindInfo() && +#endif // defined(_M_X64) && !defined(MOZ_CODE_COVERAGE) + // Run TestDynamicCodePolicy() at the end because the policy is + // irreversible. + TestDynamicCodePolicy()) { + printf("TEST-PASS | WindowsDllInterceptor | all checks passed\n"); + + LARGE_INTEGER end, freq; + QueryPerformanceCounter(&end); + + QueryPerformanceFrequency(&freq); + + LARGE_INTEGER result; + result.QuadPart = end.QuadPart - start.QuadPart; + result.QuadPart *= 1000000; + result.QuadPart /= freq.QuadPart; + + printf("Elapsed time: %lld microseconds\n", result.QuadPart); + + return 0; + } + + return 1; +} diff --git a/toolkit/xre/dllservices/tests/TestDllInterceptor.exe.manifest b/toolkit/xre/dllservices/tests/TestDllInterceptor.exe.manifest new file mode 100644 index 0000000000..11287012c5 --- /dev/null +++ b/toolkit/xre/dllservices/tests/TestDllInterceptor.exe.manifest @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" + manifestVersion="1.0" + xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"> + <assemblyIdentity type="win32" + name="TestDllInterceptor" + version="1.0.0.0" /> + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <!-- Need this to use functions in WindowsVersion.h --> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> <!-- Win10 --> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> <!-- Win8.1 --> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> <!-- Win8 --> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> <!-- Win7 --> + </application> + </compatibility> +</assembly> diff --git a/toolkit/xre/dllservices/tests/TestDllInterceptorCrossProcess.cpp b/toolkit/xre/dllservices/tests/TestDllInterceptorCrossProcess.cpp new file mode 100644 index 0000000000..32cf90bac1 --- /dev/null +++ b/toolkit/xre/dllservices/tests/TestDllInterceptorCrossProcess.cpp @@ -0,0 +1,159 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Attributes.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "nsWindowsDllInterceptor.h" +#include "nsWindowsHelpers.h" + +#include <string> + +using std::wstring; + +extern "C" __declspec(dllexport) int ReturnResult() { return 2; } + +static mozilla::CrossProcessDllInterceptor::FuncHookType< + decltype(&ReturnResult)> + gOrigReturnResult; + +static int ReturnResultHook() { + if (gOrigReturnResult() != 2) { + return 3; + } + + return 0; +} + +int ParentMain(int argc, wchar_t* argv[]) { + mozilla::SetArgv0ToFullBinaryPath(argv); + + // We'll add the child process to a job so that, in the event of a failure in + // this parent process, the child process will be automatically terminated. + nsAutoHandle job(::CreateJobObjectW(nullptr, nullptr)); + if (!job) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Job creation " + "failed\n"); + return 1; + } + + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = {}; + jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + if (!::SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation, + &jobInfo, sizeof(jobInfo))) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Job config " + "failed\n"); + return 1; + } + + wchar_t childArgv_1[] = L"-child"; + + wchar_t* childArgv[] = {argv[0], childArgv_1}; + + mozilla::UniquePtr<wchar_t[]> cmdLine( + mozilla::MakeCommandLine(mozilla::ArrayLength(childArgv), childArgv)); + + STARTUPINFOW si = {sizeof(si)}; + PROCESS_INFORMATION pi; + if (!::CreateProcessW(argv[0], cmdLine.get(), nullptr, nullptr, FALSE, + CREATE_SUSPENDED, nullptr, nullptr, &si, &pi)) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Failed to spawn " + "child process\n"); + return 1; + } + + nsAutoHandle childProcess(pi.hProcess); + nsAutoHandle childMainThread(pi.hThread); + + if (!::AssignProcessToJobObject(job.get(), childProcess.get())) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Failed to assign " + "child process to job\n"); + ::TerminateProcess(childProcess.get(), 1); + return 1; + } + + mozilla::nt::CrossExecTransferManager transferMgr(childProcess); + if (!transferMgr) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | " + "CrossExecTransferManager instantiation failed.\n"); + return 1; + } + + mozilla::CrossProcessDllInterceptor intcpt(childProcess.get()); + intcpt.Init("TestDllInterceptorCrossProcess.exe"); + + if (!gOrigReturnResult.Set(transferMgr, intcpt, "ReturnResult", + &ReturnResultHook)) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Failed to add " + "hook\n"); + return 1; + } + + printf("TEST-PASS | DllInterceptorCrossProcess | Hook added\n"); + + if (::ResumeThread(childMainThread.get()) == static_cast<DWORD>(-1)) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Failed to resume " + "child thread\n"); + return 1; + } + + BOOL remoteDebugging; + bool debugging = + ::IsDebuggerPresent() || + (::CheckRemoteDebuggerPresent(childProcess.get(), &remoteDebugging) && + remoteDebugging); + + DWORD waitResult = + ::WaitForSingleObject(childProcess.get(), debugging ? INFINITE : 60000); + if (waitResult != WAIT_OBJECT_0) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Child process " + "failed to finish\n"); + return 1; + } + + DWORD childExitCode; + if (!::GetExitCodeProcess(childProcess.get(), &childExitCode)) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Failed to obtain " + "child process exit code\n"); + return 1; + } + + if (childExitCode) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Child process " + "exit code is %lu instead of 0\n", + childExitCode); + return 1; + } + + printf( + "TEST-PASS | DllInterceptorCrossProcess | Child process exit code is " + "zero\n"); + return 0; +} + +extern "C" int wmain(int argc, wchar_t* argv[]) { + if (argc > 1) { + // clang keeps inlining this call despite every attempt to force it to do + // otherwise. We'll use GetProcAddress and call its function pointer + // instead. + auto pReturnResult = reinterpret_cast<decltype(&ReturnResult)>( + ::GetProcAddress(::GetModuleHandleW(nullptr), "ReturnResult")); + return pReturnResult(); + } + + return ParentMain(argc, argv); +} diff --git a/toolkit/xre/dllservices/tests/TestIATPatcher.cpp b/toolkit/xre/dllservices/tests/TestIATPatcher.cpp new file mode 100644 index 0000000000..2f84c0541a --- /dev/null +++ b/toolkit/xre/dllservices/tests/TestIATPatcher.cpp @@ -0,0 +1,121 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Assertions.h" +#include "mozilla/DynamicallyLinkedFunctionPtr.h" +#include "nsWindowsDllInterceptor.h" +#include "nsWindowsHelpers.h" + +#include <shlwapi.h> + +static int NormalImport() { return ::GetSystemMetrics(SM_CYCAPTION); } + +static bool DelayLoadImport() { + return !!::UrlIsW(L"http://example.com/", URLIS_FILEURL); +} + +static mozilla::WindowsIATPatcher::FuncHookType<decltype(&::GetSystemMetrics)> + gGetSystemMetricsHook; + +static mozilla::WindowsIATPatcher::FuncHookType<decltype(&::MessageBoxA)> + gMessageBoxAHook; + +static mozilla::WindowsIATPatcher::FuncHookType<decltype(&::UrlIsW)> gUrlIsHook; + +static bool gGetSystemMetricsHookCalled = false; + +static int WINAPI GetSystemMetricsHook(int aIndex) { + MOZ_DIAGNOSTIC_ASSERT(aIndex == SM_CYCAPTION); + gGetSystemMetricsHookCalled = true; + return 0; +} + +static bool gUrlIsHookCalled = false; + +static BOOL WINAPI UrlIsWHook(PCWSTR aUrl, URLIS aFlags) { + gUrlIsHookCalled = true; + return TRUE; +} + +static HMODULE GetStrongReferenceToExeModule() { + HMODULE result; + if (!::GetModuleHandleExW(0, nullptr, &result)) { + return nullptr; + } + + return result; +} + +#define PRINT_FAIL(msg) printf("TEST-UNEXPECTED-FAIL | IATPatcher | " msg "\n") + +extern "C" int wmain(int argc, wchar_t* argv[]) { + nsModuleHandle ourModule1(GetStrongReferenceToExeModule()); + if (!ourModule1) { + PRINT_FAIL("Failed obtaining HMODULE for executable"); + return 1; + } + + if (!gGetSystemMetricsHook.Set(ourModule1, "user32.dll", "GetSystemMetrics", + &GetSystemMetricsHook)) { + PRINT_FAIL("Failed setting GetSystemMetrics hook"); + return 1; + } + + if (NormalImport() || !gGetSystemMetricsHookCalled) { + PRINT_FAIL("GetSystemMetrics hook was not called"); + return 1; + } + + static const mozilla::StaticDynamicallyLinkedFunctionPtr< + decltype(&::GetSystemMetrics)> + pRealGetSystemMetrics(L"user32.dll", "GetSystemMetrics"); + if (!pRealGetSystemMetrics) { + PRINT_FAIL("Failed resolving real GetSystemMetrics pointer"); + return 1; + } + + if (gGetSystemMetricsHook.GetStub() != pRealGetSystemMetrics) { + PRINT_FAIL( + "GetSystemMetrics hook stub pointer does not match real " + "GetSystemMetrics pointer"); + return 1; + } + + nsModuleHandle ourModule2(GetStrongReferenceToExeModule()); + if (!ourModule2) { + PRINT_FAIL("Failed obtaining HMODULE for executable"); + return 1; + } + + // This should fail becuase the test never calls, and thus never imports, + // MessageBoxA + if (gMessageBoxAHook.Set(ourModule2, "user32.dll", "MessageBoxA", nullptr)) { + PRINT_FAIL("Setting MessageBoxA hook succeeded when it should have failed"); + return 1; + } + + nsModuleHandle ourModule3(GetStrongReferenceToExeModule()); + if (!ourModule3) { + PRINT_FAIL("Failed obtaining HMODULE for executable"); + return 1; + } + + // These tests involve a delay-loaded import, which are not supported; we + // expect these tests to FAIL. + + if (gUrlIsHook.Set(ourModule3, "shlwapi.dll", "UrlIsW", &UrlIsWHook)) { + PRINT_FAIL("gUrlIsHook.Set should have failed"); + return 1; + } + + if (DelayLoadImport() || gUrlIsHookCalled) { + PRINT_FAIL("gUrlIsHook should not have been called"); + return 1; + } + + printf("TEST-PASS | IATPatcher | All tests passed.\n"); + return 0; +} diff --git a/toolkit/xre/dllservices/tests/TestMMPolicy.cpp b/toolkit/xre/dllservices/tests/TestMMPolicy.cpp new file mode 100644 index 0000000000..1ae93a4ed1 --- /dev/null +++ b/toolkit/xre/dllservices/tests/TestMMPolicy.cpp @@ -0,0 +1,204 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsWindowsDllInterceptor.h" + +#include <functional> + +mozilla::interceptor::MMPolicyInProcess gPolicy; + +void DepleteVirtualAddress( + uint8_t* aStart, size_t aSize, + const std::function<void(void*)>& aPostAllocCallback) { + const DWORD granularity = gPolicy.GetAllocGranularity(); + if (aStart == 0 || aSize < granularity) { + return; + } + + uint8_t* alignedStart = reinterpret_cast<uint8_t*>( + (((reinterpret_cast<uintptr_t>(aStart) - 1) / granularity) + 1) * + granularity); + aSize -= (alignedStart - aStart); + if (auto p = VirtualAlloc(alignedStart, aSize, MEM_RESERVE, PAGE_NOACCESS)) { + aPostAllocCallback(p); + return; + } + + uintptr_t mask = ~(static_cast<uintptr_t>(granularity) - 1); + size_t halfSize = (aSize >> 1) & mask; + if (halfSize == 0) { + return; + } + + DepleteVirtualAddress(aStart, halfSize, aPostAllocCallback); + DepleteVirtualAddress(aStart + halfSize, aSize - halfSize, + aPostAllocCallback); +} + +bool ValidateFreeRegion(LPVOID aRegion, size_t aDesiredLen) { + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery(aRegion, &mbi, sizeof(mbi)) != sizeof(mbi)) { + printf( + "TEST-FAILED | TestMMPolicy | " + "VirtualQuery(%p) failed - %08lx\n", + aRegion, GetLastError()); + return false; + } + + if (mbi.State != MEM_FREE) { + printf( + "TEST-FAILED | TestMMPolicy | " + "%p is not within a free region\n", + aRegion); + return false; + } + + if (aRegion != mbi.BaseAddress || + reinterpret_cast<uintptr_t>(mbi.BaseAddress) % + gPolicy.GetAllocGranularity()) { + printf( + "TEST-FAILED | TestMMPolicy | " + "%p is not a region's start address\n", + aRegion); + return false; + } + + LPVOID allocated = VirtualAlloc(aRegion, aDesiredLen, + MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + if (!allocated) { + printf( + "TEST-FAILED | TestMMPolicy | " + "VirtualAlloc(%p) failed - %08lx\n", + aRegion, GetLastError()); + return false; + } + + if (!VirtualFree(allocated, 0, MEM_RELEASE)) { + printf( + "TEST-FAILED | TestMMPolicy | " + "VirtualFree(%p) failed - %08lx\n", + allocated, GetLastError()); + return false; + } + + return true; +} + +bool TestFindRegion() { + // Skip the near-null addresses + uint8_t* minAddr = reinterpret_cast<uint8_t*>( + std::max(gPolicy.GetAllocGranularity(), 0x1000000ul)); + // 64bit address space is too large to deplete. 32bit space is enough. + uint8_t* maxAddr = reinterpret_cast<uint8_t*>(std::min( + gPolicy.GetMaxUserModeAddress(), static_cast<uintptr_t>(0xffffffff))); + + // Keep one of the regions we allocate so that we can release it later. + void* lastResort = nullptr; + + // Reserve all free regions in the range [minAddr, maxAddr] + for (uint8_t* address = minAddr; address <= maxAddr;) { + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery(address, &mbi, sizeof(mbi)) != sizeof(mbi)) { + printf( + "TEST-FAILED | TestMMPolicy | " + "VirtualQuery(%p) failed - %08lx\n", + address, GetLastError()); + break; + } + + address = reinterpret_cast<uint8_t*>(mbi.BaseAddress); + if (mbi.State == MEM_FREE) { + DepleteVirtualAddress(address, mbi.RegionSize, + [&lastResort](void* aAllocated) { + // Pick the first address we allocate to make sure + // FindRegion scans the full range. + if (!lastResort) { + lastResort = aAllocated; + } + }); + } + + address += mbi.RegionSize; + } + + if (!lastResort) { + printf( + "TEST-SKIPPED | TestMMPolicy | " + "No free region in [%p - %p]. Skipping the testcase.\n", + minAddr, maxAddr); + return true; + } + + // Make sure there are no free regions + PVOID freeRegion = + gPolicy.FindRegion(GetCurrentProcess(), 1, minAddr, maxAddr); + if (freeRegion) { + if (reinterpret_cast<uintptr_t>(freeRegion) % + gPolicy.GetAllocGranularity()) { + printf( + "TEST-FAILED | TestMMPolicy | " + "MMPolicyBase::FindRegion returned an unaligned address %p.\n", + freeRegion); + return false; + } + + printf( + "TEST-SKIPPED | TestMMPolicy | " + "%p was freed after depletion. Skipping the testcase.\n", + freeRegion); + return true; + } + + // Free one region, and thus we can expect FindRegion finds this region + if (!VirtualFree(lastResort, 0, MEM_RELEASE)) { + printf( + "TEST-FAILED | TestMMPolicy | " + "VirtualFree(%p) failed - %08lx\n", + lastResort, GetLastError()); + return false; + } + printf("The region starting from %p has been freed.\n", lastResort); + + // Run the function several times because it uses a randon number inside + // and its result is nondeterministic. + for (int i = 0; i < 50; ++i) { + // Because one region was freed, a desire up to one region + // should be fulfilled. + const size_t desiredLengths[] = {1, gPolicy.GetAllocGranularity()}; + + for (auto desiredLen : desiredLengths) { + freeRegion = + gPolicy.FindRegion(GetCurrentProcess(), desiredLen, minAddr, maxAddr); + if (!freeRegion) { + printf( + "TEST-FAILED | TestMMPolicy | " + "Failed to find a free region.\n"); + return false; + } + + if (!ValidateFreeRegion(freeRegion, desiredLen)) { + return false; + } + } + } + + return true; +} + +extern "C" int wmain(int argc, wchar_t* argv[]) { + // Preload delayload modules (e.g. advapi32.dll, bcryptPrimitives.dll, etc.) + // by calling rand_s(), which is used in MMPolicy::FindRegion, before + // depleting the process memory during the test. + unsigned int rnd = 0; + rand_s(&rnd); + + if (!TestFindRegion()) { + return 1; + } + + printf("TEST-PASS | TestMMPolicy | All tests passed.\n"); + return 0; +} diff --git a/toolkit/xre/dllservices/tests/gtest/TestDLLBlocklist.cpp b/toolkit/xre/dllservices/tests/gtest/TestDLLBlocklist.cpp new file mode 100644 index 0000000000..5f141b22ae --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDLLBlocklist.cpp @@ -0,0 +1,223 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> +#include <winternl.h> + +#include <process.h> + +#include "gtest/gtest.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Char16.h" +#include "mozilla/gtest/MozAssertions.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsWindowsHelpers.h" + +static nsString GetFullPath(const nsAString& aLeaf) { + nsCOMPtr<nsIFile> f; + + EXPECT_TRUE(NS_SUCCEEDED( + NS_GetSpecialDirectory(NS_OS_CURRENT_WORKING_DIR, getter_AddRefs(f)))); + + EXPECT_NS_SUCCEEDED(f->Append(aLeaf)); + + bool exists; + EXPECT_TRUE(NS_SUCCEEDED(f->Exists(&exists)) && exists); + + nsString ret; + EXPECT_NS_SUCCEEDED(f->GetPath(ret)); + return ret; +} + +TEST(TestDllBlocklist, BlockDllByName) +{ + // The DLL name has capital letters, so this also tests that the comparison + // is case-insensitive. + constexpr auto kLeafName = u"TestDllBlocklist_MatchByName.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + + EXPECT_TRUE(!hDll); + EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get())); + + hDll.own(::LoadLibraryExW(dllPath.get(), nullptr, LOAD_LIBRARY_AS_DATAFILE)); + // Mapped as MEM_MAPPED + PAGE_READONLY + EXPECT_TRUE(hDll); +} + +TEST(TestDllBlocklist, BlockDllByVersion) +{ + constexpr auto kLeafName = u"TestDllBlocklist_MatchByVersion.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + + EXPECT_TRUE(!hDll); + EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get())); + + hDll.own( + ::LoadLibraryExW(dllPath.get(), nullptr, LOAD_LIBRARY_AS_IMAGE_RESOURCE)); + // Mapped as MEM_IMAGE + PAGE_READONLY + EXPECT_TRUE(hDll); +} + +TEST(TestDllBlocklist, AllowDllByVersion) +{ + constexpr auto kLeafName = u"TestDllBlocklist_AllowByVersion.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + + EXPECT_TRUE(!!hDll); + EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get())); +} + +TEST(TestDllBlocklist, SocketProcessOnly_AllowInMainProcess) +{ + constexpr auto kLeafName = u"TestDllBlocklist_SocketProcessOnly.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + + EXPECT_TRUE(!!hDll); + EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get())); +} + +TEST(TestDllBlocklist, UtilityProcessOnly_AllowInMainProcess) +{ + constexpr auto kLeafName = u"TestDllBlocklist_UtilityProcessOnly.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + + EXPECT_TRUE(!!hDll); + EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get())); +} + +// RedirectToNoOpEntryPoint needs the launcher process. +#if defined(MOZ_LAUNCHER_PROCESS) +TEST(TestDllBlocklist, NoOpEntryPoint) +{ + // DllMain of this dll has MOZ_RELEASE_ASSERT. This test makes sure we load + // the module successfully without running DllMain. + constexpr auto kLeafName = u"TestDllBlocklist_NoOpEntryPoint.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + +# if defined(MOZ_ASAN) + // With ASAN, the test uses mozglue's blocklist where + // REDIRECT_TO_NOOP_ENTRYPOINT is ignored. So LoadLibraryW + // is expected to fail. + EXPECT_TRUE(!hDll); + EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get())); +# else + EXPECT_TRUE(!!hDll); + EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get())); +# endif +} + +// User blocklist needs the launcher process +TEST(TestDllBlocklist, UserBlocked) +{ + constexpr auto kLeafName = u"TestDllBlocklist_UserBlocked.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + +// With ASAN, the test uses mozglue's blocklist where +// the user blocklist is not used. +# if !defined(MOZ_ASAN) + EXPECT_TRUE(!hDll); + EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get())); +# endif + hDll.own(::LoadLibraryExW(dllPath.get(), nullptr, LOAD_LIBRARY_AS_DATAFILE)); + // Mapped as MEM_MAPPED + PAGE_READONLY + EXPECT_TRUE(hDll); +} +#endif // defined(MOZ_LAUNCHER_PROCESS) + +#define DLL_BLOCKLIST_ENTRY(name, ...) {name, __VA_ARGS__}, +#define DLL_BLOCKLIST_STRING_TYPE const char* +#include "mozilla/WindowsDllBlocklistLegacyDefs.h" + +TEST(TestDllBlocklist, BlocklistIntegrity) +{ + nsTArray<DLL_BLOCKLIST_STRING_TYPE> dupes; + DECLARE_POINTER_TO_FIRST_DLL_BLOCKLIST_ENTRY(pFirst); + DECLARE_POINTER_TO_LAST_DLL_BLOCKLIST_ENTRY(pLast); + + EXPECT_FALSE(pLast->mName || pLast->mMaxVersion || pLast->mFlags); + + for (size_t i = 0; i < mozilla::ArrayLength(gWindowsDllBlocklist) - 1; ++i) { + auto pEntry = pFirst + i; + + // Validate name + EXPECT_TRUE(!!pEntry->mName); + EXPECT_GT(strlen(pEntry->mName), 3U); + + // Check the filename for valid characters. + for (auto pch = pEntry->mName; *pch != 0; ++pch) { + EXPECT_FALSE(*pch >= 'A' && *pch <= 'Z'); + } + + // Check for duplicate entries + for (auto&& dupe : dupes) { + EXPECT_NE(stricmp(dupe, pEntry->mName), 0); + } + + dupes.AppendElement(pEntry->mName); + } +} + +TEST(TestDllBlocklist, BlockThreadWithLoadLibraryEntryPoint) +{ + // Only supported on Nightly +#if defined(NIGHTLY_BUILD) + using ThreadProc = unsigned(__stdcall*)(void*); + + constexpr auto kLeafNameW = u"TestDllBlocklist_MatchByVersion.dll"_ns; + + nsString fullPathW = GetFullPath(kLeafNameW); + EXPECT_FALSE(fullPathW.IsEmpty()); + + nsAutoHandle threadW(reinterpret_cast<HANDLE>( + _beginthreadex(nullptr, 0, reinterpret_cast<ThreadProc>(&::LoadLibraryW), + (void*)fullPathW.get(), 0, nullptr))); + + EXPECT_TRUE(!!threadW); + EXPECT_EQ(::WaitForSingleObject(threadW, INFINITE), WAIT_OBJECT_0); + +# if !defined(MOZ_ASAN) + // ASAN builds under Windows 11 can have unexpected thread exit codes. + // See bug 1798796 + DWORD exitCode; + EXPECT_TRUE(::GetExitCodeThread(threadW, &exitCode) && !exitCode); +# endif // !defined(MOZ_ASAN) + EXPECT_TRUE(!::GetModuleHandleW(kLeafNameW.get())); + + const NS_LossyConvertUTF16toASCII fullPathA(fullPathW); + EXPECT_FALSE(fullPathA.IsEmpty()); + + nsAutoHandle threadA(reinterpret_cast<HANDLE>( + _beginthreadex(nullptr, 0, reinterpret_cast<ThreadProc>(&::LoadLibraryA), + (void*)fullPathA.get(), 0, nullptr))); + + EXPECT_TRUE(!!threadA); + EXPECT_EQ(::WaitForSingleObject(threadA, INFINITE), WAIT_OBJECT_0); +# if !defined(MOZ_ASAN) + // ASAN builds under Windows 11 can have unexpected thread exit codes. + // See bug 1798796 + EXPECT_TRUE(::GetExitCodeThread(threadA, &exitCode) && !exitCode); +# endif // !defined(MOZ_ASAN) + EXPECT_TRUE(!::GetModuleHandleW(kLeafNameW.get())); +#endif // defined(NIGHTLY_BUILD) +} diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.cpp @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) { return TRUE; } diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.rc b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.rc new file mode 100644 index 0000000000..f56aa099ff --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.rc @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <winver.h> + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 5,5,5,6 + PRODUCTVERSION 5,5,5,1 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "mozilla.org" + VALUE "FileDescription", L"Test DLL" + VALUE "FileVersion", "1.0" + VALUE "InternalName", "Test DLL" + VALUE "OriginalFilename", "TestDllBlocklist_AllowByVersion.dll" + VALUE "ProductName", "Test DLL" + VALUE "ProductVersion", "1.0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1252 + END +END diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_AllowByVersion/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_AllowByVersion/moz.build new file mode 100644 index 0000000000..0987cdde1a --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_AllowByVersion/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIST_INSTALL = False + +SharedLibrary("TestDllBlocklist_AllowByVersion") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_AllowByVersion.cpp", +] + +RCFILE = "TestDllBlocklist_AllowByVersion.rc" + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_AllowByVersion.dll"] diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByName/TestDllBlocklist_MatchByName.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByName/TestDllBlocklist_MatchByName.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByName/TestDllBlocklist_MatchByName.cpp @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) { return TRUE; } diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByName/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByName/moz.build new file mode 100644 index 0000000000..f34931898a --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByName/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIST_INSTALL = False + +SharedLibrary("TestDllBlocklist_MatchByName") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_MatchByName.cpp", +] + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_MatchByName.dll"] diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.cpp @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) { return TRUE; } diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.rc b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.rc new file mode 100644 index 0000000000..7390c1cb34 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.rc @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <winver.h> + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 5,5,5,5 + PRODUCTVERSION 5,5,5,1 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "mozilla.org" + VALUE "FileDescription", L"Test DLL" + VALUE "FileVersion", "1.0" + VALUE "InternalName", "Test DLL" + VALUE "OriginalFilename", "TestDllBlocklist_MatchByVersion.dll" + VALUE "ProductName", "Test DLL" + VALUE "ProductVersion", "1.0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1252 + END +END diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByVersion/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByVersion/moz.build new file mode 100644 index 0000000000..38e10524c7 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_MatchByVersion/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIST_INSTALL = False + +SharedLibrary("TestDllBlocklist_MatchByVersion") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_MatchByVersion.cpp", +] + +RCFILE = "TestDllBlocklist_MatchByVersion.rc" + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_MatchByVersion.dll"] diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.cpp new file mode 100644 index 0000000000..2505b8b700 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.cpp @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> + +#include "mozilla/Assertions.h" + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) { + MOZ_RELEASE_ASSERT(0); + return TRUE; +} diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.rc b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.rc new file mode 100644 index 0000000000..7c79dac373 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.rc @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <winver.h> + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 5,5,5,5 + PRODUCTVERSION 5,5,5,1 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "mozilla.org" + VALUE "FileDescription", L"Test DLL" + VALUE "FileVersion", "1.0" + VALUE "InternalName", "Test DLL" + VALUE "OriginalFilename", "TestDllBlocklist_NoOpEntryPoint.dll" + VALUE "ProductName", "Test DLL" + VALUE "ProductVersion", "1.0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1252 + END +END diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_NoOpEntryPoint/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_NoOpEntryPoint/moz.build new file mode 100644 index 0000000000..e9a10a150a --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_NoOpEntryPoint/moz.build @@ -0,0 +1,21 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIST_INSTALL = False + +SharedLibrary("TestDllBlocklist_NoOpEntryPoint") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_NoOpEntryPoint.cpp", +] + +RCFILE = "TestDllBlocklist_NoOpEntryPoint.rc" + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_NoOpEntryPoint.dll"] + +OS_LIBS += [ + "uuid", +] diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SocketProcessOnly/TestDllBlocklist_SocketProcessOnly.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SocketProcessOnly/TestDllBlocklist_SocketProcessOnly.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SocketProcessOnly/TestDllBlocklist_SocketProcessOnly.cpp @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) { return TRUE; } diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SocketProcessOnly/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SocketProcessOnly/moz.build new file mode 100644 index 0000000000..dc93544e1b --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_SocketProcessOnly/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIST_INSTALL = False + +SharedLibrary("TestDllBlocklist_SocketProcessOnly") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_SocketProcessOnly.cpp", +] + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_SocketProcessOnly.dll"] diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UserBlocked/TestDllBlocklist_UserBlocked.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UserBlocked/TestDllBlocklist_UserBlocked.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UserBlocked/TestDllBlocklist_UserBlocked.cpp @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) { return TRUE; } diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UserBlocked/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UserBlocked/moz.build new file mode 100644 index 0000000000..31996c5cb2 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UserBlocked/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIST_INSTALL = False + +SharedLibrary("TestDllBlocklist_UserBlocked") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_UserBlocked.cpp", +] + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_UserBlocked.dll"] diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UtilityProcessOnly/TestDllBlocklist_UtilityProcessOnly.cpp b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UtilityProcessOnly/TestDllBlocklist_UtilityProcessOnly.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UtilityProcessOnly/TestDllBlocklist_UtilityProcessOnly.cpp @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) { return TRUE; } diff --git a/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UtilityProcessOnly/moz.build b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UtilityProcessOnly/moz.build new file mode 100644 index 0000000000..913d0f155c --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestDllBlocklist_UtilityProcessOnly/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIST_INSTALL = False + +SharedLibrary("TestDllBlocklist_UtilityProcessOnly") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_UtilityProcessOnly.cpp", +] + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_UtilityProcessOnly.dll"] diff --git a/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules.cpp b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules.cpp new file mode 100644 index 0000000000..f7865a2ed0 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules.cpp @@ -0,0 +1,461 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +#include "gtest/gtest.h" + +#include "js/RegExp.h" +#include "mozilla/BinarySearch.h" +#include "mozilla/gtest/MozAssertions.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/UntrustedModulesProcessor.h" +#include "mozilla/WinDllServices.h" +#include "nsContentUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "TelemetryFixture.h" +#include "UntrustedModulesBackupService.h" +#include "UntrustedModulesDataSerializer.h" + +using namespace mozilla; + +class ModuleLoadCounter final { + nsTHashMap<nsStringCaseInsensitiveHashKey, int> mCounters; + + public: + template <size_t N> + ModuleLoadCounter(const nsString (&aNames)[N], const int (&aCounts)[N]) + : mCounters(N) { + for (size_t i = 0; i < N; ++i) { + mCounters.InsertOrUpdate(aNames[i], aCounts[i]); + } + } + + template <size_t N> + bool Remains(const nsString (&aNames)[N], const int (&aCounts)[N]) { + EXPECT_EQ(mCounters.Count(), N); + if (mCounters.Count() != N) { + return false; + } + + bool result = true; + for (size_t i = 0; i < N; ++i) { + auto entry = mCounters.Lookup(aNames[i]); + if (!entry) { + wprintf(L"%s is not registered.\n", + static_cast<const wchar_t*>(aNames[i].get())); + result = false; + } else if (*entry != aCounts[i]) { + // We can return false, but let's print out all unmet modules + // which may be helpful to investigate test failures. + wprintf(L"%s:%4d\n", static_cast<const wchar_t*>(aNames[i].get()), + *entry); + result = false; + } + } + return result; + } + + bool IsDone() const { + bool allZero = true; + for (const auto& data : mCounters.Values()) { + if (data < 0) { + // If any counter is negative, we know the test fails. + // No need to continue. + return true; + } + if (data > 0) { + allZero = false; + } + } + // If all counters are zero, the test finished nicely. Otherwise, those + // counters are expected to be decremented later. Let's continue. + return allZero; + } + + void Decrement(const nsString& aName) { + if (auto entry = mCounters.Lookup(aName)) { + --(*entry); + } + } +}; + +class UntrustedModulesCollector { + static constexpr int kMaximumPendingQueries = 500; + Vector<UntrustedModulesData> mData; + + public: + Vector<UntrustedModulesData>& Data() { return mData; } + + nsresult Collect(ModuleLoadCounter& aChecker) { + nsresult rv = NS_OK; + + mData.clear(); + int pendingQueries = 0; + + EXPECT_TRUE(SpinEventLoopUntil( + "xre:UntrustedModulesCollector"_ns, + [this, &pendingQueries, &aChecker, &rv]() { + // Some of expected loaded modules are still missing + // after kMaximumPendingQueries queries were submitted. + // Giving up here to avoid an infinite loop. + if (pendingQueries >= kMaximumPendingQueries) { + rv = NS_ERROR_ABORT; + return true; + } + + ++pendingQueries; + + RefPtr<DllServices> dllSvc(DllServices::Get()); + dllSvc->GetUntrustedModulesData()->Then( + GetMainThreadSerialEventTarget(), __func__, + [this, &pendingQueries, + &aChecker](Maybe<UntrustedModulesData>&& aResult) { + EXPECT_GT(pendingQueries, 0); + --pendingQueries; + + if (aResult.isSome()) { + wprintf(L"Received data. (pendingQueries=%d)\n", + pendingQueries); + for (auto item : aResult.ref().mEvents) { + aChecker.Decrement(item->mEvent.mRequestedDllName); + } + EXPECT_TRUE(mData.emplaceBack(std::move(aResult.ref()))); + } + }, + [&pendingQueries, &rv](nsresult aReason) { + EXPECT_GT(pendingQueries, 0); + --pendingQueries; + + wprintf(L"GetUntrustedModulesData() failed - %08x\n", aReason); + EXPECT_TRUE(false); + rv = aReason; + }); + + // Keep calling GetUntrustedModulesData() until we meet the condition. + return aChecker.IsDone(); + })); + + EXPECT_TRUE(SpinEventLoopUntil( + "xre:UntrustedModulesCollector(pendingQueries)"_ns, + [&pendingQueries]() { return pendingQueries <= 0; })); + + return rv; + } +}; + +class UntrustedModulesFixture : public TelemetryTestFixture { + static constexpr int kLoadCountBeforeDllServices = 5; + static constexpr int kLoadCountAfterDllServices = 5; + static constexpr uint32_t kMaxModulesArrayLen = 10; + + // One of the important test scenarios is to load modules before DllServices + // is initialized and to make sure those loading events are forwarded when + // DllServices is initialized. + // However, GTest instantiates a Fixture class every testcase and there is + // no way to re-enable DllServices and UntrustedModulesProcessor once it's + // disabled, which means no matter how many testcases we have, only the + // first testcase exercises that scenario. That's why we implement that + // test scenario in InitialModuleLoadOnce as a static member and runs it + // in the first testcase to be executed. + static INIT_ONCE sInitLoadOnce; + static UntrustedModulesCollector sInitLoadDataCollector; + + static nsString PrependWorkingDir(const nsAString& aLeaf) { + nsCOMPtr<nsIFile> file; + EXPECT_TRUE(NS_SUCCEEDED(NS_GetSpecialDirectory(NS_OS_CURRENT_WORKING_DIR, + getter_AddRefs(file)))); + EXPECT_NS_SUCCEEDED(file->Append(aLeaf)); + bool exists; + EXPECT_TRUE(NS_SUCCEEDED(file->Exists(&exists)) && exists); + nsString fullPath; + EXPECT_NS_SUCCEEDED(file->GetPath(fullPath)); + return fullPath; + } + + static BOOL CALLBACK InitialModuleLoadOnce(PINIT_ONCE, void*, void**); + + protected: + static constexpr int kInitLoadCount = + kLoadCountBeforeDllServices + kLoadCountAfterDllServices; + static const nsString kTestModules[]; + + static void ValidateUntrustedModules(const UntrustedModulesData& aData, + bool aIsTruncatedData = false); + + static void LoadAndFree(const nsAString& aLeaf) { + nsModuleHandle dll(::LoadLibraryW(PrependWorkingDir(aLeaf).get())); + EXPECT_TRUE(!!dll); + } + + virtual void SetUp() override { + TelemetryTestFixture::SetUp(); + ::InitOnceExecuteOnce(&sInitLoadOnce, InitialModuleLoadOnce, nullptr, + nullptr); + } + + static const Vector<UntrustedModulesData>& GetInitLoadData() { + return sInitLoadDataCollector.Data(); + } + + // This method is useful if we want a new instance of UntrustedModulesData + // which is not copyable. + static UntrustedModulesData CollectSingleData() { + // If we call LoadAndFree more than once, those loading events are + // likely to be merged into an instance of UntrustedModulesData, + // meaning the length of the collector's vector is at least one but + // the exact number is unknown. + LoadAndFree(kTestModules[0]); + + UntrustedModulesCollector collector; + ModuleLoadCounter waitForOne({kTestModules[0]}, {1}); + EXPECT_NS_SUCCEEDED(collector.Collect(waitForOne)); + EXPECT_TRUE(waitForOne.Remains({kTestModules[0]}, {0})); + EXPECT_EQ(collector.Data().length(), 1U); + + // Cannot "return collector.Data()[0]" as copy ctor is deleted. + return UntrustedModulesData(std::move(collector.Data()[0])); + } + + template <typename DataFetcherT> + void ValidateJSValue(const char16_t* aPattern, size_t aPatternLength, + DataFetcherT&& aDataFetcher) { + AutoJSContextWithGlobal cx(mCleanGlobal); + mozilla::Telemetry::UntrustedModulesDataSerializer serializer( + cx.GetJSContext(), kMaxModulesArrayLen); + EXPECT_TRUE(!!serializer); + aDataFetcher(serializer); + + JS::Rooted<JS::Value> jsval(cx.GetJSContext()); + serializer.GetObject(&jsval); + + nsAutoString json; + EXPECT_TRUE(nsContentUtils::StringifyJSON(cx.GetJSContext(), &jsval, json)); + + JS::Rooted<JSObject*> re( + cx.GetJSContext(), + JS::NewUCRegExpObject(cx.GetJSContext(), aPattern, aPatternLength, + JS::RegExpFlag::Global)); + EXPECT_TRUE(!!re); + + JS::Rooted<JS::Value> matchResult(cx.GetJSContext(), JS::NullValue()); + size_t idx = 0; + EXPECT_TRUE(JS::ExecuteRegExpNoStatics(cx.GetJSContext(), re, json.get(), + json.Length(), &idx, true, + &matchResult)); + // On match, with aOnlyMatch = true, ExecuteRegExpNoStatics returns boolean + // true. If no match, ExecuteRegExpNoStatics returns Null. + EXPECT_TRUE(matchResult.isBoolean() && matchResult.toBoolean()); + if (!matchResult.isBoolean() || !matchResult.toBoolean()) { + // If match failed, print out the actual JSON kindly. + wprintf(L"JSON: %s\n", static_cast<const wchar_t*>(json.get())); + wprintf(L"RE: %s\n", aPattern); + } + } +}; + +const nsString UntrustedModulesFixture::kTestModules[] = { + // Sorted for binary-search + u"TestUntrustedModules_Dll1.dll"_ns, + u"TestUntrustedModules_Dll2.dll"_ns, +}; + +INIT_ONCE UntrustedModulesFixture::sInitLoadOnce = INIT_ONCE_STATIC_INIT; +UntrustedModulesCollector UntrustedModulesFixture::sInitLoadDataCollector; + +void UntrustedModulesFixture::ValidateUntrustedModules( + const UntrustedModulesData& aData, bool aIsTruncatedData) { + // This defines a list of modules which are listed on our blocklist and + // thus its loading status is not expected to be Status::Loaded. + // Although the UntrustedModulesFixture test does not touch any of them, + // the current process might have run a test like TestDllBlocklist where + // we try to load and block them. + const struct { + const wchar_t* mName; + ModuleLoadInfo::Status mStatus; + } kKnownModules[] = { + // Sorted by mName for binary-search + {L"TestDllBlocklist_MatchByName.dll", ModuleLoadInfo::Status::Blocked}, + {L"TestDllBlocklist_MatchByVersion.dll", ModuleLoadInfo::Status::Blocked}, + {L"TestDllBlocklist_NoOpEntryPoint.dll", + ModuleLoadInfo::Status::Redirected}, +#if !defined(MOZ_ASAN) + // With ASAN, the test uses mozglue's blocklist where + // the user blocklist is not used. So only check for this + // DLL in the non-ASAN case. + {L"TestDllBlocklist_UserBlocked.dll", ModuleLoadInfo::Status::Blocked}, +#endif // !defined(MOZ_ASAN) + }; + + EXPECT_EQ(aData.mProcessType, GeckoProcessType_Default); + EXPECT_EQ(aData.mPid, ::GetCurrentProcessId()); + + nsTHashtable<nsPtrHashKey<void>> moduleSet; + for (const RefPtr<ModuleRecord>& module : aData.mModules.Values()) { + moduleSet.PutEntry(module); + } + + size_t numBlockedEvents = 0; + for (auto item : aData.mEvents) { + const auto& evt = item->mEvent; + const nsDependentSubstring leafName = + nt::GetLeafName(evt.mModule->mResolvedNtName); + const nsAutoString leafNameStr(leafName.Data(), leafName.Length()); + const ModuleLoadInfo::Status loadStatus = + static_cast<ModuleLoadInfo::Status>(evt.mLoadStatus); + if (loadStatus == ModuleLoadInfo::Status::Blocked) { + ++numBlockedEvents; + } + + size_t match; + if (BinarySearchIf( + kKnownModules, 0, ArrayLength(kKnownModules), + [&leafNameStr](const auto& aVal) { + return _wcsicmp(leafNameStr.get(), aVal.mName); + }, + &match)) { + EXPECT_EQ(loadStatus, kKnownModules[match].mStatus); + } else { + EXPECT_EQ(evt.mLoadStatus, 0U); + } + + if (BinarySearchIf( + kTestModules, 0, ArrayLength(kTestModules), + [&leafNameStr](const auto& aVal) { + return _wcsicmp(leafNameStr.get(), aVal.get()); + }, + &match)) { + // We know the test modules are loaded in the main thread, + // but we don't know about other modules. + EXPECT_EQ(evt.mThreadId, ::GetCurrentThreadId()); + } + + // Make sure mModule is pointing to an entry of mModules. + EXPECT_TRUE(moduleSet.Contains(evt.mModule)); + EXPECT_FALSE(evt.mIsDependent); + } + + // No check for the mXULLoadDurationMS field because the field has a value + // in CCov build GTest, but it is empty in non-CCov build (bug 1681936). + EXPECT_EQ(aData.mNumEvents, aData.mEvents.length()); + EXPECT_GT(aData.mNumEvents, 0U); + if (aIsTruncatedData) { + EXPECT_EQ(aData.mStacks.GetModuleCount(), 0U); + EXPECT_LE(aData.mNumEvents, UntrustedModulesData::kMaxEvents); + } else if (numBlockedEvents == aData.mNumEvents) { + // If all loading events were blocked or aData is truncated, + // the stacks are empty. + EXPECT_EQ(aData.mStacks.GetModuleCount(), 0U); + } else { + EXPECT_GT(aData.mStacks.GetModuleCount(), 0U); + } + EXPECT_EQ(aData.mSanitizationFailures, 0U); + EXPECT_EQ(aData.mTrustTestFailures, 0U); +} + +BOOL CALLBACK UntrustedModulesFixture::InitialModuleLoadOnce(PINIT_ONCE, void*, + void**) { + for (int i = 0; i < kLoadCountBeforeDllServices; ++i) { + for (const auto& mod : kTestModules) { + LoadAndFree(mod); + } + } + + RefPtr<DllServices> dllSvc(DllServices::Get()); + dllSvc->StartUntrustedModulesProcessor(true); + + for (int i = 0; i < kLoadCountAfterDllServices; ++i) { + for (const auto& mod : kTestModules) { + LoadAndFree(mod); + } + } + + ModuleLoadCounter waitForTwo(kTestModules, {kInitLoadCount, kInitLoadCount}); + EXPECT_EQ(sInitLoadDataCollector.Collect(waitForTwo), NS_OK); + EXPECT_TRUE(waitForTwo.Remains(kTestModules, {0, 0})); + + for (const auto& event : GetInitLoadData()) { + ValidateUntrustedModules(event); + } + + // Data was removed when retrieved. No data is retrieved again. + UntrustedModulesCollector collector; + ModuleLoadCounter waitOnceForEach(kTestModules, {1, 1}); + EXPECT_EQ(collector.Collect(waitOnceForEach), NS_ERROR_ABORT); + EXPECT_TRUE(waitOnceForEach.Remains(kTestModules, {1, 1})); + + return TRUE; +} + +#define PROCESS_OBJ(TYPE, PID) \ + u"\"" TYPE u"\\." PID u"\":{" \ + u"\"processType\":\"" TYPE u"\",\"elapsed\":\\d+\\.\\d+," \ + u"\"sanitizationFailures\":0,\"trustTestFailures\":0," \ + u"\"events\":\\[{" \ + u"\"processUptimeMS\":\\d+,\"loadDurationMS\":\\d+\\.\\d+," \ + u"\"threadID\":\\d+,\"threadName\":\"Main Thread\"," \ + u"\"baseAddress\":\"0x[0-9a-f]+\",\"moduleIndex\":0," \ + u"\"isDependent\":false,\"loadStatus\":0}\\]," \ + u"\"combinedStacks\":{" \ + u"\"memoryMap\":\\[\\[\"\\w+\\.\\w+\",\"[0-9A-Z]+\"\\]" \ + u"(,\\[\"\\w+\\.\\w+\",\"[0-9A-Z]+\\\"\\])*\\]," \ + u"\"stacks\":\\[\\[\\[(-1|\\d+),\\d+\\]" \ + u"(,\\[(-1|\\d+),\\d+\\])*\\]\\]}}" + +TEST_F(UntrustedModulesFixture, Serialize) { + // clang-format off + const char16_t kPattern[] = u"{\"structVersion\":1," + u"\"modules\":\\[{" + u"\"resolvedDllName\":\"TestUntrustedModules_Dll1\\.dll\"," + u"\"fileVersion\":\"1\\.2\\.3\\.4\"," + u"\"companyName\":\"Mozilla Corporation\",\"trustFlags\":0}\\]," + u"\"blockedModules\":\\[.*?\\]," // allow for the case where there are some blocked modules + u"\"processes\":{" + PROCESS_OBJ(u"browser", u"0xabc") u"," + PROCESS_OBJ(u"browser", u"0x4") u"," + PROCESS_OBJ(u"rdd", u"0x4") + u"}}"; + // clang-format on + + UntrustedModulesBackupData backup1, backup2; + { + UntrustedModulesData data1 = CollectSingleData(); + UntrustedModulesData data2 = CollectSingleData(); + UntrustedModulesData data3 = CollectSingleData(); + + data1.mPid = 0xabc; + data2.mPid = 0x4; + data2.mProcessType = GeckoProcessType_RDD; + data3.mPid = 0x4; + + backup1.Add(std::move(data1)); + backup2.Add(std::move(data2)); + backup1.Add(std::move(data3)); + } + + ValidateJSValue(kPattern, ArrayLength(kPattern) - 1, + [&backup1, &backup2]( + Telemetry::UntrustedModulesDataSerializer& aSerializer) { + EXPECT_NS_SUCCEEDED(aSerializer.Add(backup1)); + EXPECT_NS_SUCCEEDED(aSerializer.Add(backup2)); + }); +} + +TEST_F(UntrustedModulesFixture, Backup) { + RefPtr<UntrustedModulesBackupService> backupSvc( + UntrustedModulesBackupService::Get()); + for (int i = 0; i < 100; ++i) { + backupSvc->Backup(CollectSingleData()); + } + + backupSvc->SettleAllStagingData(); + EXPECT_TRUE(backupSvc->Staging().IsEmpty()); + + for (const auto& entry : backupSvc->Settled()) { + const RefPtr<UntrustedModulesDataContainer>& container = entry.GetData(); + EXPECT_TRUE(!!container); + const UntrustedModulesData& data = container->mData; + EXPECT_EQ(entry.GetKey(), ProcessHashKey(data.mProcessType, data.mPid)); + ValidateUntrustedModules(data, /*aIsTruncatedData*/ true); + } +} diff --git a/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll1/TestUntrustedModules_Dll1.cpp b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll1/TestUntrustedModules_Dll1.cpp new file mode 100644 index 0000000000..4f6ce877eb --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll1/TestUntrustedModules_Dll1.cpp @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> + +BOOL WINAPI DllMain(HINSTANCE, DWORD, LPVOID) { return TRUE; } diff --git a/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll1/TestUntrustedModules_Dll1.rc b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll1/TestUntrustedModules_Dll1.rc new file mode 100644 index 0000000000..2358b88b93 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll1/TestUntrustedModules_Dll1.rc @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <winver.h>
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION 1,2,3,4 // This field will be collected
+ PRODUCTVERSION 5,6,7,8
+ FILEFLAGSMASK 0x3fL
+#ifdef _DEBUG
+ FILEFLAGS 0x1L
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS__WINDOWS32
+ FILETYPE VFT_DLL
+ FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904e4"
+ BEGIN
+ VALUE "CompanyName", "Mozilla Corporation"
+ VALUE "OriginalFilename", "TestUntrustedModules_Dll1.dll"
+ VALUE "ProductName", "Test DLL"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x0409, 1252
+ END
+END
diff --git a/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll1/moz.build b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll1/moz.build new file mode 100644 index 0000000000..57fc59ca8a --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll1/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIST_INSTALL = False + +SharedLibrary("TestUntrustedModules_Dll1") + +UNIFIED_SOURCES = [ + "TestUntrustedModules_Dll1.cpp", +] + +RCFILE = "TestUntrustedModules_Dll1.rc" + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestUntrustedModules_Dll1.dll"] diff --git a/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll2/TestUntrustedModules_Dll2.cpp b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll2/TestUntrustedModules_Dll2.cpp new file mode 100644 index 0000000000..4f6ce877eb --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll2/TestUntrustedModules_Dll2.cpp @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> + +BOOL WINAPI DllMain(HINSTANCE, DWORD, LPVOID) { return TRUE; } diff --git a/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll2/moz.build b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll2/moz.build new file mode 100644 index 0000000000..fcefe41329 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/TestUntrustedModules_Dll2/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIST_INSTALL = False + +SharedLibrary("TestUntrustedModules_Dll2") + +UNIFIED_SOURCES = [ + "TestUntrustedModules_Dll2.cpp", +] + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestUntrustedModules_Dll2.dll"] diff --git a/toolkit/xre/dllservices/tests/gtest/moz.build b/toolkit/xre/dllservices/tests/gtest/moz.build new file mode 100644 index 0000000000..525eba0c38 --- /dev/null +++ b/toolkit/xre/dllservices/tests/gtest/moz.build @@ -0,0 +1,35 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Library("dllservicestest") + +UNIFIED_SOURCES += [ + "TestDLLBlocklist.cpp", +] + +if CONFIG["CPU_ARCH"] != "x86": + UNIFIED_SOURCES += [ + "TestUntrustedModules.cpp", + ] + +LOCAL_INCLUDES += [ + "/toolkit/components/telemetry/other", + "/toolkit/components/telemetry/tests/gtest", +] + +TEST_DIRS += [ + "TestDllBlocklist_AllowByVersion", + "TestDllBlocklist_MatchByName", + "TestDllBlocklist_MatchByVersion", + "TestDllBlocklist_NoOpEntryPoint", + "TestDllBlocklist_SocketProcessOnly", + "TestDllBlocklist_UserBlocked", + "TestDllBlocklist_UtilityProcessOnly", + "TestUntrustedModules_Dll1", + "TestUntrustedModules_Dll2", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" diff --git a/toolkit/xre/dllservices/tests/moz.build b/toolkit/xre/dllservices/tests/moz.build new file mode 100644 index 0000000000..eee1b22ae3 --- /dev/null +++ b/toolkit/xre/dllservices/tests/moz.build @@ -0,0 +1,46 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +GeckoCppUnitTests( + [ + "TestDllInterceptor", + "TestIATPatcher", + "TestMMPolicy", + ], + linkage=None, +) + +if CONFIG["CPU_ARCH"] in ("x86", "x86_64"): + # Cross-process interceptors not yet supported on aarch64 + GeckoCppUnitTests( + [ + "TestDllInterceptorCrossProcess", + ], + linkage=None, + ) + +OS_LIBS += [ + "advapi32", + "ntdll", + "ole32", + "shlwapi", + "user32", + "uuid", +] + +DELAYLOAD_DLLS += [ + "shlwapi.dll", +] + +if CONFIG["CC_TYPE"] in ("gcc", "clang"): + # This allows us to use wmain as the entry point on mingw + LDFLAGS += [ + "-municode", + ] + +TEST_DIRS += [ + "gtest", +] |