Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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__)
WINBASEAPI
PVOID WINAPI VirtualAlloc2(HANDLE Process, PVOID BaseAddress, SIZE_T Size,
ULONG AllocationType, ULONG PageProtection,
MEM_EXTENDED_PARAMETER* ExtendedParameters,
ULONG ParameterCount);
WINBASEAPI
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