diff options
Diffstat (limited to 'js/src/ds/PageProtectingVector.h')
-rw-r--r-- | js/src/ds/PageProtectingVector.h | 257 |
1 files changed, 257 insertions, 0 deletions
diff --git a/js/src/ds/PageProtectingVector.h b/js/src/ds/PageProtectingVector.h new file mode 100644 index 000000000..86d52fc0c --- /dev/null +++ b/js/src/ds/PageProtectingVector.h @@ -0,0 +1,257 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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 */ |