Mypal/js/src/ds/PageProtectingVector.h

257 lines
8.7 KiB
C++

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef ds_PageProtectingVector_h
#define ds_PageProtectingVector_h
#include "mozilla/Vector.h"
#include "ds/MemoryProtectionExceptionHandler.h"
#include "gc/Memory.h"
namespace js {
/*
* PageProtectingVector is a vector that can only grow or be cleared, and marks
* all of its fully used memory pages as read-only. It can be used to detect
* heap corruption in important buffers, since anything that tries to write
* into its protected pages will crash.
*
* PageProtectingVector's protection is limited to full pages. If the front
* of its buffer is not aligned on a page boundary, bytes preceding the first
* page boundary will not be protected. Similarly, the end of the buffer will
* not be fully protected unless it is aligned on a page boundary. Altogether,
* up to two pages of memory may not be protected.
*/
template<typename T,
size_t MinInlineCapacity = 0,
class AllocPolicy = mozilla::MallocAllocPolicy>
class PageProtectingVector final
{
mozilla::Vector<T, MinInlineCapacity, AllocPolicy> vector;
size_t pageSize;
size_t pageMask;
/*
* The number of bytes between the start of the buffer being used by
* |vector| and the first page we can protect. With jemalloc, this number
* should always be 0 for vectors with a buffer larger than |pageSize / 2|
* bytes, but with other allocators large buffers may not be page-aligned.
*/
size_t offsetToPage;
/* The number of currently protected bytes (a multiple of pageSize). */
size_t protectedBytes;
/*
* The number of bytes that are currently unprotected, but could be.
* This number starts at |-offsetToPage|, since any bytes before
* |vector.begin() + offsetToPage| can never be protected (as we do not own
* the whole page). As a result, if |unprotectedBytes >= pageSize|, we know
* we can protect at least one more page, and |unprotectedBytes & ~pageMask|
* is always the number of additional bytes we can protect. Put another way,
* |offsetToPage + protectedBytes + unprotectedBytes == [size in bytes]|
* always holds, and if |protectedBytes != 0| then |unprotectedBytes >= 0|.
*/
intptr_t unprotectedBytes;
/*
* The size in bytes that a buffer needs to be before its pages will be
* protected. This is intended to reduce churn for small vectors while
* still offering protection when they grow large enough.
*/
size_t protectionLowerBound;
bool protectionEnabled;
bool regionUnprotected;
void updateOffsetToPage() {
unprotectedBytes += offsetToPage;
offsetToPage = (pageSize - (uintptr_t(vector.begin()) & pageMask)) & pageMask;
unprotectedBytes -= offsetToPage;
#if 0
protectionEnabled = vector.capacity() * sizeof(T) >= protectionLowerBound &&
vector.capacity() * sizeof(T) >= pageSize + offsetToPage;
#endif
}
void protect() {
if (!regionUnprotected && protectionEnabled && unprotectedBytes >= intptr_t(pageSize)) {
size_t toProtect = size_t(unprotectedBytes) & ~pageMask;
uintptr_t addr = uintptr_t(vector.begin()) + offsetToPage + protectedBytes;
gc::MakePagesReadOnly(reinterpret_cast<void*>(addr), toProtect);
unprotectedBytes -= toProtect;
protectedBytes += toProtect;
}
}
void unprotect() {
MOZ_ASSERT_IF(!protectionEnabled, !protectedBytes);
if (!regionUnprotected && protectedBytes) {
uintptr_t addr = uintptr_t(vector.begin()) + offsetToPage;
gc::UnprotectPages(reinterpret_cast<void*>(addr), protectedBytes);
unprotectedBytes += protectedBytes;
protectedBytes = 0;
}
}
void protectNewBuffer() {
updateOffsetToPage();
if (protectionEnabled)
MemoryProtectionExceptionHandler::addRegion(vector.begin(), vector.capacity() * sizeof(T));
protect();
}
void unprotectOldBuffer() {
if (protectionEnabled)
MemoryProtectionExceptionHandler::removeRegion(vector.begin());
unprotect();
}
bool anyProtected(size_t first, size_t last) {
return last >= offsetToPage && first < offsetToPage + protectedBytes;
}
void setContainingRegion(size_t first, size_t last, uintptr_t* addr, size_t* size) {
if (first < offsetToPage)
first = offsetToPage;
if (last > offsetToPage + protectedBytes - 1)
last = offsetToPage + protectedBytes - 1;
uintptr_t firstAddr = uintptr_t(vector.begin());
uintptr_t firstPage = (firstAddr + first) & ~pageMask;
uintptr_t lastPage = (firstAddr + last) & ~pageMask;
*size = pageSize + (lastPage - firstPage);
*addr = firstPage;
}
void increaseElemsUsed(size_t used) {
unprotectedBytes += used * sizeof(T);
protect();
}
/* A helper class to simplify unprotecting and reprotecting when needed. */
class AutoUnprotect
{
PageProtectingVector* vector;
public:
AutoUnprotect() : vector(nullptr) {};
void emplace(PageProtectingVector* holder) {
vector = holder;
vector->unprotectOldBuffer();
}
explicit AutoUnprotect(PageProtectingVector* holder) {
emplace(holder);
}
~AutoUnprotect() {
if (vector)
vector->protectNewBuffer();
}
};
public:
explicit PageProtectingVector(AllocPolicy policy = AllocPolicy())
: vector(policy),
pageSize(gc::SystemPageSize()),
pageMask(pageSize - 1),
offsetToPage(0),
protectedBytes(0),
unprotectedBytes(0),
protectionLowerBound(0),
protectionEnabled(false),
regionUnprotected(false) { protectNewBuffer(); }
~PageProtectingVector() { unprotectOldBuffer(); }
/*
* Sets the lower bound on the size, in bytes, that this vector's underlying
* capacity has to be before its used pages will be protected.
*/
void setLowerBoundForProtection(size_t bytes) {
if (protectionLowerBound != bytes) {
unprotectOldBuffer();
protectionLowerBound = bytes;
protectNewBuffer();
}
}
/*
* Disable protection on the smallest number of pages containing
* both |firstByteOffset| and |lastByteOffset|.
*/
void unprotectRegion(size_t firstByteOffset, size_t lastByteOffset) {
MOZ_ASSERT(!regionUnprotected);
regionUnprotected = true;
if (!protectedBytes || !anyProtected(firstByteOffset, lastByteOffset))
return;
size_t size;
uintptr_t addr;
setContainingRegion(firstByteOffset, lastByteOffset, &addr, &size);
gc::UnprotectPages(reinterpret_cast<void*>(addr), size);
}
/*
* Re-enable protection on the region containing
* |firstByteOffset| and |lastByteOffset|.
*/
void reprotectRegion(size_t firstByteOffset, size_t lastByteOffset) {
MOZ_ASSERT(regionUnprotected);
regionUnprotected = false;
if (!protectedBytes || !anyProtected(firstByteOffset, lastByteOffset))
return;
size_t size;
uintptr_t addr;
setContainingRegion(firstByteOffset, lastByteOffset, &addr, &size);
gc::MakePagesReadOnly(reinterpret_cast<void*>(addr), size);
}
size_t length() const { return vector.length(); }
T* begin() { return vector.begin(); }
const T* begin() const { return vector.begin(); }
void clear() {
AutoUnprotect guard(this);
vector.clear();
offsetToPage = 0;
unprotectedBytes = 0;
}
MOZ_MUST_USE bool reserve(size_t size) {
AutoUnprotect guard;
if (size > vector.capacity())
guard.emplace(this);
return vector.reserve(size);
}
template<typename U>
MOZ_ALWAYS_INLINE void infallibleAppend(const U* values, size_t size) {
vector.infallibleAppend(values, size);
increaseElemsUsed(size);
}
template<typename U>
MOZ_MUST_USE bool append(const U* values, size_t size) {
bool ret;
{
AutoUnprotect guard;
if (MOZ_UNLIKELY(vector.length() + size > vector.capacity()))
guard.emplace(this);
ret = vector.append(values, size);
}
if (ret)
increaseElemsUsed(size);
return ret;
}
};
} /* namespace js */
#endif /* ds_PageProtectingVector_h */