Bug 1532470: Part 1 - Add ability to specify desired memory range when reserving memory; r=handyman

In order to support 4-byte patches on ARM64, we need to be able to reserve
trampoline space within +/- 128 MB of the beginning of a function.

These changes allow us to make such reservations using OS APIs when
available.

Differential Revision: https://phabricator.services.mozilla.com/D32190

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Aaron Klotz 2019-06-12 17:27:16 +00:00
Родитель 1e222b2b52
Коммит 2c8e5e76b3
1 изменённых файлов: 301 добавлений и 62 удалений

Просмотреть файл

@ -8,12 +8,66 @@
#define mozilla_interceptor_MMPolicies_h
#include "mozilla/Assertions.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 <windows.h>
// MinGW does not have these definitions yet
#if defined(__MINGW32__)
typedef struct _MEM_ADDRESS_REQUIREMENTS {
PVOID LowestStartingAddress;
PVOID HighestEndingAddress;
SIZE_T Alignment;
} MEM_ADDRESS_REQUIREMENTS, *PMEM_ADDRESS_REQUIREMENTS;
typedef enum MEM_EXTENDED_PARAMETER_TYPE {
MemExtendedParameterInvalidType = 0,
MemExtendedParameterAddressRequirements,
MemExtendedParameterNumaNode,
MemExtendedParameterPartitionHandle,
MemExtendedParameterUserPhysicalHandle,
MemExtendedParameterAttributeFlags,
MemExtendedParameterMax
} MEM_EXTENDED_PARAMETER_TYPE, *PMEM_EXTENDED_PARAMETER_TYPE;
#define MEM_EXTENDED_PARAMETER_TYPE_BITS 8
typedef struct DECLSPEC_ALIGN(8) MEM_EXTENDED_PARAMETER {
struct {
DWORD64 Type : MEM_EXTENDED_PARAMETER_TYPE_BITS;
DWORD64 Reserved : 64 - MEM_EXTENDED_PARAMETER_TYPE_BITS;
} DUMMYSTRUCTNAME;
union {
DWORD64 ULong64;
PVOID Pointer;
SIZE_T Size;
HANDLE Handle;
DWORD ULong;
} DUMMYUNIONNAME;
} MEM_EXTENDED_PARAMETER, *PMEM_EXTENDED_PARAMETER;
#endif // defined(__MINGW32__)
#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);
@ -22,14 +76,22 @@ extern "C" errno_t rand_s(unsigned int* randomValue);
namespace mozilla {
namespace interceptor {
enum class ReservationFlags : uint32_t {
eDefault = 0,
eForceFirst2GB = 1,
};
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);
}
MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(ReservationFlags)
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));
}
class MMPolicyBase {
public:
static DWORD ComputeAllocationSize(const uint32_t aRequestedSize) {
MOZ_ASSERT(aRequestedSize);
@ -65,45 +127,149 @@ class MMPolicyBase {
return kPageSize;
}
#if defined(_M_X64)
static uintptr_t GetMaxUserModeAddress() {
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.
*/
static Maybe<Span<const uint8_t>> SpanFromPivotAndDistance(
const uint32_t aSize, const uintptr_t aPivotAddr,
const uint32_t aMaxDistanceFromPivot) {
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(MakeSpan(reinterpret_cast<const uint8_t*>(lowerBound.value()),
upperBound.value() - lowerBound.value()));
}
/**
* This function locates a virtual memory region of |aDesiredBytesLen| that
* resides in the lowest 2GB of address space. We do this by scanning the
* 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. 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 the
* found region must be 0.
* large.
*/
static PVOID FindLowRegion(HANDLE aProcess, const size_t aDesiredBytesLen) {
const DWORD granularity = GetAllocGranularity();
MOZ_ASSERT(aDesiredBytesLen / granularity > 0);
static PVOID FindRegion(HANDLE aProcess, const size_t aDesiredBytesLen,
const uint8_t* aRangeMin, const uint8_t* aRangeMax) {
const DWORD kGranularity = GetAllocGranularity();
MOZ_ASSERT(aDesiredBytesLen >= kGranularity);
if (!aDesiredBytesLen) {
return nullptr;
}
MOZ_ASSERT(aRangeMin < aRangeMax);
if (aRangeMin >= aRangeMax) {
return nullptr;
}
// Generate a randomized base address that falls within the interval
// [1MiB, 2GiB - aDesiredBytesLen]
// [aRangeMin, aRangeMax - aDesiredBytesLen]
unsigned int rnd = 0;
rand_s(&rnd);
// Reduce rnd to a value that falls within the acceptable range
const uint64_t kMinAddress = 0x0000000000100000ULL;
const uint64_t kMaxAddress = 0x0000000080000000ULL;
uint64_t maxOffset =
(kMaxAddress - kMinAddress - aDesiredBytesLen) / granularity;
uint64_t offset = (uint64_t(rnd) % maxOffset) * granularity;
uintptr_t maxOffset =
(aRangeMax - aRangeMin - aDesiredBytesLen) / kGranularity;
uintptr_t offset = (uintptr_t(rnd) % maxOffset) * kGranularity;
// Start searching at this address
char* address = reinterpret_cast<char*>(kMinAddress) + offset;
const uint8_t* address = aRangeMin + offset;
// The max address needs to incorporate the desired length
char* const kMaxPtr =
reinterpret_cast<char*>(kMaxAddress) - aDesiredBytesLen;
const uint8_t* const kMaxPtr = aRangeMax - aDesiredBytesLen;
MOZ_DIAGNOSTIC_ASSERT(address <= kMaxPtr);
MEMORY_BASIC_INFORMATION mbi = {};
MEMORY_BASIC_INFORMATION mbi;
SIZE_T len = sizeof(mbi);
// Scan the range for a free chunk that is at least as large as
@ -114,55 +280,83 @@ class MMPolicyBase {
return mbi.BaseAddress;
}
address = reinterpret_cast<char*>(mbi.BaseAddress) + mbi.RegionSize;
address =
reinterpret_cast<const uint8_t*>(mbi.BaseAddress) + mbi.RegionSize;
}
return nullptr;
}
#endif // defined(_M_X64)
template <typename ReserveFnT>
/**
* 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>
static PVOID Reserve(HANDLE aProcess, const uint32_t aSize,
const ReserveFnT& aReserveFn,
const ReservationFlags aFlags) {
#if defined(_M_X64)
if (aFlags & ReservationFlags::eForceFirst2GB) {
size_t curAttempt = 0;
const size_t kMaxAttempts = 8;
const ReserveRangeFnT& aReserveRangeFn,
const Maybe<Span<const uint8_t>>& aBounds) {
if (!aBounds) {
// No restrictions, let the OS choose the base address
return aReserveFn(aProcess, nullptr, aSize);
}
// We loop here because |FindLowRegion| may return a base address that
// is reserved elsewhere before we have had a chance to reserve it
// ourselves.
while (curAttempt < kMaxAttempts) {
PVOID base = FindLowRegion(aProcess, aSize);
if (!base) {
return nullptr;
}
const uint8_t* lowerBound = GetLowerBound(aBounds.ref());
const uint8_t* upperBoundExcl = GetUpperBoundExcl(aBounds.ref());
PVOID result = aReserveFn(aProcess, base, aSize);
if (result) {
return result;
}
Maybe<PVOID> result =
aReserveRangeFn(aProcess, aSize, lowerBound, upperBoundExcl);
if (result) {
return result.value();
}
++curAttempt;
// 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;
}
// 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.
result = Some(aReserveFn(aProcess, base, aSize));
if (result.value()) {
return result.value();
}
++curAttempt;
}
#endif // defined(_M_X64)
// 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.
return aReserveFn(aProcess, nullptr, aSize);
}
};
class MMPolicyInProcess : public MMPolicyBase {
class MOZ_TRIVIAL_CTOR_DTOR MMPolicyInProcess : public MMPolicyBase {
public:
typedef MMPolicyInProcess MMPolicyT;
explicit MMPolicyInProcess()
constexpr MMPolicyInProcess()
: mBase(nullptr), mReservationSize(0), mCommitOffset(0) {}
MMPolicyInProcess(const MMPolicyInProcess&) = delete;
@ -186,9 +380,6 @@ class MMPolicyInProcess : public MMPolicyBase {
return *this;
}
// We always leak mBase
~MMPolicyInProcess() = default;
explicit operator bool() const { return !!mBase; }
/**
@ -262,7 +453,8 @@ class MMPolicyInProcess : public MMPolicyBase {
/**
* @return the effective number of bytes reserved, or 0 on failure
*/
uint32_t Reserve(const uint32_t aSize, const ReservationFlags aFlags) {
uint32_t Reserve(const uint32_t aSize,
const Maybe<Span<const uint8_t>>& aBounds) {
if (!aSize) {
return 0;
}
@ -278,8 +470,31 @@ class MMPolicyInProcess : public MMPolicyBase {
return ::VirtualAlloc(aBase, aSize, MEM_RESERVE, PAGE_NOACCESS);
};
mBase = static_cast<uint8_t*>(MMPolicyBase::Reserve(
::GetCurrentProcess(), mReservationSize, reserveFn, aFlags));
auto reserveWithinRangeFn =
[](HANDLE aProcess, uint32_t aSize, const uint8_t* aRangeMin,
const uint8_t* aRangeMaxExcl) -> Maybe<PVOID> {
static const DynamicallyLinkedFunctionPtr<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;
@ -471,7 +686,8 @@ class MMPolicyOutOfProcess : public MMPolicyBase {
/**
* @return the effective number of bytes reserved, or 0 on failure
*/
uint32_t Reserve(const uint32_t aSize, const ReservationFlags aFlags) {
uint32_t Reserve(const uint32_t aSize,
const Maybe<Span<const uint8_t>>& aBounds) {
if (!aSize || !mProcess) {
return 0;
}
@ -502,8 +718,31 @@ class MMPolicyOutOfProcess : public MMPolicyBase {
PAGE_EXECUTE_READ);
};
mRemoteView =
MMPolicyBase::Reserve(mProcess, mReservationSize, reserveFn, aFlags);
auto reserveWithinRangeFn =
[mapping = mMapping](HANDLE aProcess, uint32_t aSize,
const uint8_t* aRangeMin,
const uint8_t* aRangeMaxExcl) -> Maybe<PVOID> {
static const DynamicallyLinkedFunctionPtr<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;
}