2171 lines
77 KiB
C++
2171 lines
77 KiB
C++
/* 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/. */
|
|
|
|
// PHC is a probabilistic heap checker. A tiny fraction of randomly chosen heap
|
|
// allocations are subject to some expensive checking via the use of OS page
|
|
// access protection. A failed check triggers a crash, whereupon useful
|
|
// information about the failure is put into the crash report. The cost and
|
|
// coverage for each user is minimal, but spread over the entire user base the
|
|
// coverage becomes significant.
|
|
//
|
|
// The idea comes from Chromium, where it is called GWP-ASAN. (Firefox uses PHC
|
|
// as the name because GWP-ASAN is long, awkward, and doesn't have any
|
|
// particular meaning.)
|
|
//
|
|
// In the current implementation up to 64 allocations per process can become
|
|
// PHC allocations. These allocations must be page-sized or smaller. Each PHC
|
|
// allocation gets its own page, and when the allocation is freed its page is
|
|
// marked inaccessible until the page is reused for another allocation. This
|
|
// means that a use-after-free defect (which includes double-frees) will be
|
|
// caught if the use occurs before the page is reused for another allocation.
|
|
// The crash report will contain stack traces for the allocation site, the free
|
|
// site, and the use-after-free site, which is often enough to diagnose the
|
|
// defect.
|
|
//
|
|
// Also, each PHC allocation is followed by a guard page. The PHC allocation is
|
|
// positioned so that its end abuts the guard page (or as close as possible,
|
|
// given alignment constraints). This means that a bounds violation at the end
|
|
// of the allocation (overflow) will be caught. The crash report will contain
|
|
// stack traces for the allocation site and the bounds violation use site,
|
|
// which is often enough to diagnose the defect.
|
|
//
|
|
// (A bounds violation at the start of the allocation (underflow) will not be
|
|
// caught, unless it is sufficiently large to hit the preceding allocation's
|
|
// guard page, which is not that likely. It would be possible to look more
|
|
// assiduously for underflow by randomly placing some allocations at the end of
|
|
// the page and some at the start of the page, and GWP-ASAN does this. PHC does
|
|
// not, however, because overflow is likely to be much more common than
|
|
// underflow in practice.)
|
|
//
|
|
// We use a simple heuristic to categorize a guard page access as overflow or
|
|
// underflow: if the address falls in the lower half of the guard page, we
|
|
// assume it is overflow, otherwise we assume it is underflow. More
|
|
// sophisticated heuristics are possible, but this one is very simple, and it is
|
|
// likely that most overflows/underflows in practice are very close to the page
|
|
// boundary.
|
|
//
|
|
// The design space for the randomization strategy is large. The current
|
|
// implementation has a large random delay before it starts operating, and a
|
|
// small random delay between each PHC allocation attempt. Each freed PHC
|
|
// allocation is quarantined for a medium random delay before being reused, in
|
|
// order to increase the chance of catching UAFs.
|
|
//
|
|
// The basic cost of PHC's operation is as follows.
|
|
//
|
|
// - The physical memory cost is 64 pages plus some metadata (including stack
|
|
// traces) for each page. This amounts to 256 KiB per process on
|
|
// architectures with 4 KiB pages and 1024 KiB on macOS/AArch64 which uses
|
|
// 16 KiB pages.
|
|
//
|
|
// - The virtual memory cost is the physical memory cost plus the guard pages:
|
|
// another 64 pages. This amounts to another 256 KiB per process on
|
|
// architectures with 4 KiB pages and 1024 KiB on macOS/AArch64 which uses
|
|
// 16 KiB pages. PHC is currently only enabled on 64-bit platforms so the
|
|
// impact of the virtual memory usage is negligible.
|
|
//
|
|
// - Every allocation requires a size check and a decrement-and-check of an
|
|
// atomic counter. When the counter reaches zero a PHC allocation can occur,
|
|
// which involves marking a page as accessible and getting a stack trace for
|
|
// the allocation site. Otherwise, mozjemalloc performs the allocation.
|
|
//
|
|
// - Every deallocation requires a range check on the pointer to see if it
|
|
// involves a PHC allocation. (The choice to only do PHC allocations that are
|
|
// a page or smaller enables this range check, because the 64 pages are
|
|
// contiguous. Allowing larger allocations would make this more complicated,
|
|
// and we definitely don't want something as slow as a hash table lookup on
|
|
// every deallocation.) PHC deallocations involve marking a page as
|
|
// inaccessible and getting a stack trace for the deallocation site.
|
|
//
|
|
// Note that calls to realloc(), free(), and malloc_usable_size() will
|
|
// immediately crash if the given pointer falls within a page allocation's
|
|
// page, but does not point to the start of the allocation itself.
|
|
//
|
|
// void* p = malloc(64);
|
|
// free(p + 1); // p+1 doesn't point to the allocation start; crash
|
|
//
|
|
// Such crashes will not have the PHC fields in the crash report.
|
|
//
|
|
// PHC-specific tests can be run with the following commands:
|
|
// - gtests: `./mach gtest '*PHC*'`
|
|
// - xpcshell-tests: `./mach test toolkit/crashreporter/test/unit`
|
|
// - This runs some non-PHC tests as well.
|
|
|
|
#include "PHC.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <time.h>
|
|
|
|
#include <algorithm>
|
|
#include <bit>
|
|
|
|
#ifdef XP_WIN
|
|
# include <process.h>
|
|
#else
|
|
# include <sys/mman.h>
|
|
# include <sys/types.h>
|
|
# include <pthread.h>
|
|
# include <unistd.h>
|
|
#endif
|
|
|
|
#include "mozjemalloc.h"
|
|
#include "BaseArray.h"
|
|
#include "BaseAlloc.h"
|
|
#include "Chunk.h"
|
|
#include "FdPrintf.h"
|
|
#include "Mutex.h"
|
|
#include "mozilla/Assertions.h"
|
|
#include "mozilla/Atomics.h"
|
|
#include "mozilla/Attributes.h"
|
|
#include "mozilla/CheckedInt.h"
|
|
#include "mozilla/Maybe.h"
|
|
#include "mozilla/StackWalk.h"
|
|
#include "mozilla/ThreadLocal.h"
|
|
#include "mozilla/XorShift128PlusRNG.h"
|
|
|
|
using namespace mozilla;
|
|
|
|
//---------------------------------------------------------------------------
|
|
// Utilities
|
|
//---------------------------------------------------------------------------
|
|
|
|
#ifdef ANDROID
|
|
// Android doesn't have pthread_atfork defined in pthread.h.
|
|
extern "C" MOZ_EXPORT int pthread_atfork(void (*)(void), void (*)(void),
|
|
void (*)(void));
|
|
#endif
|
|
|
|
#ifndef DISALLOW_COPY_AND_ASSIGN
|
|
# define DISALLOW_COPY_AND_ASSIGN(T) \
|
|
T(const T&); \
|
|
void operator=(const T&)
|
|
#endif
|
|
|
|
//---------------------------------------------------------------------------
|
|
// Stack traces
|
|
//---------------------------------------------------------------------------
|
|
|
|
// This code is similar to the equivalent code within DMD.
|
|
|
|
class StackTrace : public phc::StackTrace {
|
|
public:
|
|
StackTrace() = default;
|
|
|
|
void Clear() { mLength = 0; }
|
|
|
|
void Fill();
|
|
|
|
private:
|
|
static void StackWalkCallback(uint32_t aFrameNumber, void* aPc, void* aSp,
|
|
void* aClosure) {
|
|
StackTrace* st = (StackTrace*)aClosure;
|
|
MOZ_ASSERT(st->mLength < kMaxFrames);
|
|
st->mPcs[st->mLength] = aPc;
|
|
st->mLength++;
|
|
MOZ_ASSERT(st->mLength == aFrameNumber);
|
|
}
|
|
};
|
|
|
|
// WARNING WARNING WARNING: this function must only be called when PHC::mMutex
|
|
// is *not* locked, otherwise we might get deadlocks.
|
|
//
|
|
// How? On Windows, MozStackWalk() can lock a mutex, M, from the shared library
|
|
// loader. Another thread might call malloc() while holding M locked (when
|
|
// loading a shared library) and try to lock PHC::mMutex, causing a deadlock.
|
|
// So PHC::mMutex can't be locked during the call to MozStackWalk(). (For
|
|
// details, see https://bugzilla.mozilla.org/show_bug.cgi?id=374829#c8. On
|
|
// Linux, something similar can happen; see bug 824340. So we just disallow it
|
|
// on all platforms.)
|
|
//
|
|
// In DMD, to avoid this problem we temporarily unlock the equivalent mutex for
|
|
// the MozStackWalk() call. But that's grotty, and things are a bit different
|
|
// here, so we just require that stack traces be obtained before locking
|
|
// PHC::mMutex.
|
|
//
|
|
// Unfortunately, there is no reliable way at compile-time or run-time to ensure
|
|
// this pre-condition. Hence this large comment.
|
|
//
|
|
void StackTrace::Fill() {
|
|
mLength = 0;
|
|
|
|
// These ifdefs should be kept in sync with the conditions in
|
|
// phc_implies_frame_pointers in build/moz.configure/memory.configure
|
|
#if defined(XP_WIN) && defined(_M_IX86)
|
|
// This avoids MozStackWalk(), which causes unusably slow startup on Win32
|
|
// when it is called during static initialization (see bug 1241684).
|
|
//
|
|
// This code is cribbed from the Gecko Profiler, which also uses
|
|
// FramePointerStackWalk() on Win32: Registers::SyncPopulate() for the
|
|
// frame pointer, and GetStackTop() for the stack end.
|
|
CONTEXT context;
|
|
RtlCaptureContext(&context);
|
|
void** fp = reinterpret_cast<void**>(context.Ebp);
|
|
|
|
PNT_TIB pTib = reinterpret_cast<PNT_TIB>(NtCurrentTeb());
|
|
void* stackEnd = static_cast<void*>(pTib->StackBase);
|
|
FramePointerStackWalk(StackWalkCallback, kMaxFrames, this, fp, stackEnd);
|
|
#elif defined(XP_DARWIN)
|
|
// This avoids MozStackWalk(), which has become unusably slow on Mac due to
|
|
// changes in libunwind.
|
|
//
|
|
// This code is cribbed from the Gecko Profiler, which also uses
|
|
// FramePointerStackWalk() on Mac: Registers::SyncPopulate() for the frame
|
|
// pointer, and GetStackTop() for the stack end.
|
|
# pragma GCC diagnostic push
|
|
# pragma GCC diagnostic ignored "-Wframe-address"
|
|
void** fp = reinterpret_cast<void**>(__builtin_frame_address(1));
|
|
# pragma GCC diagnostic pop
|
|
void* stackEnd = pthread_get_stackaddr_np(pthread_self());
|
|
FramePointerStackWalk(StackWalkCallback, kMaxFrames, this, fp, stackEnd);
|
|
#else
|
|
MozStackWalk(StackWalkCallback, nullptr, kMaxFrames, this);
|
|
#endif
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
// Logging
|
|
//---------------------------------------------------------------------------
|
|
|
|
// Change this to 1 to enable some PHC logging. Useful for debugging.
|
|
#define PHC_LOGGING 0
|
|
|
|
static void Log(const char* fmt, ...);
|
|
|
|
//---------------------------------------------------------------------------
|
|
// Global state
|
|
//---------------------------------------------------------------------------
|
|
|
|
// Throughout this entire file time is measured as the number of sub-page
|
|
// allocations performed (by PHC and mozjemalloc combined). `Time` is 64-bit
|
|
// because we could have more than 2**32 allocations in a long-running session.
|
|
// `Delay` is 32-bit because the delays used within PHC are always much smaller
|
|
// than 2**32. Delay must be unsigned so that std::has_single_bit() can work on
|
|
// some Delay values.
|
|
using Time = uint64_t; // A moment in time.
|
|
using Delay = uint32_t; // A time duration.
|
|
static constexpr Delay DELAY_MAX = UINT32_MAX / 2;
|
|
|
|
// PHC only runs if the page size is 4 KiB; anything more is uncommon and would
|
|
// use too much memory. So we hardwire this size for all platforms but macOS
|
|
// on ARM processors. For the latter we make an exception because the minimum
|
|
// page size supported is 16KiB so there's no way to go below that.
|
|
static const size_t kPhcPageSize =
|
|
#if defined(XP_DARWIN) && defined(__aarch64__)
|
|
16384
|
|
#else
|
|
4096
|
|
#endif
|
|
;
|
|
|
|
// We align the PHC area to a multiple of the jemalloc and JS GC chunk size
|
|
// (both use 1MB aligned chunks) so that their address computations don't lead
|
|
// from non-PHC memory into PHC memory causing misleading PHC stacks to be
|
|
// attached to a crash report.
|
|
static const size_t kPhcAlign = 1024 * 1024;
|
|
|
|
static_assert(std::has_single_bit(kPhcAlign));
|
|
static_assert((kPhcAlign % kPhcPageSize) == 0);
|
|
|
|
// PHC will reserve some address space this large, then depending on runtime
|
|
// configuration will use a smaller fraction of it. Making
|
|
// kPhcVirtualReservation the upper-bound of PHC's memory size.
|
|
// * On 32bit systems with less available address space we choose a more
|
|
// moderate value.
|
|
// * On 64bit systems we set the limit to so that there are no more than 32,768
|
|
// mappings, half of Linux's default limit (Bug 1969856). For 4KB pages
|
|
// that's 128MB.
|
|
#ifdef HAVE_64BIT_BUILD
|
|
# if defined(XP_DARWIN) && defined(__aarch64__)
|
|
static const size_t kPhcVirtualReservation = 512 * 1024 * 1024;
|
|
# else
|
|
static const size_t kPhcVirtualReservation = 128 * 1024 * 1024;
|
|
# endif
|
|
#else
|
|
static const size_t kPhcVirtualReservation = 2 * 1024 * 1024;
|
|
#endif
|
|
|
|
// The amount to decrement from the shared allocation delay each time a thread's
|
|
// local allocation delay reaches zero.
|
|
static const Delay kDelayDecrementAmount = 256;
|
|
|
|
// When PHC is disabled on the current thread wait this many allocations before
|
|
// accessing sAllocDelay once more.
|
|
static const Delay kDelayBackoffAmount = 64;
|
|
|
|
// When PHC is disabled globally reset the shared delay by this many allocations
|
|
// to keep code running on the fast path.
|
|
static const Delay kDelayResetWhenDisabled = 64 * 1024;
|
|
|
|
// The default state for PHC. Either Enabled or OnlyFree.
|
|
#define DEFAULT_STATE mozilla::phc::OnlyFree
|
|
|
|
// The maximum time.
|
|
static const Time kMaxTime = ~(Time(0));
|
|
|
|
// Truncate aRnd to the range (1 .. aAvgDelay*2). If aRnd is random, this
|
|
// results in an average value of aAvgDelay + 0.5, which is close enough to
|
|
// aAvgDelay. aAvgDelay must be a power-of-two for speed.
|
|
constexpr Delay Rnd64ToDelay(Delay aAvgDelay, uint64_t aRnd) {
|
|
MOZ_ASSERT(std::has_single_bit(aAvgDelay), "must be a power of two");
|
|
|
|
return (aRnd & (uint64_t(aAvgDelay) * 2 - 1)) + 1;
|
|
}
|
|
|
|
static Delay CheckProbability(int64_t aProb) {
|
|
// Limit delays calculated from prefs to 0x80000000, this is the largest
|
|
// power-of-two that fits in a Delay since it is a uint32_t.
|
|
// The minimum is 2 that way not every allocation goes straight to PHC.
|
|
return RoundUpPow2(std::clamp(aProb, int64_t(2), int64_t(0x80000000)));
|
|
}
|
|
|
|
// On MacOS, the first __thread/thread_local access calls malloc, which leads
|
|
// to an infinite loop. So we use pthread-based TLS instead, which somehow
|
|
// doesn't have this problem.
|
|
#if !defined(XP_DARWIN)
|
|
# define PHC_THREAD_LOCAL(T) MOZ_THREAD_LOCAL(T)
|
|
#else
|
|
# define PHC_THREAD_LOCAL(T) \
|
|
detail::ThreadLocal<T, detail::ThreadLocalKeyStorage>
|
|
#endif
|
|
|
|
enum class AllocPageState {
|
|
NeverAllocated = 0,
|
|
InUse = 1,
|
|
Freed = 2,
|
|
};
|
|
|
|
// Metadata for each allocation page.
|
|
class AllocPageInfo {
|
|
public:
|
|
AllocPageInfo()
|
|
: mState(AllocPageState::NeverAllocated),
|
|
mBaseAddr(nullptr),
|
|
mReuseTime(0) {}
|
|
|
|
// The current allocation page state.
|
|
AllocPageState mState;
|
|
|
|
// The arena that the allocation is nominally from. This isn't meaningful
|
|
// within PHC, which has no arenas. But it is necessary for reallocation of
|
|
// page allocations as normal allocations, such as in this code:
|
|
//
|
|
// p = moz_arena_malloc(arenaId, 4096);
|
|
// realloc(p, 8192);
|
|
//
|
|
// The realloc is more than one page, and thus too large for PHC to handle.
|
|
// Therefore, if PHC handles the first allocation, it must ask mozjemalloc
|
|
// to allocate the 8192 bytes in the correct arena, and to do that, it must
|
|
// call MozJemalloc::moz_arena_malloc with the correct arenaId under the
|
|
// covers. Therefore it must record that arenaId.
|
|
//
|
|
// This field is also needed for jemalloc_ptr_info() to work, because it
|
|
// also returns the arena ID (but only in debug builds).
|
|
//
|
|
// - NeverAllocated: must be 0.
|
|
// - InUse | Freed: can be any valid arena ID value.
|
|
Maybe<arena_id_t> mArenaId;
|
|
|
|
// The starting address of the allocation. Will not be the same as the page
|
|
// address unless the allocation is a full page.
|
|
// - NeverAllocated: must be 0.
|
|
// - InUse | Freed: must be within the allocation page.
|
|
uint8_t* mBaseAddr;
|
|
|
|
// Usable size is computed as the number of bytes between the pointer and
|
|
// the end of the allocation page. This might be bigger than the requested
|
|
// size, especially if an outsized alignment is requested.
|
|
size_t UsableSize() const {
|
|
return mState == AllocPageState::NeverAllocated
|
|
? 0
|
|
: kPhcPageSize - (reinterpret_cast<uintptr_t>(mBaseAddr) &
|
|
(kPhcPageSize - 1));
|
|
}
|
|
|
|
// The internal fragmentation for this allocation.
|
|
size_t FragmentationBytes() const {
|
|
MOZ_ASSERT(kPhcPageSize >= UsableSize());
|
|
return mState == AllocPageState::InUse ? kPhcPageSize - UsableSize() : 0;
|
|
}
|
|
|
|
// The allocation stack.
|
|
// - NeverAllocated: Nothing.
|
|
// - InUse | Freed: Some.
|
|
Maybe<StackTrace> mAllocStack;
|
|
|
|
// The free stack.
|
|
// - NeverAllocated | InUse: Nothing.
|
|
// - Freed: Some.
|
|
Maybe<StackTrace> mFreeStack;
|
|
|
|
// The time at which the page is available for reuse, as measured against
|
|
// mNow. When the page is in use this value will be kMaxTime.
|
|
// - NeverAllocated: must be 0.
|
|
// - InUse: must be kMaxTime.
|
|
// - Freed: must be > 0 and < kMaxTime.
|
|
Time mReuseTime;
|
|
|
|
#if PHC_LOGGING
|
|
Time mFreeTime;
|
|
#endif
|
|
|
|
// The next index for a free list of pages.`
|
|
Maybe<uintptr_t> mNextPage;
|
|
|
|
void AssertInUse() const {
|
|
MOZ_ASSERT(mState == AllocPageState::InUse);
|
|
// There is nothing to assert about aPage.mArenaId.
|
|
MOZ_ASSERT(mBaseAddr);
|
|
MOZ_ASSERT(UsableSize() > 0);
|
|
MOZ_ASSERT(mAllocStack.isSome());
|
|
MOZ_ASSERT(mFreeStack.isNothing());
|
|
MOZ_ASSERT(mReuseTime == kMaxTime);
|
|
MOZ_ASSERT(!mNextPage);
|
|
}
|
|
|
|
void AssertNotInUse() const {
|
|
// We can assert a lot about `NeverAllocated` pages, but not much about
|
|
// `Freed` pages.
|
|
#ifdef DEBUG
|
|
bool isFresh = mState == AllocPageState::NeverAllocated;
|
|
MOZ_ASSERT(isFresh || mState == AllocPageState::Freed);
|
|
MOZ_ASSERT_IF(isFresh, mArenaId == Nothing());
|
|
MOZ_ASSERT(isFresh == (mBaseAddr == nullptr));
|
|
MOZ_ASSERT(isFresh == (mAllocStack.isNothing()));
|
|
MOZ_ASSERT(isFresh == (mFreeStack.isNothing()));
|
|
MOZ_ASSERT(mReuseTime != kMaxTime);
|
|
#endif
|
|
}
|
|
|
|
bool IsPageInUse() const { return mState == AllocPageState::InUse; }
|
|
bool IsPageFreed() const { return mState == AllocPageState::Freed; }
|
|
|
|
bool IsPageAllocatable(Time aNow) const {
|
|
return !IsPageInUse() && aNow >= mReuseTime;
|
|
}
|
|
|
|
void SetInUse(const Maybe<arena_id_t>& aArenaId, uint8_t* aBaseAddr,
|
|
const StackTrace& aAllocStack) {
|
|
AssertNotInUse();
|
|
mState = AllocPageState::InUse;
|
|
mArenaId = aArenaId;
|
|
mBaseAddr = aBaseAddr;
|
|
mAllocStack = Some(aAllocStack);
|
|
mFreeStack = Nothing();
|
|
mReuseTime = kMaxTime;
|
|
|
|
MOZ_ASSERT(!mNextPage);
|
|
}
|
|
|
|
void ResizeInUse(const Maybe<arena_id_t>& aArenaId, uint8_t* aNewBaseAddr,
|
|
const StackTrace& aAllocStack) {
|
|
AssertInUse();
|
|
|
|
// page.mState is not changed.
|
|
if (aArenaId.isSome()) {
|
|
// Crash if the arenas don't match.
|
|
MOZ_RELEASE_ASSERT(mArenaId == aArenaId);
|
|
}
|
|
mBaseAddr = aNewBaseAddr;
|
|
// We could just keep the original alloc stack, but the realloc stack is
|
|
// more recent and therefore seems more useful.
|
|
mAllocStack = Some(aAllocStack);
|
|
// mFreeStack is not changed.
|
|
// mReuseTime is not changed.
|
|
// mNextPage is not changed.
|
|
}
|
|
|
|
void SetPageFreed(const Maybe<arena_id_t>& aArenaId,
|
|
const StackTrace& aFreeStack, Delay aReuseDelay,
|
|
Time aNow) {
|
|
AssertInUse();
|
|
|
|
mState = AllocPageState::Freed;
|
|
|
|
// page.mArenaId is left unchanged, for jemalloc_ptr_info() calls that
|
|
// occur after freeing (e.g. in the PtrInfo test in TestJemalloc.cpp).
|
|
if (aArenaId.isSome()) {
|
|
// Crash if the arenas don't match.
|
|
MOZ_RELEASE_ASSERT(mArenaId == aArenaId);
|
|
}
|
|
|
|
// page.musableSize is left unchanged, for reporting on UAF, and for
|
|
// jemalloc_ptr_info() calls that occur after freeing (e.g. in the PtrInfo
|
|
// test in TestJemalloc.cpp).
|
|
|
|
// page.mAllocStack is left unchanged, for reporting on UAF.
|
|
|
|
mFreeStack = Some(aFreeStack);
|
|
#if PHC_LOGGING
|
|
mFreeTime = aNow;
|
|
#endif
|
|
mReuseTime = aNow + aReuseDelay;
|
|
}
|
|
};
|
|
|
|
// The virtual address space reserved by PHC. It is shared, immutable global
|
|
// state. Initialized by phc_init() and never changed after that. phc_init()
|
|
// runs early enough that no synchronization is needed.
|
|
class PHCRegion {
|
|
private:
|
|
// The bounds of PHC's virtual address space. These are only ever set once
|
|
// before any threads are spawned, after that they're read only, and therefore
|
|
// can be accessed without a lock.
|
|
uint8_t* mPagesStart = nullptr;
|
|
uint8_t* mPagesLimit = nullptr;
|
|
|
|
public:
|
|
// Allocates the allocation pages and the guard pages, contiguously.
|
|
bool AllocVirtualAddresses() {
|
|
MOZ_ASSERT(!mPagesStart || !mPagesLimit);
|
|
|
|
// The memory allocated here is never freed, because it would happen at
|
|
// process termination when it would be of little use.
|
|
|
|
// On Windows in particular we want to control how the memory is initially
|
|
// reserved. Windows pages memory in immediately which creates performance
|
|
// problems and could affect stability.
|
|
void* pages =
|
|
pages_mmap_aligned(kPhcVirtualReservation, kPhcAlign, ReserveOnly);
|
|
if (!pages) {
|
|
return false;
|
|
}
|
|
|
|
mPagesStart = static_cast<uint8_t*>(pages);
|
|
mPagesLimit = mPagesStart + kPhcVirtualReservation;
|
|
Log("AllocVirtualAddresses at %p..%p\n", mPagesStart, mPagesLimit);
|
|
return true;
|
|
}
|
|
|
|
void ReleaseVirtualAddresses() {
|
|
MOZ_ASSERT(!!mPagesStart && !!mPagesLimit);
|
|
|
|
pages_unmap(mPagesStart, reinterpret_cast<uintptr_t>(mPagesLimit) -
|
|
reinterpret_cast<uintptr_t>(mPagesStart));
|
|
mPagesStart = nullptr;
|
|
mPagesLimit = nullptr;
|
|
}
|
|
|
|
constexpr PHCRegion() {}
|
|
|
|
bool IsInFirstGuardPage(const void* aPtr) {
|
|
MOZ_ASSERT(mPagesStart != nullptr && mPagesLimit != nullptr);
|
|
return mPagesStart <= aPtr && aPtr < mPagesStart + kPhcPageSize;
|
|
}
|
|
|
|
// Get the address of the allocation page referred to via an index. Used when
|
|
// marking the page as accessible/inaccessible.
|
|
uint8_t* AllocPagePtr(uintptr_t aIndex) {
|
|
MOZ_ASSERT(mPagesStart != nullptr && mPagesLimit != nullptr);
|
|
// Multiply by two and add one to account for allocation pages *and* guard
|
|
// pages.
|
|
return mPagesStart + (2 * aIndex + 1) * kPhcPageSize;
|
|
}
|
|
|
|
MOZ_ALWAYS_INLINE bool WithinBounds(const void* aPtr) const {
|
|
MOZ_ASSERT(mPagesStart && mPagesLimit);
|
|
return aPtr >= mPagesStart && aPtr < mPagesLimit;
|
|
}
|
|
|
|
const uint8_t* PagesStart() const { return mPagesStart; }
|
|
|
|
size_t ReservedBytes() const {
|
|
return mPagesStart ? kPhcVirtualReservation - kPhcPageSize : 0;
|
|
}
|
|
};
|
|
|
|
class PtrKind;
|
|
|
|
// Shared, mutable global state. Many fields are protected by sMutex; functions
|
|
// that access those feilds should take a PHCLock as proof that mMutex is held.
|
|
// Other fields are TLS or Atomic and don't need the lock.
|
|
class PHC : public BaseAllocClass {
|
|
public:
|
|
// The RNG seeds here are poor, but non-reentrant since this can be called
|
|
// from malloc(). SetState() will reset the RNG later.
|
|
PHC() : mRNG(RandomSeed<1>(), RandomSeed<2>()) {
|
|
mMutex.Init();
|
|
if (!tlsIsDisabled.init()) {
|
|
MOZ_CRASH();
|
|
}
|
|
if (!tlsAllocDelay.init()) {
|
|
MOZ_CRASH();
|
|
}
|
|
if (!tlsLastDelay.init()) {
|
|
MOZ_CRASH();
|
|
}
|
|
|
|
// This constructor is part of PHC's very early initialisation,
|
|
// see phc_init(), and if PHC is default-on it'll start marking allocations
|
|
// and we must setup the delay. However once XPCOM starts it'll call
|
|
// SetState() which will re-initialise the RNG and allocation delay.
|
|
|
|
#ifdef EARLY_BETA_OR_EARLIER
|
|
Resize(16 * 1024 * 1024);
|
|
#else
|
|
// Before Bug 1867191 PHC used no more than approximately 1.1MB when it was
|
|
// set to a round number of 256 pages. To keep the size the same we now
|
|
// specify this strange total size, but will follow-up with a more sensible
|
|
// maximum in the future.
|
|
Resize((1024 + 128) * 1024);
|
|
#endif
|
|
|
|
{
|
|
MutexAutoLock lock(mMutex);
|
|
ForceSetNewAllocDelay(Rnd64ToDelay(mAvgFirstAllocDelay, Random64()));
|
|
}
|
|
}
|
|
|
|
void Resize(size_t aSizeBytes) {
|
|
// -1 since the last page in the virtual address space must be a guard page.
|
|
size_t max_pages = (kPhcVirtualReservation / kPhcPageSize / 2) - 1;
|
|
size_t size_pages = aSizeBytes / kPhcPageSize;
|
|
size_pages = std::min(size_pages, max_pages);
|
|
|
|
MutexAutoLock lock(mMutex);
|
|
|
|
size_t old_size_pages = NumAllocPages();
|
|
if (size_pages > old_size_pages) {
|
|
Log("Growing PHC storage from %zu to %zu\n", old_size_pages, size_pages);
|
|
if (mAllocPages.GrowTo(size_pages)) {
|
|
for (size_t i = old_size_pages; i < size_pages; i++) {
|
|
AppendPageToFreeList(i);
|
|
}
|
|
}
|
|
} else if (size_pages < old_size_pages) {
|
|
Log("Shrink requested and ignored.");
|
|
}
|
|
}
|
|
|
|
uint64_t Random64() MOZ_REQUIRES(mMutex) { return mRNG.next(); }
|
|
|
|
PtrKind GetPtrKind(const void* aPtr);
|
|
|
|
// Get the address of the allocation page referred to via an index. Used
|
|
// when checking pointers against page boundaries.
|
|
uint8_t* AllocPageBaseAddr(uintptr_t aIndex) MOZ_REQUIRES(mMutex) {
|
|
return mAllocPages[aIndex].mBaseAddr;
|
|
}
|
|
|
|
Maybe<arena_id_t> PageArena(uintptr_t aIndex) MOZ_REQUIRES(mMutex) {
|
|
const AllocPageInfo& page = mAllocPages[aIndex];
|
|
page.AssertInUse();
|
|
|
|
return page.mArenaId;
|
|
}
|
|
|
|
size_t PageUsableSize(uintptr_t aIndex) MOZ_REQUIRES(mMutex) {
|
|
const AllocPageInfo& page = mAllocPages[aIndex];
|
|
page.AssertInUse();
|
|
|
|
return page.UsableSize();
|
|
}
|
|
|
|
void GetMemoryUsage(phc::MemoryUsage& aInfo) MOZ_EXCLUDES(mMutex) {
|
|
MutexAutoLock lock(mMutex);
|
|
|
|
aInfo = phc::MemoryUsage();
|
|
for (const auto& page : mAllocPages) {
|
|
if (page.IsPageInUse()) {
|
|
aInfo.mAllocatedBytes += page.UsableSize();
|
|
aInfo.mFragmentationBytes += page.FragmentationBytes();
|
|
}
|
|
}
|
|
|
|
// We know `this` is heap allocated.
|
|
aInfo.mMetadataBytes = MozJemalloc::malloc_usable_size(this) +
|
|
mAllocPages.SizeOfExcludingThis();
|
|
}
|
|
|
|
void SetPageInUse(uintptr_t aIndex, const Maybe<arena_id_t>& aArenaId,
|
|
uint8_t* aBaseAddr, const StackTrace& aAllocStack)
|
|
MOZ_REQUIRES(mMutex) {
|
|
mAllocPages[aIndex].SetInUse(aArenaId, aBaseAddr, aAllocStack);
|
|
}
|
|
|
|
#if PHC_LOGGING
|
|
Time GetFreeTime(uintptr_t aIndex) const MOZ_REQUIRES(mMutex) {
|
|
return mAllocPages[aIndex].mFreeTime;
|
|
}
|
|
#endif
|
|
|
|
void ResizePageInUse(uintptr_t aIndex, const Maybe<arena_id_t>& aArenaId,
|
|
uint8_t* aNewBaseAddr, const StackTrace& aAllocStack)
|
|
MOZ_REQUIRES(mMutex) {
|
|
mAllocPages[aIndex].ResizeInUse(aArenaId, aNewBaseAddr, aAllocStack);
|
|
};
|
|
|
|
void SetPageFreed(uintptr_t aIndex, const Maybe<arena_id_t>& aArenaId,
|
|
const StackTrace& aFreeStack, Delay aReuseDelay)
|
|
MOZ_REQUIRES(mMutex) {
|
|
AllocPageInfo& page = mAllocPages[aIndex];
|
|
|
|
page.SetPageFreed(aArenaId, aFreeStack, aReuseDelay, Now());
|
|
|
|
MOZ_ASSERT(!page.mNextPage);
|
|
AppendPageToFreeList(aIndex);
|
|
}
|
|
|
|
static void CrashOnGuardPage(void* aPtr) {
|
|
// An operation on a guard page? This is a bounds violation. Deliberately
|
|
// touch the page in question to cause a crash that triggers the usual PHC
|
|
// machinery.
|
|
Log("CrashOnGuardPage(%p), bounds violation\n", aPtr);
|
|
*static_cast<uint8_t*>(aPtr) = 0;
|
|
MOZ_CRASH("unreachable");
|
|
}
|
|
|
|
void EnsureValidAndInUse(void* aPtr, uintptr_t aIndex) MOZ_REQUIRES(mMutex) {
|
|
const AllocPageInfo& page = mAllocPages[aIndex];
|
|
|
|
// The pointer must point to the start of the allocation.
|
|
MOZ_RELEASE_ASSERT(page.mBaseAddr == aPtr);
|
|
|
|
if (page.mState == AllocPageState::Freed) {
|
|
Log("EnsureValidAndInUse(%p), use-after-free\n", aPtr);
|
|
// An operation on a freed page? This is a particular kind of
|
|
// use-after-free. Deliberately touch the page in question, in order to
|
|
// cause a crash that triggers the usual PHC machinery. But unlock mMutex
|
|
// first, because that self-same PHC machinery needs to re-lock it, and
|
|
// the crash causes non-local control flow so mMutex won't be unlocked
|
|
// the normal way in the caller.
|
|
mMutex.Unlock();
|
|
*static_cast<uint8_t*>(aPtr) = 0;
|
|
MOZ_CRASH("unreachable");
|
|
}
|
|
}
|
|
|
|
// This expects sPHC::mMutex to be locked but can't check it with a parameter
|
|
// since we try-lock it.
|
|
void FillAddrInfo(uintptr_t aIndex, const void* aBaseAddr, bool isGuardPage,
|
|
phc::AddrInfo& aOut) MOZ_REQUIRES(mMutex) {
|
|
const AllocPageInfo& page = mAllocPages[aIndex];
|
|
if (isGuardPage) {
|
|
aOut.mKind = phc::AddrInfo::Kind::GuardPage;
|
|
} else {
|
|
switch (page.mState) {
|
|
case AllocPageState::NeverAllocated:
|
|
aOut.mKind = phc::AddrInfo::Kind::NeverAllocatedPage;
|
|
break;
|
|
|
|
case AllocPageState::InUse:
|
|
aOut.mKind = phc::AddrInfo::Kind::InUsePage;
|
|
break;
|
|
|
|
case AllocPageState::Freed:
|
|
aOut.mKind = phc::AddrInfo::Kind::FreedPage;
|
|
break;
|
|
|
|
default:
|
|
MOZ_CRASH();
|
|
}
|
|
}
|
|
aOut.mBaseAddr = page.mBaseAddr;
|
|
aOut.mUsableSize = page.UsableSize();
|
|
aOut.mAllocStack = page.mAllocStack;
|
|
aOut.mFreeStack = page.mFreeStack;
|
|
}
|
|
|
|
void FillJemallocPtrInfo(const void* aPtr, uintptr_t aIndex,
|
|
jemalloc_ptr_info_t* aInfo) MOZ_REQUIRES(mMutex) {
|
|
const AllocPageInfo& page = mAllocPages[aIndex];
|
|
switch (page.mState) {
|
|
case AllocPageState::NeverAllocated:
|
|
break;
|
|
|
|
case AllocPageState::InUse: {
|
|
// Only return TagLiveAlloc if the pointer is within the bounds of the
|
|
// allocation's usable size.
|
|
uint8_t* base = page.mBaseAddr;
|
|
uint8_t* limit = base + page.UsableSize();
|
|
if (base <= aPtr && aPtr < limit) {
|
|
*aInfo = {TagLiveAlloc, page.mBaseAddr, page.UsableSize(),
|
|
page.mArenaId.valueOr(0)};
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case AllocPageState::Freed: {
|
|
// Only return TagFreedAlloc if the pointer is within the bounds of the
|
|
// former allocation's usable size.
|
|
uint8_t* base = page.mBaseAddr;
|
|
uint8_t* limit = base + page.UsableSize();
|
|
if (base <= aPtr && aPtr < limit) {
|
|
*aInfo = {TagFreedAlloc, page.mBaseAddr, page.UsableSize(),
|
|
page.mArenaId.valueOr(0)};
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
MOZ_CRASH();
|
|
}
|
|
|
|
// Pointers into guard pages will end up here, as will pointers into
|
|
// allocation pages that aren't within the allocation's bounds.
|
|
*aInfo = {TagUnknown, nullptr, 0, 0};
|
|
}
|
|
|
|
#ifndef XP_WIN
|
|
static void prefork() MOZ_NO_THREAD_SAFETY_ANALYSIS {
|
|
PHC::sPHC->mMutex.Lock();
|
|
}
|
|
static void postfork_parent() MOZ_NO_THREAD_SAFETY_ANALYSIS {
|
|
PHC::sPHC->mMutex.Unlock();
|
|
}
|
|
static void postfork_child() { PHC::sPHC->mMutex.Init(); }
|
|
#endif
|
|
|
|
void IncPageAllocHits() MOZ_REQUIRES(mMutex) {
|
|
#if PHC_LOGGING
|
|
mPageAllocHits++;
|
|
#endif
|
|
}
|
|
void IncPageAllocMisses() MOZ_REQUIRES(mMutex) {
|
|
#if PHC_LOGGING
|
|
mPageAllocMisses++;
|
|
#endif
|
|
}
|
|
|
|
phc::PHCStats GetPageStatsLocked() MOZ_REQUIRES(mMutex) {
|
|
phc::PHCStats stats;
|
|
|
|
for (const auto& page : mAllocPages) {
|
|
stats.mSlotsAllocated += page.IsPageInUse() ? 1 : 0;
|
|
stats.mSlotsFreed += page.IsPageFreed() ? 1 : 0;
|
|
}
|
|
stats.mSlotsUnused =
|
|
NumAllocPages() - stats.mSlotsAllocated - stats.mSlotsFreed;
|
|
|
|
return stats;
|
|
}
|
|
|
|
phc::PHCStats GetPageStats() MOZ_EXCLUDES(mMutex) {
|
|
MutexAutoLock lock(mMutex);
|
|
return GetPageStatsLocked();
|
|
}
|
|
|
|
#if PHC_LOGGING
|
|
size_t PageAllocHits() MOZ_REQUIRES(mMutex) { return mPageAllocHits; }
|
|
size_t PageAllocAttempts() MOZ_REQUIRES(mMutex) {
|
|
return mPageAllocHits + mPageAllocMisses;
|
|
}
|
|
|
|
// This is an integer because FdPrintf only supports integer printing.
|
|
size_t PageAllocHitRate() MOZ_REQUIRES(mMutex) {
|
|
return mPageAllocHits * 100 / (mPageAllocHits + mPageAllocMisses);
|
|
}
|
|
#endif
|
|
|
|
void LogNoAlloc(size_t aReqSize, size_t aAlignment, Delay newAllocDelay);
|
|
|
|
// Should we make new PHC allocations?
|
|
bool ShouldMakeNewAllocations() const {
|
|
return mPhcState == mozilla::phc::Enabled;
|
|
}
|
|
|
|
using PHCState = mozilla::phc::PHCState;
|
|
void SetState(PHCState aState) {
|
|
if (mPhcState != PHCState::Enabled && aState == PHCState::Enabled) {
|
|
MutexAutoLock lock(mMutex);
|
|
// Reset the RNG at this point with a better seed.
|
|
ResetRNG();
|
|
ForceSetNewAllocDelay(Rnd64ToDelay(mAvgFirstAllocDelay, Random64()));
|
|
}
|
|
|
|
mPhcState = aState;
|
|
}
|
|
|
|
void ResetRNG() MOZ_REQUIRES(mMutex) {
|
|
mRNG = non_crypto::XorShift128PlusRNG(RandomSeed<0>(), RandomSeed<1>());
|
|
}
|
|
|
|
void SetProbabilities(int64_t aAvgDelayFirst, int64_t aAvgDelayNormal,
|
|
int64_t aAvgDelayPageReuse) MOZ_EXCLUDES(mMutex) {
|
|
MutexAutoLock lock(mMutex);
|
|
|
|
mAvgFirstAllocDelay = CheckProbability(aAvgDelayFirst);
|
|
mAvgAllocDelay = CheckProbability(aAvgDelayNormal);
|
|
mAvgPageReuseDelay = CheckProbability(aAvgDelayPageReuse);
|
|
}
|
|
|
|
static void DisableOnCurrentThread() {
|
|
MOZ_ASSERT(!tlsIsDisabled.get());
|
|
tlsIsDisabled.set(true);
|
|
}
|
|
|
|
void EnableOnCurrentThread() {
|
|
MOZ_ASSERT(tlsIsDisabled.get());
|
|
tlsIsDisabled.set(false);
|
|
}
|
|
|
|
static bool IsDisabledOnCurrentThread() { return tlsIsDisabled.get(); }
|
|
|
|
static Time Now() {
|
|
if (!sPHC) {
|
|
return 0;
|
|
}
|
|
|
|
return sPHC->mNow;
|
|
}
|
|
|
|
void AdvanceNow(uint32_t delay = 0) {
|
|
mNow += tlsLastDelay.get() - delay;
|
|
tlsLastDelay.set(delay);
|
|
}
|
|
|
|
// Decrements the delay and returns true if it's time to make a new PHC
|
|
// allocation.
|
|
static bool DecrementDelay() {
|
|
const Delay alloc_delay = tlsAllocDelay.get();
|
|
|
|
if (MOZ_LIKELY(alloc_delay > 0)) {
|
|
tlsAllocDelay.set(alloc_delay - 1);
|
|
return false;
|
|
}
|
|
// The local delay has expired, check the shared delay. This path is also
|
|
// executed on a new thread's first allocation, the result is the same: all
|
|
// the thread's TLS fields will be initialised.
|
|
|
|
// This accesses sPHC but we want to ensure it's still a static member
|
|
// function so that sPHC isn't dereferenced until after the hot path above.
|
|
MOZ_ASSERT(sPHC);
|
|
sPHC->AdvanceNow();
|
|
|
|
// Use an atomic fetch-and-subtract. This uses unsigned underflow semantics
|
|
// to avoid doing a full compare-and-swap.
|
|
Delay new_delay = (sAllocDelay -= kDelayDecrementAmount);
|
|
Delay old_delay = new_delay + kDelayDecrementAmount;
|
|
if (MOZ_LIKELY(new_delay < DELAY_MAX)) {
|
|
// Normal case, we decremented the shared delay but it's not yet
|
|
// underflowed.
|
|
tlsAllocDelay.set(kDelayDecrementAmount);
|
|
tlsLastDelay.set(kDelayDecrementAmount);
|
|
Log("Update sAllocDelay <- %zu, tlsAllocDelay <- %zu\n",
|
|
size_t(new_delay), size_t(kDelayDecrementAmount));
|
|
return false;
|
|
}
|
|
|
|
if (old_delay < new_delay) {
|
|
// The shared delay only just underflowed, so unless we hit exactly zero
|
|
// we should set our local counter and continue.
|
|
Log("Update sAllocDelay <- %zu, tlsAllocDelay <- %zu\n",
|
|
size_t(new_delay), size_t(old_delay));
|
|
if (old_delay == 0) {
|
|
// We don't need to set tlsAllocDelay because it's already zero, we know
|
|
// because the condition at the beginning of this function failed.
|
|
return true;
|
|
}
|
|
tlsAllocDelay.set(old_delay);
|
|
tlsLastDelay.set(old_delay);
|
|
return false;
|
|
}
|
|
|
|
// The delay underflowed on another thread or a previous failed allocation
|
|
// by this thread. Return true and attempt the next allocation, if the
|
|
// other thread wins we'll check for that before committing.
|
|
Log("Update sAllocDelay <- %zu, tlsAllocDelay <- %zu\n", size_t(new_delay),
|
|
size_t(alloc_delay));
|
|
return true;
|
|
}
|
|
|
|
static void ResetLocalAllocDelay(Delay aDelay = 0) {
|
|
// We could take some delay from the shared delay but we'd need a
|
|
// compare-and-swap because this is called on paths that don't make
|
|
// allocations. Or we can set the local delay to zero and let it get
|
|
// initialised on the next allocation.
|
|
tlsAllocDelay.set(aDelay);
|
|
tlsLastDelay.set(aDelay);
|
|
}
|
|
|
|
static void ForceSetNewAllocDelay(Delay aNewAllocDelay) {
|
|
Log("Setting sAllocDelay <- %zu\n", size_t(aNewAllocDelay));
|
|
sAllocDelay = aNewAllocDelay;
|
|
ResetLocalAllocDelay();
|
|
}
|
|
|
|
// Set a new allocation delay and return true if the delay was less than zero
|
|
// (but it's unsigned so interpret it as signed) indicating that we won the
|
|
// race to make the next allocation.
|
|
static bool SetNewAllocDelay(Delay aNewAllocDelay) {
|
|
bool cas_retry;
|
|
do {
|
|
// We read the current delay on every iteration, we consider that the PHC
|
|
// allocation is still "up for grabs" if sAllocDelay < 0. This is safe
|
|
// even while other threads continuing to fetch-and-subtract sAllocDelay
|
|
// in DecrementDelay(), up to DELAY_MAX (2^31) calls to DecrementDelay().
|
|
Delay read_delay = sAllocDelay;
|
|
if (read_delay < DELAY_MAX) {
|
|
// Another thread already set a valid delay.
|
|
Log("Observe delay %zu this thread lost the race\n",
|
|
size_t(read_delay));
|
|
ResetLocalAllocDelay();
|
|
return false;
|
|
} else {
|
|
Log("Preparing for CAS, read sAllocDelay %zu\n", size_t(read_delay));
|
|
}
|
|
|
|
cas_retry = !sAllocDelay.compareExchange(read_delay, aNewAllocDelay);
|
|
if (cas_retry) {
|
|
Log("Lost the CAS, sAllocDelay is now %zu\n", size_t(sAllocDelay));
|
|
cpu_pause();
|
|
// We raced against another thread and lost.
|
|
}
|
|
} while (cas_retry);
|
|
Log("Won the CAS, set sAllocDelay = %zu\n", size_t(sAllocDelay));
|
|
ResetLocalAllocDelay();
|
|
return true;
|
|
}
|
|
|
|
static Delay LocalAllocDelay() { return tlsAllocDelay.get(); }
|
|
static Delay SharedAllocDelay() { return sAllocDelay; }
|
|
|
|
static Delay LastDelay() { return tlsLastDelay.get(); }
|
|
|
|
Maybe<uintptr_t> PopNextFreeIfAllocatable(Time now) MOZ_REQUIRES(mMutex) {
|
|
if (!mFreePageListHead) {
|
|
return Nothing();
|
|
}
|
|
|
|
uintptr_t index = mFreePageListHead.value();
|
|
|
|
MOZ_RELEASE_ASSERT(index < NumAllocPages());
|
|
AllocPageInfo& page = mAllocPages[index];
|
|
page.AssertNotInUse();
|
|
|
|
if (!page.IsPageAllocatable(now)) {
|
|
return Nothing();
|
|
}
|
|
|
|
mFreePageListHead = page.mNextPage;
|
|
page.mNextPage = Nothing();
|
|
if (!mFreePageListHead) {
|
|
mFreePageListTail = Nothing();
|
|
}
|
|
|
|
return Some(index);
|
|
}
|
|
|
|
void UnpopNextFree(uintptr_t index) MOZ_REQUIRES(mMutex) {
|
|
AllocPageInfo& page = mAllocPages[index];
|
|
MOZ_ASSERT(!page.mNextPage);
|
|
|
|
page.mNextPage = mFreePageListHead;
|
|
mFreePageListHead = Some(index);
|
|
if (!mFreePageListTail) {
|
|
mFreePageListTail = Some(index);
|
|
}
|
|
}
|
|
|
|
void AppendPageToFreeList(uintptr_t aIndex) MOZ_REQUIRES(mMutex) {
|
|
MOZ_RELEASE_ASSERT(aIndex < NumAllocPages());
|
|
AllocPageInfo& page = mAllocPages[aIndex];
|
|
MOZ_ASSERT(!page.mNextPage);
|
|
MOZ_ASSERT(mFreePageListHead != Some(aIndex) &&
|
|
mFreePageListTail != Some(aIndex));
|
|
|
|
if (!mFreePageListTail) {
|
|
// The list is empty this page will become the beginning and end.
|
|
MOZ_ASSERT(!mFreePageListHead);
|
|
mFreePageListHead = Some(aIndex);
|
|
} else {
|
|
MOZ_ASSERT(mFreePageListTail.value() < NumAllocPages());
|
|
AllocPageInfo& tail_page = mAllocPages[mFreePageListTail.value()];
|
|
MOZ_ASSERT(!tail_page.mNextPage);
|
|
tail_page.mNextPage = Some(aIndex);
|
|
}
|
|
page.mNextPage = Nothing();
|
|
mFreePageListTail = Some(aIndex);
|
|
}
|
|
|
|
private:
|
|
template <int N>
|
|
uint64_t RandomSeed() {
|
|
// An older version of this code used RandomUint64() here, but on Mac that
|
|
// function uses arc4random(), which can allocate, which would cause
|
|
// re-entry, which would be bad. So we just use time(), a local variable
|
|
// address and a global variable address. These are mediocre sources of
|
|
// entropy, but good enough for PHC.
|
|
static_assert(N == 0 || N == 1 || N == 2, "must be 0, 1 or 2");
|
|
uint64_t seed;
|
|
if (N == 0) {
|
|
time_t t = time(nullptr);
|
|
seed = t ^ (t << 32);
|
|
} else if (N == 1) {
|
|
seed = uintptr_t(&seed) ^ (uintptr_t(&seed) << 32);
|
|
} else {
|
|
seed = uintptr_t(&sRegion) ^ (uintptr_t(&sRegion) << 32);
|
|
}
|
|
return seed;
|
|
}
|
|
|
|
public:
|
|
// Attempt a page allocation if the time and the size are right. Allocated
|
|
// memory is zeroed if aZero is true. On failure, the caller should attempt a
|
|
// normal allocation via MozJemalloc. Can be called in a context where
|
|
// PHC::mMutex is locked.
|
|
void* MaybePageAlloc(const Maybe<arena_id_t>& aArenaId, size_t aReqSize,
|
|
size_t aAlignment, bool aZero);
|
|
|
|
void FreePage(uintptr_t aIndex, const Maybe<arena_id_t>& aArenaId,
|
|
const StackTrace& aFreeStack, Delay aReuseDelay);
|
|
|
|
// This handles both free and moz_arena_free.
|
|
void PageFree(const Maybe<arena_id_t>& aArenaId, void* aPtr);
|
|
|
|
Maybe<void*> PageRealloc(const Maybe<arena_id_t>& aArenaId, void* aOldPtr,
|
|
size_t aNewSize);
|
|
|
|
void PagePtrInfo(const void* aPtr, jemalloc_ptr_info_t* aInfo);
|
|
|
|
size_t PtrUsableSize(usable_ptr_t aPtr);
|
|
|
|
bool IsPHCAllocation(const void* aPtr, mozilla::phc::AddrInfo* aOut);
|
|
|
|
void Crash(const char* aMessage);
|
|
|
|
private:
|
|
// To improve locality we try to order this file by how frequently different
|
|
// fields are modified and place all the modified-together fields early and
|
|
// ideally within a single cache line.
|
|
// The mutex that protects the other members.
|
|
alignas(kCacheLineSize) Mutex mMutex MOZ_UNANNOTATED;
|
|
|
|
// The current time. We use ReleaseAcquire semantics since we attempt to
|
|
// update this by larger increments and don't want to lose an entire update.
|
|
Atomic<Time, ReleaseAcquire> mNow;
|
|
|
|
// This will only ever be updated from one thread. The other threads should
|
|
// eventually get the update.
|
|
Atomic<PHCState, Relaxed> mPhcState =
|
|
Atomic<PHCState, Relaxed>(DEFAULT_STATE);
|
|
|
|
// RNG for deciding which allocations to treat specially. It doesn't need to
|
|
// be high quality.
|
|
//
|
|
// This is a raw pointer for the reason explained in the comment above
|
|
// PHC's constructor. Don't change it to UniquePtr or anything like that.
|
|
non_crypto::XorShift128PlusRNG mRNG MOZ_GUARDED_BY(mMutex);
|
|
|
|
// A linked list of free pages. Pages are allocated from the head of the list
|
|
// and returned to the tail. The list will naturally order itself by "last
|
|
// freed time" so if the head of the list can't satisfy an allocation due to
|
|
// time then none of the pages can.
|
|
Maybe<uintptr_t> mFreePageListHead MOZ_GUARDED_BY(mMutex);
|
|
Maybe<uintptr_t> mFreePageListTail MOZ_GUARDED_BY(mMutex);
|
|
|
|
#if PHC_LOGGING
|
|
// How many allocations that could have been page allocs actually were? As
|
|
// constrained kNumAllocPages. If the hit ratio isn't close to 100% it's
|
|
// likely that the global constants are poorly chosen.
|
|
size_t mPageAllocHits MOZ_GUARDED_BY(mMutex) = 0;
|
|
size_t mPageAllocMisses MOZ_GUARDED_BY(mMutex) = 0;
|
|
#endif
|
|
|
|
// The remaining fields are updated much less often, place them on the next
|
|
// cache line.
|
|
|
|
// The average delay before doing any page allocations at the start of a
|
|
// process. Note that roughly 1 million allocations occur in the main process
|
|
// while starting the browser. The delay range is 1..gAvgFirstAllocDelay*2.
|
|
alignas(kCacheLineSize) Delay mAvgFirstAllocDelay
|
|
MOZ_GUARDED_BY(mMutex) = 64 * 1024;
|
|
|
|
// The average delay until the next attempted page allocation, once we get
|
|
// past the first delay. The delay range is 1..kAvgAllocDelay*2.
|
|
Delay mAvgAllocDelay MOZ_GUARDED_BY(mMutex) = 16 * 1024;
|
|
|
|
// The average delay before reusing a freed page. Should be significantly
|
|
// larger than kAvgAllocDelay, otherwise there's not much point in having it.
|
|
// The delay range is (kAvgAllocDelay / 2)..(kAvgAllocDelay / 2 * 3). This is
|
|
// different to the other delay ranges in not having a minimum of 1, because
|
|
// that's such a short delay that there is a high likelihood of bad stacks in
|
|
// any crash report.
|
|
Delay mAvgPageReuseDelay MOZ_GUARDED_BY(mMutex) = 256 * 1024;
|
|
|
|
// When true, PHC does as little as possible.
|
|
//
|
|
// (a) It does not allocate any new page allocations.
|
|
//
|
|
// (b) It avoids doing any operations that might call malloc/free/etc., which
|
|
// would cause re-entry into PHC. (In practice, MozStackWalk() is the
|
|
// only such operation.) Note that calls to the functions in MozJemalloc
|
|
// are ok.
|
|
//
|
|
// For example, replace_malloc() will just fall back to mozjemalloc. However,
|
|
// operations involving existing allocations are more complex, because those
|
|
// existing allocations may be page allocations. For example, if
|
|
// replace_free() is passed a page allocation on a PHC-disabled thread, it
|
|
// will free the page allocation in the usual way, but it will get a dummy
|
|
// freeStack in order to avoid calling MozStackWalk(), as per (b) above.
|
|
//
|
|
// This single disabling mechanism has two distinct uses.
|
|
//
|
|
// - It's used to prevent re-entry into PHC, which can cause correctness
|
|
// problems. For example, consider this sequence.
|
|
//
|
|
// 1. enter replace_free()
|
|
// 2. which calls PageFree()
|
|
// 3. which calls MozStackWalk()
|
|
// 4. which locks a mutex M, and then calls malloc
|
|
// 5. enter replace_malloc()
|
|
// 6. which calls MaybePageAlloc()
|
|
// 7. which calls MozStackWalk()
|
|
// 8. which (re)locks a mutex M --> deadlock
|
|
//
|
|
// We avoid this sequence by "disabling" the thread in PageFree() (at step
|
|
// 2), which causes MaybePageAlloc() to fail, avoiding the call to
|
|
// MozStackWalk() (at step 7).
|
|
//
|
|
// In practice, realloc or free of a PHC allocation is unlikely on a thread
|
|
// that is disabled because of this use: MozStackWalk() will probably only
|
|
// realloc/free allocations that it allocated itself, but those won't be
|
|
// page allocations because PHC is disabled before calling MozStackWalk().
|
|
//
|
|
// (Note that MaybePageAlloc() could safely do a page allocation so long as
|
|
// it avoided calling MozStackWalk() by getting a dummy allocStack. But it
|
|
// wouldn't be useful, and it would prevent the second use below.)
|
|
//
|
|
// - It's used to prevent PHC allocations in some tests that rely on
|
|
// mozjemalloc's exact allocation behaviour, which PHC does not replicate
|
|
// exactly. (Note that (b) isn't necessary for this use -- MozStackWalk()
|
|
// could be safely called -- but it is necessary for the first use above.)
|
|
//
|
|
static PHC_THREAD_LOCAL(bool) tlsIsDisabled;
|
|
|
|
// Delay until the next attempt at a page allocation. The delay is made up of
|
|
// two parts the global delay and each thread's local portion of that delay:
|
|
//
|
|
// delay = sDelay + sum_all_threads(tlsAllocDelay)
|
|
//
|
|
// Threads use their local delay to reduce contention on the shared delay.
|
|
//
|
|
// See the comment in MaybePageAlloc() for an explanation of why it uses
|
|
// ReleaseAcquire semantics.
|
|
static Atomic<Delay, ReleaseAcquire> sAllocDelay;
|
|
static PHC_THREAD_LOCAL(Delay) tlsAllocDelay;
|
|
|
|
// The last value we set tlsAllocDelay to before starting to count down.
|
|
static PHC_THREAD_LOCAL(Delay) tlsLastDelay;
|
|
|
|
// Using mfbt/Array.h makes MOZ_GUARDED_BY more reliable than a C array.
|
|
BaseArray<AllocPageInfo> mAllocPages MOZ_GUARDED_BY(mMutex);
|
|
|
|
public:
|
|
// There are two kinds of page.
|
|
// - Allocation pages, from which allocations are made.
|
|
// - Guard pages, which are never touched by PHC.
|
|
//
|
|
size_t NumAllocPages() const MOZ_REQUIRES(mMutex) {
|
|
return mAllocPages.Capacity();
|
|
}
|
|
|
|
// These page kinds are interleaved; each allocation page has a guard page on
|
|
// either side.
|
|
size_t NumAllPages() const MOZ_REQUIRES(mMutex) {
|
|
return NumAllocPages() * 2 + 1;
|
|
}
|
|
|
|
Delay GetAvgAllocDelay() MOZ_REQUIRES(mMutex) { return mAvgAllocDelay; }
|
|
Delay GetAvgFirstAllocDelay() MOZ_REQUIRES(mMutex) {
|
|
return mAvgFirstAllocDelay;
|
|
}
|
|
Delay GetAvgPageReuseDelay() MOZ_REQUIRES(mMutex) {
|
|
return mAvgPageReuseDelay;
|
|
}
|
|
Delay ReuseDelay() MOZ_REQUIRES(mMutex) {
|
|
Delay avg_reuse_delay = GetAvgPageReuseDelay();
|
|
return (avg_reuse_delay / 2) +
|
|
Rnd64ToDelay(avg_reuse_delay / 2, Random64());
|
|
}
|
|
|
|
// Both of these are accessed early on hot code paths. We make them both
|
|
// static variables rathan making sRegion a member of sPHC to keep these hot
|
|
// code paths as fast as possible. They're both "write once" so they can
|
|
// share a cache line.
|
|
static PHCRegion sRegion;
|
|
static PHC* sPHC;
|
|
};
|
|
|
|
// Maps a pointer to a PHC-specific structure:
|
|
// - A guard page (it is unspecified which one)
|
|
// - An allocation page (with an index < kNumAllocPages)
|
|
//
|
|
// PtrKind should only be used on pointers that are within PHC's virtual address
|
|
// range. Callers should usually check sRegion.WithinBounds() first, if
|
|
// successful then PHC::GetPtrKind() can be used safely.
|
|
//
|
|
// The standard way of handling a PtrKind is to check sRegion.WithinBounds()
|
|
// first, and if that succeeds, to call GetPtrKind and check IsGuardPage(), and
|
|
// if that fails, then this is a PHC pointer.
|
|
class PtrKind {
|
|
private:
|
|
enum class Tag : uint8_t {
|
|
GuardPage,
|
|
AllocPage,
|
|
};
|
|
|
|
Tag mTag = Tag::GuardPage;
|
|
uintptr_t mIndex = 0; // Only used if mTag == Tag::AllocPage.
|
|
|
|
protected:
|
|
// Detect what a pointer points to. This constructor must be fast because it
|
|
// is called for every call to free(), realloc(), malloc_usable_size(), and
|
|
// jemalloc_ptr_info().
|
|
PtrKind(const void* aPtr, const uint8_t* aPagesStart) {
|
|
uintptr_t offset = static_cast<const uint8_t*>(aPtr) - aPagesStart;
|
|
uintptr_t allPageIndex = offset / kPhcPageSize;
|
|
|
|
if (allPageIndex & 1) {
|
|
// Odd-indexed pages are allocation pages.
|
|
uintptr_t allocPageIndex = allPageIndex / 2;
|
|
mTag = Tag::AllocPage;
|
|
mIndex = allocPageIndex;
|
|
} else {
|
|
// Even-numbered pages are guard pages.
|
|
mTag = Tag::GuardPage;
|
|
}
|
|
}
|
|
friend PtrKind PHC::GetPtrKind(const void* aPtr);
|
|
|
|
public:
|
|
bool IsGuardPage() const { return mTag == Tag::GuardPage; }
|
|
|
|
// This should only be called after IsGuardPage() has returned false.
|
|
Maybe<uintptr_t> AllocPageIndex(uintptr_t aNumPages) const {
|
|
MOZ_RELEASE_ASSERT(mTag == Tag::AllocPage);
|
|
|
|
if (mIndex < aNumPages) {
|
|
return Some(mIndex);
|
|
} else {
|
|
return Nothing();
|
|
}
|
|
}
|
|
};
|
|
|
|
PtrKind PHC::GetPtrKind(const void* aPtr) {
|
|
MOZ_ASSERT(sRegion.WithinBounds(aPtr));
|
|
return PtrKind(aPtr, sRegion.PagesStart());
|
|
}
|
|
|
|
// These globals are read together and hardly ever written. They should be on
|
|
// the same cache line. They should be in a different cache line to data that
|
|
// is manipulated often (sMutex and mNow are members of sPHC for that reason) so
|
|
// that this cache line can be shared amoung cores.
|
|
alignas(kCacheLineSize) PHCRegion PHC::sRegion;
|
|
PHC* PHC::sPHC;
|
|
|
|
PHC_THREAD_LOCAL(bool) PHC::tlsIsDisabled;
|
|
PHC_THREAD_LOCAL(Delay) PHC::tlsAllocDelay;
|
|
Atomic<Delay, ReleaseAcquire> PHC::sAllocDelay;
|
|
PHC_THREAD_LOCAL(Delay) PHC::tlsLastDelay;
|
|
|
|
// When PHC wants to crash we first have to unlock so that the crash reporter
|
|
// can call into PHC to lockup its pointer. That also means that before calling
|
|
// PHCCrash please ensure that state is consistent. Because this can report an
|
|
// arbitrary string, use of it must be reviewed by Firefox data stewards.
|
|
void PHC::Crash(const char* aMessage) MOZ_REQUIRES(mMutex) {
|
|
mMutex.Unlock();
|
|
MOZ_CRASH_UNSAFE(aMessage);
|
|
}
|
|
|
|
class AutoDisableOnCurrentThread {
|
|
public:
|
|
AutoDisableOnCurrentThread(const AutoDisableOnCurrentThread&) = delete;
|
|
|
|
const AutoDisableOnCurrentThread& operator=(
|
|
const AutoDisableOnCurrentThread&) = delete;
|
|
|
|
explicit AutoDisableOnCurrentThread() { PHC::DisableOnCurrentThread(); }
|
|
~AutoDisableOnCurrentThread() { PHC::sPHC->EnableOnCurrentThread(); }
|
|
};
|
|
|
|
//---------------------------------------------------------------------------
|
|
// Initialisation
|
|
//---------------------------------------------------------------------------
|
|
|
|
// WARNING: this function runs *very* early -- before all static initializers
|
|
// have run. For this reason, non-scalar globals (sPHC) are allocated
|
|
// dynamically (so we can guarantee their construction in this function) rather
|
|
// than statically. sRegion is allocated statically to avoid an extra
|
|
// dereference.
|
|
//
|
|
// If initialisation fails sPHC will be null. Returning bool won't help the
|
|
// caller as there's nothing they can do.
|
|
void phc_init() {
|
|
// We must only initialise once.
|
|
MOZ_ASSERT(!PHC::sPHC);
|
|
|
|
if (GetKernelPageSize() != kPhcPageSize) {
|
|
return;
|
|
}
|
|
|
|
if (!PHC::sRegion.AllocVirtualAddresses()) {
|
|
return;
|
|
}
|
|
|
|
// sPHC is never freed. It lives for the life of the process.
|
|
PHC::sPHC = new (fallible) PHC();
|
|
if (!PHC::sPHC) {
|
|
PHC::sRegion.ReleaseVirtualAddresses();
|
|
return;
|
|
}
|
|
|
|
#ifndef XP_WIN
|
|
// Avoid deadlocks when forking by acquiring our state lock prior to forking
|
|
// and releasing it after forking. See |LogAlloc|'s |phc_init| for
|
|
// in-depth details.
|
|
pthread_atfork(PHC::prefork, PHC::postfork_parent, PHC::postfork_child);
|
|
#endif
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
// Page allocation operations
|
|
//---------------------------------------------------------------------------
|
|
|
|
// This is the hot-path for testing if we should make a PHC allocation, it
|
|
// should be inlined into the caller while the remainder of the tests that are
|
|
// in MaybePageAlloc need not be inlined.
|
|
static MOZ_ALWAYS_INLINE bool ShouldPageAllocHot(size_t aReqSize) {
|
|
if (MOZ_UNLIKELY(!PHC::sPHC)) {
|
|
return false;
|
|
}
|
|
|
|
if (MOZ_UNLIKELY(aReqSize > kPhcPageSize)) {
|
|
return false;
|
|
}
|
|
|
|
// Decrement the delay. If it's zero, we do a page allocation and reset the
|
|
// delay to a random number.
|
|
if (MOZ_LIKELY(!PHC::DecrementDelay())) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void PHC::LogNoAlloc(size_t aReqSize, size_t aAlignment, Delay newAllocDelay)
|
|
MOZ_REQUIRES(mMutex) {
|
|
// No pages are available, or VirtualAlloc/mprotect failed.
|
|
#if PHC_LOGGING
|
|
phc::PHCStats stats = GetPageStatsLocked();
|
|
Log("No PageAlloc(%zu, %zu), sAllocDelay <- %zu, fullness %zu/%zu/%zu, "
|
|
"hits %zu/%zu (%zu%%)\n",
|
|
aReqSize, aAlignment, size_t(newAllocDelay), stats.mSlotsAllocated,
|
|
stats.mSlotsFreed, NumAllocPages(), PageAllocHits(), PageAllocAttempts(),
|
|
PageAllocHitRate());
|
|
#endif
|
|
}
|
|
|
|
void* PHC::MaybePageAlloc(const Maybe<arena_id_t>& aArenaId, size_t aReqSize,
|
|
size_t aAlignment, bool aZero) {
|
|
MOZ_ASSERT(std::has_single_bit(aAlignment));
|
|
if (!ShouldMakeNewAllocations()) {
|
|
// Reset the allocation delay so that we take the fast path most of the
|
|
// time. Rather than take the lock and use the RNG which are unnecessary
|
|
// when PHC is disabled, instead set the delay to a reasonably high number,
|
|
// the default average first allocation delay. This is reset when PHC is
|
|
// re-enabled anyway.
|
|
ForceSetNewAllocDelay(kDelayResetWhenDisabled);
|
|
return nullptr;
|
|
}
|
|
|
|
if (IsDisabledOnCurrentThread()) {
|
|
// We don't reset sAllocDelay since that might affect other threads. We
|
|
// assume this is okay because either this thread will be re-enabled after
|
|
// less than DELAY_MAX allocations or that there are other active threads
|
|
// that will reset sAllocDelay. We do reset our local delay which will
|
|
// cause this thread to "back off" from updating sAllocDelay on future
|
|
// allocations.
|
|
ResetLocalAllocDelay(kDelayBackoffAmount);
|
|
return nullptr;
|
|
}
|
|
|
|
// Disable on this thread *before* getting the stack trace.
|
|
AutoDisableOnCurrentThread disable;
|
|
|
|
// Get the stack trace *before* locking the mutex. If we return nullptr then
|
|
// it was a waste, but it's not so frequent, and doing a stack walk while
|
|
// the mutex is locked is problematic (see the big comment on
|
|
// StackTrace::Fill() for details).
|
|
StackTrace allocStack;
|
|
allocStack.Fill();
|
|
|
|
MutexAutoLock lock(mMutex);
|
|
|
|
Time now = Now();
|
|
|
|
Delay newAllocDelay = Rnd64ToDelay(GetAvgAllocDelay(), Random64());
|
|
if (!SetNewAllocDelay(newAllocDelay)) {
|
|
return nullptr;
|
|
}
|
|
|
|
// Pages are allocated from a free list populated in order of when they're
|
|
// freed. If the page at the head of the list is too recently freed to be
|
|
// reused then no other pages on the list will be either.
|
|
|
|
Maybe<uintptr_t> mb_index = PopNextFreeIfAllocatable(now);
|
|
if (!mb_index) {
|
|
IncPageAllocMisses();
|
|
LogNoAlloc(aReqSize, aAlignment, newAllocDelay);
|
|
return nullptr;
|
|
}
|
|
uintptr_t index = mb_index.value();
|
|
|
|
#if PHC_LOGGING
|
|
Time lifetime = 0;
|
|
#endif
|
|
uint8_t* pagePtr = sRegion.AllocPagePtr(index);
|
|
MOZ_ASSERT(pagePtr);
|
|
bool ok =
|
|
#ifdef XP_WIN
|
|
!!VirtualAlloc(pagePtr, kPhcPageSize, MEM_COMMIT, PAGE_READWRITE);
|
|
#else
|
|
mprotect(pagePtr, kPhcPageSize, PROT_READ | PROT_WRITE) == 0;
|
|
#endif
|
|
|
|
if (!ok) {
|
|
UnpopNextFree(index);
|
|
IncPageAllocMisses();
|
|
LogNoAlloc(aReqSize, aAlignment, newAllocDelay);
|
|
return nullptr;
|
|
}
|
|
|
|
size_t usableSize = MozJemalloc::malloc_good_size(aReqSize);
|
|
MOZ_ASSERT(usableSize > 0);
|
|
|
|
// Put the allocation as close to the end of the page as possible,
|
|
// allowing for alignment requirements.
|
|
uint8_t* ptr = pagePtr + kPhcPageSize - usableSize;
|
|
if (aAlignment != 1) {
|
|
ptr = reinterpret_cast<uint8_t*>(
|
|
(reinterpret_cast<uintptr_t>(ptr) & ~(aAlignment - 1)));
|
|
}
|
|
|
|
#if PHC_LOGGING
|
|
Time then = GetFreeTime(index);
|
|
lifetime = then != 0 ? now - then : 0;
|
|
#endif
|
|
|
|
SetPageInUse(index, aArenaId, ptr, allocStack);
|
|
|
|
if (aZero) {
|
|
memset(ptr, 0, usableSize);
|
|
} else {
|
|
#ifdef DEBUG
|
|
memset(ptr, kAllocJunk, usableSize);
|
|
#endif
|
|
}
|
|
|
|
IncPageAllocHits();
|
|
#if PHC_LOGGING
|
|
phc::PHCStats stats = GetPageStatsLocked();
|
|
Log("PageAlloc(%zu, %zu) -> %p[%zu]/%p (%zu) (z%zu), sAllocDelay <- %zu, "
|
|
"fullness %zu/%zu/%zu, hits %zu/%zu (%zu%%), lifetime %zu\n",
|
|
aReqSize, aAlignment, pagePtr, index, ptr, usableSize,
|
|
size_t(newAllocDelay), size_t(SharedAllocDelay()), stats.mSlotsAllocated,
|
|
stats.mSlotsFreed, NumAllocPages(), PageAllocHits(), PageAllocAttempts(),
|
|
PageAllocHitRate(), lifetime);
|
|
#endif
|
|
|
|
return ptr;
|
|
}
|
|
|
|
void PHC::FreePage(uintptr_t aIndex, const Maybe<arena_id_t>& aArenaId,
|
|
const StackTrace& aFreeStack, Delay aReuseDelay)
|
|
MOZ_REQUIRES(mMutex) {
|
|
void* pagePtr = sRegion.AllocPagePtr(aIndex);
|
|
|
|
#ifdef XP_WIN
|
|
if (!VirtualFree(pagePtr, kPhcPageSize, MEM_DECOMMIT)) {
|
|
Crash("VirtualFree failed");
|
|
}
|
|
#else
|
|
if (mmap(pagePtr, kPhcPageSize, PROT_NONE, MAP_FIXED | MAP_PRIVATE | MAP_ANON,
|
|
-1, 0) == MAP_FAILED) {
|
|
Crash("mmap failed");
|
|
}
|
|
#endif
|
|
|
|
SetPageFreed(aIndex, aArenaId, aFreeStack, aReuseDelay);
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
// replace-malloc machinery
|
|
//---------------------------------------------------------------------------
|
|
|
|
// This handles malloc, moz_arena_malloc, and realloc-with-a-nullptr.
|
|
MOZ_ALWAYS_INLINE static void* PageMalloc(const Maybe<arena_id_t>& aArenaId,
|
|
size_t aReqSize) {
|
|
void* ptr =
|
|
ShouldPageAllocHot(aReqSize)
|
|
// The test on aArenaId here helps the compiler optimise away
|
|
// the construction of Nothing() in the caller.
|
|
? PHC::sPHC->MaybePageAlloc(aArenaId.isSome() ? aArenaId : Nothing(),
|
|
aReqSize, /* aAlignment */ 1,
|
|
/* aZero */ false)
|
|
: nullptr;
|
|
return ptr ? ptr
|
|
: (aArenaId.isSome()
|
|
? MozJemalloc::moz_arena_malloc(*aArenaId, aReqSize)
|
|
: MozJemalloc::malloc(aReqSize));
|
|
}
|
|
|
|
inline void* MozJemallocPHC::malloc(size_t aReqSize) {
|
|
return PageMalloc(Nothing(), aReqSize);
|
|
}
|
|
|
|
// This handles both calloc and moz_arena_calloc.
|
|
MOZ_ALWAYS_INLINE static void* PageCalloc(const Maybe<arena_id_t>& aArenaId,
|
|
size_t aNum, size_t aReqSize) {
|
|
CheckedInt<size_t> checkedSize = CheckedInt<size_t>(aNum) * aReqSize;
|
|
if (!checkedSize.isValid()) {
|
|
return nullptr;
|
|
}
|
|
|
|
void* ptr =
|
|
ShouldPageAllocHot(checkedSize.value())
|
|
// The test on aArenaId here helps the compiler optimise away
|
|
// the construction of Nothing() in the caller.
|
|
? PHC::sPHC->MaybePageAlloc(aArenaId.isSome() ? aArenaId : Nothing(),
|
|
checkedSize.value(), /* aAlignment */ 1,
|
|
/* aZero */ true)
|
|
: nullptr;
|
|
return ptr ? ptr
|
|
: (aArenaId.isSome()
|
|
? MozJemalloc::moz_arena_calloc(*aArenaId, aNum, aReqSize)
|
|
: MozJemalloc::calloc(aNum, aReqSize));
|
|
}
|
|
|
|
inline void* MozJemallocPHC::calloc(size_t aNum, size_t aReqSize) {
|
|
return PageCalloc(Nothing(), aNum, aReqSize);
|
|
}
|
|
|
|
MOZ_ALWAYS_INLINE static bool FastIsPHCPtr(const void* aPtr) {
|
|
if (MOZ_UNLIKELY(!PHC::sPHC)) {
|
|
return false;
|
|
}
|
|
|
|
return PHC::sRegion.WithinBounds(aPtr);
|
|
}
|
|
|
|
// This function handles both realloc and moz_arena_realloc.
|
|
//
|
|
// As always, realloc is complicated, and doubly so when there are two
|
|
// different kinds of allocations in play. Here are the possible transitions,
|
|
// and what we do in practice.
|
|
//
|
|
// - normal-to-normal: This is straightforward and obviously necessary.
|
|
//
|
|
// - normal-to-page: This is disallowed because it would require getting the
|
|
// arenaId of the normal allocation, which isn't possible in non-DEBUG builds
|
|
// for security reasons.
|
|
//
|
|
// - page-to-page: This is done whenever possible, i.e. whenever the new size
|
|
// is less than or equal to 4 KiB. This choice counterbalances the
|
|
// disallowing of normal-to-page allocations, in order to avoid biasing
|
|
// towards or away from page allocations. It always occurs in-place.
|
|
//
|
|
// - page-to-normal: this is done only when necessary, i.e. only when the new
|
|
// size is greater than 4 KiB. This choice naturally flows from the
|
|
// prior choice on page-to-page transitions.
|
|
//
|
|
// In summary: realloc doesn't change the allocation kind unless it must.
|
|
//
|
|
// This function may return:
|
|
// - Some(pointer) when PHC handled the reallocation.
|
|
// - Some(nullptr) when PHC should have handled a page-to-normal transition
|
|
// but couldn't because of OOM.
|
|
// - Nothing() when PHC is disabled or the original allocation was not
|
|
// under PHC.
|
|
MOZ_ALWAYS_INLINE static Maybe<void*> MaybePageRealloc(
|
|
const Maybe<arena_id_t>& aArenaId, void* aOldPtr, size_t aNewSize) {
|
|
if (!aOldPtr) {
|
|
// Null pointer. Treat like malloc(aNewSize).
|
|
return Some(PageMalloc(aArenaId, aNewSize));
|
|
}
|
|
|
|
if (MOZ_UNLIKELY(!FastIsPHCPtr(aOldPtr))) {
|
|
// A normal-to-normal transition.
|
|
return Nothing();
|
|
}
|
|
|
|
return PHC::sPHC->PageRealloc(aArenaId, aOldPtr, aNewSize);
|
|
}
|
|
|
|
Maybe<void*> PHC::PageRealloc(const Maybe<arena_id_t>& aArenaId, void* aOldPtr,
|
|
size_t aNewSize) MOZ_EXCLUDES(mMutex) {
|
|
PtrKind pk = GetPtrKind(aOldPtr);
|
|
|
|
if (pk.IsGuardPage()) {
|
|
CrashOnGuardPage(aOldPtr);
|
|
}
|
|
|
|
// A page-to-something transition.
|
|
AdvanceNow(LocalAllocDelay());
|
|
|
|
// Note that `disable` has no effect unless it is emplaced below.
|
|
Maybe<AutoDisableOnCurrentThread> disable;
|
|
// Get the stack trace *before* locking the mutex.
|
|
StackTrace stack;
|
|
if (IsDisabledOnCurrentThread()) {
|
|
// PHC is disabled on this thread. Leave the stack empty.
|
|
} else {
|
|
// Disable on this thread *before* getting the stack trace.
|
|
disable.emplace();
|
|
stack.Fill();
|
|
}
|
|
|
|
MutexAutoLock lock(mMutex);
|
|
|
|
Maybe<uintptr_t> mb_index = pk.AllocPageIndex(NumAllocPages());
|
|
if (!mb_index) {
|
|
Crash("Realloc of invalid pointer");
|
|
}
|
|
// At this point we know we have an allocation page.
|
|
uintptr_t index = mb_index.value();
|
|
|
|
// Check for realloc() of a freed block.
|
|
EnsureValidAndInUse(aOldPtr, index);
|
|
|
|
if (aNewSize <= kPhcPageSize && ShouldMakeNewAllocations()) {
|
|
// A page-to-page transition. Just keep using the page allocation. We do
|
|
// this even if the thread is disabled, because it doesn't create a new
|
|
// page allocation. Note that ResizePageInUse() checks aArenaId.
|
|
//
|
|
// Move the bytes with memmove(), because the old allocation and the new
|
|
// allocation overlap. Move the usable size rather than the requested size,
|
|
// because the user might have used malloc_usable_size() and filled up the
|
|
// usable size.
|
|
size_t oldUsableSize = PageUsableSize(index);
|
|
size_t newUsableSize = MozJemalloc::malloc_good_size(aNewSize);
|
|
uint8_t* pagePtr = sRegion.AllocPagePtr(index);
|
|
uint8_t* newPtr = pagePtr + kPhcPageSize - newUsableSize;
|
|
memmove(newPtr, aOldPtr, std::min(oldUsableSize, aNewSize));
|
|
ResizePageInUse(index, aArenaId, newPtr, stack);
|
|
Log("PageRealloc-Reuse(%p, %zu) -> %p\n", aOldPtr, aNewSize, newPtr);
|
|
return Some(newPtr);
|
|
}
|
|
|
|
// A page-to-normal transition (with the new size greater than page-sized).
|
|
// (Note that aArenaId is checked below.)
|
|
void* newPtr;
|
|
if (aArenaId.isSome()) {
|
|
newPtr = MozJemalloc::moz_arena_malloc(*aArenaId, aNewSize);
|
|
} else {
|
|
Maybe<arena_id_t> oldArenaId = PageArena(index);
|
|
newPtr = (oldArenaId.isSome()
|
|
? MozJemalloc::moz_arena_malloc(*oldArenaId, aNewSize)
|
|
: MozJemalloc::malloc(aNewSize));
|
|
}
|
|
if (!newPtr) {
|
|
return Some(nullptr);
|
|
}
|
|
|
|
Delay reuseDelay = ReuseDelay();
|
|
|
|
// Copy the usable size rather than the requested size, because the user
|
|
// might have used malloc_usable_size() and filled up the usable size. Note
|
|
// that FreePage() checks aArenaId (via SetPageFreed()).
|
|
size_t oldUsableSize = PageUsableSize(index);
|
|
memcpy(newPtr, aOldPtr, std::min(oldUsableSize, aNewSize));
|
|
FreePage(index, aArenaId, stack, reuseDelay);
|
|
Log("PageRealloc-Free(%p[%zu], %zu) -> %p, %zu delay, reuse at ~%zu\n",
|
|
aOldPtr, index, aNewSize, newPtr, size_t(reuseDelay),
|
|
size_t(Now()) + reuseDelay);
|
|
|
|
return Some(newPtr);
|
|
}
|
|
|
|
MOZ_ALWAYS_INLINE static void* PageRealloc(const Maybe<arena_id_t>& aArenaId,
|
|
void* aOldPtr, size_t aNewSize) {
|
|
Maybe<void*> ptr = MaybePageRealloc(aArenaId, aOldPtr, aNewSize);
|
|
|
|
return ptr.isSome()
|
|
? *ptr
|
|
: (aArenaId.isSome() ? MozJemalloc::moz_arena_realloc(
|
|
*aArenaId, aOldPtr, aNewSize)
|
|
: MozJemalloc::realloc(aOldPtr, aNewSize));
|
|
}
|
|
|
|
inline void* MozJemallocPHC::realloc(void* aOldPtr, size_t aNewSize) {
|
|
return PageRealloc(Nothing(), aOldPtr, aNewSize);
|
|
}
|
|
|
|
void PHC::PageFree(const Maybe<arena_id_t>& aArenaId, void* aPtr)
|
|
MOZ_EXCLUDES(mMutex) {
|
|
PtrKind pk = GetPtrKind(aPtr);
|
|
|
|
if (pk.IsGuardPage()) {
|
|
PHC::CrashOnGuardPage(aPtr);
|
|
}
|
|
|
|
AdvanceNow(LocalAllocDelay());
|
|
|
|
// Note that `disable` has no effect unless it is emplaced below.
|
|
Maybe<AutoDisableOnCurrentThread> disable;
|
|
// Get the stack trace *before* locking the mutex.
|
|
StackTrace freeStack;
|
|
if (IsDisabledOnCurrentThread()) {
|
|
// PHC is disabled on this thread. Leave the stack empty.
|
|
} else {
|
|
// Disable on this thread *before* getting the stack trace.
|
|
disable.emplace();
|
|
freeStack.Fill();
|
|
}
|
|
|
|
MutexAutoLock lock(mMutex);
|
|
|
|
Maybe<uintptr_t> mb_index = pk.AllocPageIndex(NumAllocPages());
|
|
if (!mb_index) {
|
|
Crash("free of invalid pointer");
|
|
}
|
|
// At this point we know we have an allocation page.
|
|
uintptr_t index = mb_index.value();
|
|
|
|
// Check for a double-free.
|
|
EnsureValidAndInUse(aPtr, index);
|
|
|
|
// Note that FreePage() checks aArenaId (via SetPageFreed()).
|
|
Delay reuseDelay = ReuseDelay();
|
|
FreePage(index, aArenaId, freeStack, reuseDelay);
|
|
|
|
#if PHC_LOGGING
|
|
phc::PHCStats stats = GetPageStatsLocked();
|
|
Log("PageFree(%p[%zu]), %zu delay, reuse at ~%zu, fullness %zu/%zu/%zu\n",
|
|
aPtr, index, size_t(reuseDelay), size_t(Now()) + reuseDelay,
|
|
stats.mSlotsAllocated, stats.mSlotsFreed, NumAllocPages());
|
|
#endif
|
|
}
|
|
|
|
MOZ_ALWAYS_INLINE static void PageFree(const Maybe<arena_id_t>& aArenaId,
|
|
void* aPtr) {
|
|
if (MOZ_UNLIKELY(FastIsPHCPtr(aPtr))) {
|
|
// The tenery expression here helps the compiler optimise away the
|
|
// construction of Nothing() in the caller.
|
|
PHC::sPHC->PageFree(aArenaId.isSome() ? aArenaId : Nothing(), aPtr);
|
|
return;
|
|
}
|
|
|
|
aArenaId.isSome() ? MozJemalloc::moz_arena_free(*aArenaId, aPtr)
|
|
: MozJemalloc::free(aPtr);
|
|
}
|
|
|
|
inline void MozJemallocPHC::free(void* aPtr) { PageFree(Nothing(), aPtr); }
|
|
|
|
// This handles memalign and moz_arena_memalign.
|
|
MOZ_ALWAYS_INLINE static void* PageMemalign(const Maybe<arena_id_t>& aArenaId,
|
|
size_t aAlignment,
|
|
size_t aReqSize) {
|
|
MOZ_RELEASE_ASSERT(std::has_single_bit(aAlignment));
|
|
|
|
// PHC can't satisfy an alignment greater than a page size, so fall back to
|
|
// mozjemalloc in that case.
|
|
void* ptr = nullptr;
|
|
if (ShouldPageAllocHot(aReqSize) && aAlignment <= kPhcPageSize) {
|
|
// The test on aArenaId here helps the compiler optimise away
|
|
// the construction of Nothing() in the caller.
|
|
ptr = PHC::sPHC->MaybePageAlloc(aArenaId.isSome() ? aArenaId : Nothing(),
|
|
aReqSize, aAlignment, /* aZero */ false);
|
|
}
|
|
return ptr ? ptr
|
|
: (aArenaId.isSome()
|
|
? MozJemalloc::moz_arena_memalign(*aArenaId, aAlignment,
|
|
aReqSize)
|
|
: MozJemalloc::memalign(aAlignment, aReqSize));
|
|
}
|
|
|
|
inline void* MozJemallocPHC::memalign(size_t aAlignment, size_t aReqSize) {
|
|
return PageMemalign(Nothing(), aAlignment, aReqSize);
|
|
}
|
|
|
|
inline size_t MozJemallocPHC::malloc_usable_size(usable_ptr_t aPtr) {
|
|
if (MOZ_LIKELY(!FastIsPHCPtr(aPtr))) {
|
|
// Not a page allocation. Measure it normally.
|
|
return MozJemalloc::malloc_usable_size(aPtr);
|
|
}
|
|
|
|
return PHC::sPHC->PtrUsableSize(aPtr);
|
|
}
|
|
|
|
size_t PHC::PtrUsableSize(usable_ptr_t aPtr) MOZ_EXCLUDES(mMutex) {
|
|
PtrKind pk = GetPtrKind(aPtr);
|
|
|
|
if (pk.IsGuardPage()) {
|
|
CrashOnGuardPage(const_cast<void*>(aPtr));
|
|
}
|
|
|
|
MutexAutoLock lock(mMutex);
|
|
|
|
Maybe<uintptr_t> index = pk.AllocPageIndex(NumAllocPages());
|
|
if (!index) {
|
|
Crash("PtrUsableSize() of invalid pointer");
|
|
}
|
|
|
|
// At this point we know aPtr lands within an allocation page. But if aPtr
|
|
// points to memory before the base address of the allocation, we return 0.
|
|
void* pageBaseAddr = AllocPageBaseAddr(index.value());
|
|
|
|
if (MOZ_UNLIKELY(aPtr < pageBaseAddr)) {
|
|
return 0;
|
|
}
|
|
|
|
return PageUsableSize(index.value());
|
|
}
|
|
|
|
inline void MozJemallocPHC::jemalloc_stats_internal(
|
|
jemalloc_stats_t* aStats, jemalloc_bin_stats_t* aBinStats) {
|
|
MozJemalloc::jemalloc_stats_internal(aStats, aBinStats);
|
|
|
|
if (!PHC::sPHC) {
|
|
// If we're not initialised, then we're not using any additional memory and
|
|
// have nothing to add to the report.
|
|
return;
|
|
}
|
|
|
|
// Add PHC's memory usage to the allocator's.
|
|
phc::MemoryUsage mem_info;
|
|
PHC::sPHC->GetMemoryUsage(mem_info);
|
|
aStats->allocated += mem_info.mAllocatedBytes;
|
|
aStats->waste += mem_info.mFragmentationBytes;
|
|
aStats->mapped += PHC::sRegion.ReservedBytes() - mem_info.mAllocatedBytes -
|
|
mem_info.mFragmentationBytes;
|
|
|
|
// guards is the gap between `allocated` and `mapped`. In some ways this
|
|
// almost fits into aStats->wasted since it feels like wasted memory. However
|
|
// wasted should only include committed memory and these guard pages are
|
|
// uncommitted. Therefore we don't include it anywhere.
|
|
// size_t guards = mapped - allocated;
|
|
|
|
// aStats.page_cache and aStats.bin_unused are left unchanged because PHC
|
|
// doesn't have anything corresponding to those.
|
|
|
|
// The metadata is `bookkeeping`.
|
|
aStats->bookkeeping += mem_info.mMetadataBytes;
|
|
}
|
|
|
|
inline void MozJemallocPHC::jemalloc_stats_lite(jemalloc_stats_lite_t* aStats) {
|
|
MozJemalloc::jemalloc_stats_lite(aStats);
|
|
}
|
|
|
|
inline void MozJemallocPHC::jemalloc_ptr_info(const void* aPtr,
|
|
jemalloc_ptr_info_t* aInfo) {
|
|
if (MOZ_LIKELY(!FastIsPHCPtr(aPtr))) {
|
|
// Not a page allocation.
|
|
MozJemalloc::jemalloc_ptr_info(aPtr, aInfo);
|
|
return;
|
|
}
|
|
|
|
PHC::sPHC->PagePtrInfo(aPtr, aInfo);
|
|
}
|
|
|
|
void PHC::PagePtrInfo(const void* aPtr, jemalloc_ptr_info_t* aInfo)
|
|
MOZ_EXCLUDES(mMutex) {
|
|
// We need to implement this properly, because various code locations do
|
|
// things like checking that allocations are in the expected arena.
|
|
|
|
PtrKind pk = GetPtrKind(aPtr);
|
|
|
|
if (pk.IsGuardPage()) {
|
|
// Treat a guard page as unknown because there's no better alternative.
|
|
*aInfo = {TagUnknown, nullptr, 0, 0};
|
|
return;
|
|
}
|
|
|
|
MutexAutoLock lock(mMutex);
|
|
|
|
// At this point we know we have an allocation page.
|
|
Maybe<uintptr_t> index = pk.AllocPageIndex(NumAllocPages());
|
|
|
|
if (!index) {
|
|
Crash("JemallocPtrInfo of invalid pointer");
|
|
}
|
|
|
|
FillJemallocPtrInfo(aPtr, index.value(), aInfo);
|
|
#if DEBUG
|
|
Log("JemallocPtrInfo(%p[%zu]) -> {%zu, %p, %zu, %zu}\n", aPtr, index.value(),
|
|
size_t(aInfo->tag), aInfo->addr, aInfo->size, aInfo->arenaId);
|
|
#else
|
|
Log("JemallocPtrInfo(%p[%zu]) -> {%zu, %p, %zu}\n", aPtr, index.value(),
|
|
size_t(aInfo->tag), aInfo->addr, aInfo->size);
|
|
#endif
|
|
}
|
|
|
|
inline void* MozJemallocPHC::moz_arena_malloc(arena_id_t aArenaId,
|
|
size_t aReqSize) {
|
|
return PageMalloc(Some(aArenaId), aReqSize);
|
|
}
|
|
|
|
inline void* MozJemallocPHC::moz_arena_calloc(arena_id_t aArenaId, size_t aNum,
|
|
size_t aReqSize) {
|
|
return PageCalloc(Some(aArenaId), aNum, aReqSize);
|
|
}
|
|
|
|
inline void* MozJemallocPHC::moz_arena_realloc(arena_id_t aArenaId,
|
|
void* aOldPtr, size_t aNewSize) {
|
|
return PageRealloc(Some(aArenaId), aOldPtr, aNewSize);
|
|
}
|
|
|
|
inline void MozJemallocPHC::moz_arena_free(arena_id_t aArenaId, void* aPtr) {
|
|
return PageFree(Some(aArenaId), aPtr);
|
|
}
|
|
|
|
inline void* MozJemallocPHC::moz_arena_memalign(arena_id_t aArenaId,
|
|
size_t aAlignment,
|
|
size_t aReqSize) {
|
|
return PageMemalign(Some(aArenaId), aAlignment, aReqSize);
|
|
}
|
|
|
|
bool PHC::IsPHCAllocation(const void* aPtr, mozilla::phc::AddrInfo* aOut) {
|
|
PtrKind pk = GetPtrKind(aPtr);
|
|
|
|
bool isGuardPage = false;
|
|
if (pk.IsGuardPage()) {
|
|
if ((uintptr_t(aPtr) % kPhcPageSize) < (kPhcPageSize / 2)) {
|
|
// The address is in the lower half of a guard page, so it's probably an
|
|
// overflow. But first check that it is not on the very first guard
|
|
// page, in which case it cannot be an overflow, and we ignore it.
|
|
if (sRegion.IsInFirstGuardPage(aPtr)) {
|
|
return false;
|
|
}
|
|
|
|
// Get the allocation page preceding this guard page.
|
|
pk = GetPtrKind(static_cast<const uint8_t*>(aPtr) - kPhcPageSize);
|
|
|
|
} else {
|
|
// The address is in the upper half of a guard page, so it's probably an
|
|
// underflow. Get the allocation page following this guard page.
|
|
pk = GetPtrKind(static_cast<const uint8_t*>(aPtr) + kPhcPageSize);
|
|
}
|
|
|
|
// Make a note of the fact that we hit a guard page.
|
|
isGuardPage = true;
|
|
}
|
|
|
|
if (aOut) {
|
|
if (mMutex.TryLock()) {
|
|
// At this point we know we have an allocation page.
|
|
Maybe<uintptr_t> index = pk.AllocPageIndex(NumAllocPages());
|
|
if (!index) {
|
|
mMutex.Unlock();
|
|
return false;
|
|
}
|
|
FillAddrInfo(index.value(), aPtr, isGuardPage, *aOut);
|
|
Log("IsPHCAllocation: %zu, %p, %zu, %zu, %zu\n", size_t(aOut->mKind),
|
|
aOut->mBaseAddr, aOut->mUsableSize,
|
|
aOut->mAllocStack.isSome() ? aOut->mAllocStack->mLength : 0,
|
|
aOut->mFreeStack.isSome() ? aOut->mFreeStack->mLength : 0);
|
|
mMutex.Unlock();
|
|
} else {
|
|
Log("IsPHCAllocation: PHC is locked\n");
|
|
aOut->mPhcWasLocked = true;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
namespace mozilla::phc {
|
|
|
|
bool IsPHCAllocation(const void* aPtr, AddrInfo* aOut) {
|
|
if (MOZ_LIKELY(!FastIsPHCPtr(aPtr))) {
|
|
return false;
|
|
}
|
|
|
|
return PHC::sPHC->IsPHCAllocation(aPtr, aOut);
|
|
}
|
|
|
|
void DisablePHCOnCurrentThread() {
|
|
PHC::DisableOnCurrentThread();
|
|
Log("DisablePHCOnCurrentThread: %zu\n", 0ul);
|
|
}
|
|
|
|
void ReenablePHCOnCurrentThread() {
|
|
PHC::sPHC->EnableOnCurrentThread();
|
|
Log("ReenablePHCOnCurrentThread: %zu\n", 0ul);
|
|
}
|
|
|
|
bool IsPHCEnabledOnCurrentThread() {
|
|
bool enabled = !PHC::IsDisabledOnCurrentThread();
|
|
Log("IsPHCEnabledOnCurrentThread: %zu\n", size_t(enabled));
|
|
return enabled;
|
|
}
|
|
|
|
void PHCMemoryUsage(MemoryUsage& aMemoryUsage) {
|
|
aMemoryUsage = MemoryUsage();
|
|
if (PHC::sPHC) {
|
|
PHC::sPHC->GetMemoryUsage(aMemoryUsage);
|
|
}
|
|
}
|
|
|
|
void SetPHCSize(size_t aSizeBytes) {
|
|
if (PHC::sPHC) {
|
|
PHC::sPHC->Resize(aSizeBytes);
|
|
}
|
|
}
|
|
|
|
void GetPHCStats(PHCStats& aStats) {
|
|
if (!PHC::sPHC) {
|
|
aStats = PHCStats();
|
|
return;
|
|
}
|
|
|
|
aStats = PHC::sPHC->GetPageStats();
|
|
}
|
|
|
|
// Enable or Disable PHC at runtime. If PHC is disabled it will still trap
|
|
// bad uses of previous allocations, but won't track any new allocations.
|
|
void SetPHCState(PHCState aState) {
|
|
if (!PHC::sPHC) {
|
|
return;
|
|
}
|
|
|
|
PHC::sPHC->SetState(aState);
|
|
}
|
|
|
|
void SetPHCProbabilities(int64_t aAvgDelayFirst, int64_t aAvgDelayNormal,
|
|
int64_t aAvgDelayPageReuse) {
|
|
if (!PHC::sPHC) {
|
|
return;
|
|
}
|
|
|
|
PHC::sPHC->SetProbabilities(aAvgDelayFirst, aAvgDelayNormal,
|
|
aAvgDelayPageReuse);
|
|
}
|
|
|
|
} // namespace mozilla::phc
|
|
|
|
#if PHC_LOGGING
|
|
static size_t GetPid() { return size_t(getpid()); }
|
|
|
|
static size_t GetTid() {
|
|
# if defined(XP_WIN)
|
|
return size_t(GetCurrentThreadId());
|
|
# else
|
|
return size_t(pthread_self());
|
|
# endif
|
|
}
|
|
#endif // PHC_LOGGING
|
|
|
|
static void Log(const char* fmt, ...) {
|
|
#if PHC_LOGGING
|
|
# if defined(XP_WIN)
|
|
# define LOG_STDERR \
|
|
reinterpret_cast<intptr_t>(GetStdHandle(STD_ERROR_HANDLE))
|
|
# else
|
|
# define LOG_STDERR 2
|
|
# endif
|
|
|
|
char buf[256];
|
|
size_t pos = SNPrintf(buf, sizeof(buf), "PHC[%zu,%zu,~%zu] ", GetPid(),
|
|
GetTid(), size_t(PHC::Now()));
|
|
va_list vargs;
|
|
va_start(vargs, fmt);
|
|
pos += VSNPrintf(&buf[pos], sizeof(buf) - pos, fmt, vargs);
|
|
MOZ_ASSERT(pos < sizeof(buf));
|
|
va_end(vargs);
|
|
|
|
FdPuts(LOG_STDERR, buf, pos);
|
|
#endif // PHC_LOGGING
|
|
}
|