js/src/ds/PageProtectingVector.h
author Mihai Alexandru Michis <malexandru@mozilla.com>
Thu, 24 Sep 2020 10:52:40 +0300
changeset 550100 7b98b01d238e1581d109397b51ba3356568e6566
parent 480654 1621ba12b29232e3eafa61a6c0e1bb0770147554
permissions -rw-r--r--
Backed out 2 changesets (bug 1666373, bug 1665802) for causing gv-junit and xpcshell failures. Backed out changeset 296c8ba967c9 (bug 1666373) Backed out changeset c1fe3374ab97 (bug 1665802)

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * vim: set ts=8 sts=2 et sw=2 tw=80:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifndef ds_PageProtectingVector_h
#define ds_PageProtectingVector_h

#include "mozilla/Atomics.h"
#include "mozilla/IntegerPrintfMacros.h"
#include "mozilla/PodOperations.h"
#include "mozilla/Types.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, restricts
 * access to memory pages that haven't been used yet, 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. On Nightly and Aurora, these crashes will additionally be
 * annotated with a moz crash reason using MemoryProtectionExceptionHandler.
 *
 * PageProtectingVector's protection is limited to full pages. If the front
 * of its buffer is not aligned on a page boundary, elems 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,
          bool ProtectUsed = true, bool ProtectUnused = true,
          size_t InitialLowerBound = 0, bool PoisonUnused = true,
          uint8_t PoisonPattern = 0xe3>
class PageProtectingVector final {
  mozilla::Vector<T, MinInlineCapacity, AllocPolicy> vector;

  static constexpr size_t toShift(size_t v) {
    return v <= 1 ? 0 : 1 + toShift(v >> 1);
  }

  static_assert(
      (sizeof(T) & (sizeof(T) - 1)) == 0,
      "For performance reasons, "
      "PageProtectingVector only works with power-of-2 sized elements!");

  static const size_t elemShift = toShift(sizeof(T));
  static const size_t elemSize = 1 << elemShift;
  static const size_t elemMask = elemSize - 1;

  /* We hardcode the page size here to minimize administrative overhead. */
  static const size_t pageShift = 12;
  static const size_t pageSize = 1 << pageShift;
  static const size_t pageMask = pageSize - 1;

  /*
   * The number of elements that can be added before we need to either adjust
   * the active page or resize the buffer. If |elemsUntilTest < 0| we will
   * take the slow paths in the append calls.
   */
  intptr_t elemsUntilTest;

  /*
   * The offset of the currently 'active' page - that is, the page that is
   * currently being written to. If both used and unused bytes are protected,
   * this will be the only (fully owned) page with read and write access.
   */
  size_t currPage;

  /*
   * The first fully owned page. This is the first page that can
   * be protected, but it may not be the first *active* page.
   */
  size_t initPage;

  /*
   * The last fully owned page. This is the last page that can
   * be protected, but it may not be the last *active* page.
   */
  size_t lastPage;

  /*
   * The size in elems 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 lowerBound;

#ifdef DEBUG
  bool regionUnprotected;
#endif

  bool usable;
  bool enabled;
  bool protectUsedEnabled;
  bool protectUnusedEnabled;

  MOZ_ALWAYS_INLINE void resetTest() {
    MOZ_ASSERT(protectUsedEnabled || protectUnusedEnabled);
    size_t nextPage =
        (pageSize - (uintptr_t(begin() + length()) & pageMask)) >> elemShift;
    size_t nextResize = capacity() - length();
    if (MOZ_LIKELY(nextPage <= nextResize)) {
      elemsUntilTest = intptr_t(nextPage);
    } else {
      elemsUntilTest = intptr_t(nextResize);
    }
  }

  MOZ_ALWAYS_INLINE void setTestInitial() {
    if (MOZ_LIKELY(!protectUsedEnabled && !protectUnusedEnabled)) {
      elemsUntilTest = intptr_t(capacity() - length());
    } else {
      resetTest();
    }
  }

  MOZ_ALWAYS_INLINE void resetForNewBuffer() {
    initPage = (uintptr_t(begin() - 1) >> pageShift) + 1;
    currPage = (uintptr_t(begin() + length()) >> pageShift);
    lastPage = (uintptr_t(begin() + capacity()) >> pageShift) - 1;
    protectUsedEnabled =
        ProtectUsed && usable && enabled && initPage <= lastPage &&
        (uintptr_t(begin()) & elemMask) == 0 && capacity() >= lowerBound;
    protectUnusedEnabled =
        ProtectUnused && usable && enabled && initPage <= lastPage &&
        (uintptr_t(begin()) & elemMask) == 0 && capacity() >= lowerBound;
    setTestInitial();
  }

  MOZ_ALWAYS_INLINE void poisonNewBuffer() {
    if (!PoisonUnused) {
      return;
    }
    T* addr = begin() + length();
    size_t toPoison = (capacity() - length()) * sizeof(T);
    memset(addr, PoisonPattern, toPoison);
  }

  MOZ_ALWAYS_INLINE void addExceptionHandler() {
    if (MOZ_UNLIKELY(protectUsedEnabled || protectUnusedEnabled)) {
      MemoryProtectionExceptionHandler::addRegion(begin(), capacity()
                                                               << elemShift);
    }
  }

  MOZ_ALWAYS_INLINE void removeExceptionHandler() {
    if (MOZ_UNLIKELY(protectUsedEnabled || protectUnusedEnabled)) {
      MemoryProtectionExceptionHandler::removeRegion(begin());
    }
  }

  MOZ_ALWAYS_INLINE void protectUsed() {
    if (MOZ_LIKELY(!protectUsedEnabled)) {
      return;
    }
    if (MOZ_UNLIKELY(currPage <= initPage)) {
      return;
    }
    T* addr = reinterpret_cast<T*>(initPage << pageShift);
    size_t size = (currPage - initPage) << pageShift;
    gc::MakePagesReadOnly(addr, size);
  }

  MOZ_ALWAYS_INLINE void unprotectUsed() {
    if (MOZ_LIKELY(!protectUsedEnabled)) {
      return;
    }
    if (MOZ_UNLIKELY(currPage <= initPage)) {
      return;
    }
    T* addr = reinterpret_cast<T*>(initPage << pageShift);
    size_t size = (currPage - initPage) << pageShift;
    gc::UnprotectPages(addr, size);
  }

  MOZ_ALWAYS_INLINE void protectUnused() {
    if (MOZ_LIKELY(!protectUnusedEnabled)) {
      return;
    }
    if (MOZ_UNLIKELY(currPage >= lastPage)) {
      return;
    }
    T* addr = reinterpret_cast<T*>((currPage + 1) << pageShift);
    size_t size = (lastPage - currPage) << pageShift;
    gc::ProtectPages(addr, size);
  }

  MOZ_ALWAYS_INLINE void unprotectUnused() {
    if (MOZ_LIKELY(!protectUnusedEnabled)) {
      return;
    }
    if (MOZ_UNLIKELY(currPage >= lastPage)) {
      return;
    }
    T* addr = reinterpret_cast<T*>((currPage + 1) << pageShift);
    size_t size = (lastPage - currPage) << pageShift;
    gc::UnprotectPages(addr, size);
  }

  MOZ_ALWAYS_INLINE void protectNewBuffer() {
    resetForNewBuffer();
    addExceptionHandler();
    poisonNewBuffer();
    protectUsed();
    protectUnused();
  }

  MOZ_ALWAYS_INLINE void unprotectOldBuffer() {
    MOZ_ASSERT(!regionUnprotected);
    unprotectUnused();
    unprotectUsed();
    removeExceptionHandler();
  }

  MOZ_ALWAYS_INLINE void protectUnusedPartial(size_t curr, size_t next) {
    if (MOZ_LIKELY(!protectUnusedEnabled)) {
      return;
    }
    if (MOZ_UNLIKELY(next > lastPage)) {
      --next;
    }
    if (MOZ_UNLIKELY(next == curr)) {
      return;
    }
    void* addr = reinterpret_cast<T*>((curr + 1) << pageShift);
    size_t size = (next - curr) << pageShift;
    gc::ProtectPages(addr, size);
  }

  MOZ_ALWAYS_INLINE void unprotectUnusedPartial(size_t curr, size_t next) {
    if (MOZ_LIKELY(!protectUnusedEnabled)) {
      return;
    }
    if (MOZ_UNLIKELY(next > lastPage)) {
      --next;
    }
    if (MOZ_UNLIKELY(next == curr)) {
      return;
    }
    void* addr = reinterpret_cast<T*>((curr + 1) << pageShift);
    size_t size = (next - curr) << pageShift;
    gc::UnprotectPages(addr, size);
  }

  MOZ_ALWAYS_INLINE void protectUsedPartial(size_t curr, size_t next) {
    if (MOZ_LIKELY(!protectUsedEnabled)) {
      return;
    }
    if (MOZ_UNLIKELY(curr < initPage)) {
      ++curr;
    }
    if (MOZ_UNLIKELY(next == curr)) {
      return;
    }
    void* addr = reinterpret_cast<T*>(curr << pageShift);
    size_t size = (next - curr) << pageShift;
    gc::MakePagesReadOnly(addr, size);
  }

  MOZ_ALWAYS_INLINE MOZ_MUST_USE bool reserveNewBuffer(size_t size) {
    unprotectOldBuffer();
    bool ret = vector.reserve(size);
    protectNewBuffer();
    return ret;
  }

  template <typename U>
  MOZ_ALWAYS_INLINE void infallibleAppendNewPage(const U* values, size_t size) {
    size_t nextPage = uintptr_t(begin() + length() + size) >> pageShift;
    MOZ_ASSERT(currPage < nextPage);
    unprotectUnusedPartial(currPage, nextPage);
    vector.infallibleAppend(values, size);
    protectUsedPartial(currPage, nextPage);
    currPage = nextPage;
    resetTest();
  }

  template <typename U>
  MOZ_ALWAYS_INLINE MOZ_MUST_USE bool appendNewPage(const U* values,
                                                    size_t size) {
    size_t nextPage = uintptr_t(begin() + length() + size) >> pageShift;
    MOZ_ASSERT(currPage < nextPage);
    unprotectUnusedPartial(currPage, nextPage);
    bool ret = vector.append(values, size);
    if (MOZ_LIKELY(ret)) {
      protectUsedPartial(currPage, nextPage);
      currPage = nextPage;
    } else {
      protectUnusedPartial(currPage, nextPage);
    }
    resetTest();
    return ret;
  }

  template <typename U>
  MOZ_ALWAYS_INLINE MOZ_MUST_USE bool appendNewBuffer(const U* values,
                                                      size_t size) {
    unprotectOldBuffer();
    bool ret = vector.append(values, size);
    protectNewBuffer();
    return ret;
  }

  MOZ_NEVER_INLINE void unprotectRegionSlow(uintptr_t l, uintptr_t r);
  MOZ_NEVER_INLINE void reprotectRegionSlow(uintptr_t l, uintptr_t r);

  MOZ_NEVER_INLINE MOZ_MUST_USE bool reserveSlow(size_t size);

  template <typename U>
  MOZ_NEVER_INLINE void infallibleAppendSlow(const U* values, size_t size);

  template <typename U>
  MOZ_NEVER_INLINE MOZ_MUST_USE bool appendSlow(const U* values, size_t size);

 public:
  explicit PageProtectingVector(AllocPolicy policy = AllocPolicy())
      : vector(std::move(policy)),
        elemsUntilTest(0),
        currPage(0),
        initPage(0),
        lastPage(0),
        lowerBound(InitialLowerBound),
#ifdef DEBUG
        regionUnprotected(false),
#endif
        usable(true),
        enabled(true),
        protectUsedEnabled(false),
        protectUnusedEnabled(false) {
    if (gc::SystemPageSize() != pageSize) {
      usable = false;
    }
    protectNewBuffer();
  }

  ~PageProtectingVector() { unprotectOldBuffer(); }

  void disableProtection() {
    MOZ_ASSERT(enabled);
    unprotectOldBuffer();
    enabled = false;
    resetForNewBuffer();
  }

  void enableProtection() {
    MOZ_ASSERT(!enabled);
    enabled = true;
    protectNewBuffer();
  }

  /*
   * Sets the lower bound on the size, in elems, that this vector's underlying
   * capacity has to be before its used pages will be protected.
   */
  void setLowerBoundForProtection(size_t elems) {
    if (lowerBound != elems) {
      unprotectOldBuffer();
      lowerBound = elems;
      protectNewBuffer();
    }
  }

  /* Disable protection on the smallest containing region. */
  MOZ_ALWAYS_INLINE void unprotectRegion(T* first, size_t size) {
#ifdef DEBUG
    regionUnprotected = true;
#endif
    if (MOZ_UNLIKELY(protectUsedEnabled)) {
      uintptr_t l = uintptr_t(first) >> pageShift;
      uintptr_t r = uintptr_t(first + size - 1) >> pageShift;
      if (r >= initPage && l < currPage) {
        unprotectRegionSlow(l, r);
      }
    }
  }

  /* Re-enable protection on the smallest containing region. */
  MOZ_ALWAYS_INLINE void reprotectRegion(T* first, size_t size) {
#ifdef DEBUG
    regionUnprotected = false;
#endif
    if (MOZ_UNLIKELY(protectUsedEnabled)) {
      uintptr_t l = uintptr_t(first) >> pageShift;
      uintptr_t r = uintptr_t(first + size - 1) >> pageShift;
      if (r >= initPage && l < currPage) {
        reprotectRegionSlow(l, r);
      }
    }
  }

  MOZ_ALWAYS_INLINE size_t capacity() const { return vector.capacity(); }
  MOZ_ALWAYS_INLINE size_t length() const { return vector.length(); }

  MOZ_ALWAYS_INLINE T* begin() { return vector.begin(); }
  MOZ_ALWAYS_INLINE const T* begin() const { return vector.begin(); }

  void clear() {
    unprotectOldBuffer();
    vector.clear();
    protectNewBuffer();
  }

  MOZ_ALWAYS_INLINE MOZ_MUST_USE bool reserve(size_t size) {
    if (MOZ_LIKELY(size <= capacity())) {
      return vector.reserve(size);
    }
    return reserveSlow(size);
  }

  template <typename U>
  MOZ_ALWAYS_INLINE void infallibleAppend(const U* values, size_t size) {
    elemsUntilTest -= size;
    if (MOZ_LIKELY(elemsUntilTest >= 0)) {
      return vector.infallibleAppend(values, size);
    }
    infallibleAppendSlow(values, size);
  }

  template <typename U>
  MOZ_ALWAYS_INLINE MOZ_MUST_USE bool append(const U* values, size_t size) {
    elemsUntilTest -= size;
    if (MOZ_LIKELY(elemsUntilTest >= 0)) {
      return vector.append(values, size);
    }
    return appendSlow(values, size);
  }
};

template <typename T, size_t A, class B, bool C, bool D, size_t E, bool F,
          uint8_t G>
MOZ_NEVER_INLINE void
PageProtectingVector<T, A, B, C, D, E, F, G>::unprotectRegionSlow(uintptr_t l,
                                                                  uintptr_t r) {
  if (l < initPage) {
    l = initPage;
  }
  if (r >= currPage) {
    r = currPage - 1;
  }
  T* addr = reinterpret_cast<T*>(l << pageShift);
  size_t size = (r - l + 1) << pageShift;
  gc::UnprotectPages(addr, size);
}

template <typename T, size_t A, class B, bool C, bool D, size_t E, bool F,
          uint8_t G>
MOZ_NEVER_INLINE void
PageProtectingVector<T, A, B, C, D, E, F, G>::reprotectRegionSlow(uintptr_t l,
                                                                  uintptr_t r) {
  if (l < initPage) {
    l = initPage;
  }
  if (r >= currPage) {
    r = currPage - 1;
  }
  T* addr = reinterpret_cast<T*>(l << pageShift);
  size_t size = (r - l + 1) << pageShift;
  gc::MakePagesReadOnly(addr, size);
}

template <typename T, size_t A, class B, bool C, bool D, size_t E, bool F,
          uint8_t G>
MOZ_NEVER_INLINE MOZ_MUST_USE bool
PageProtectingVector<T, A, B, C, D, E, F, G>::reserveSlow(size_t size) {
  return reserveNewBuffer(size);
}

template <typename T, size_t A, class B, bool C, bool D, size_t E, bool F,
          uint8_t G>
template <typename U>
MOZ_NEVER_INLINE void
PageProtectingVector<T, A, B, C, D, E, F, G>::infallibleAppendSlow(
    const U* values, size_t size) {
  // Ensure that we're here because we reached a page
  // boundary and not because of a buffer overflow.
  MOZ_RELEASE_ASSERT(
      MOZ_LIKELY(length() + size <= capacity()),
      "About to overflow our AssemblerBuffer using infallibleAppend!");
  infallibleAppendNewPage(values, size);
}

template <typename T, size_t A, class B, bool C, bool D, size_t E, bool F,
          uint8_t G>
template <typename U>
MOZ_NEVER_INLINE MOZ_MUST_USE bool
PageProtectingVector<T, A, B, C, D, E, F, G>::appendSlow(const U* values,
                                                         size_t size) {
  if (MOZ_LIKELY(length() + size <= capacity())) {
    return appendNewPage(values, size);
  }
  return appendNewBuffer(values, size);
}

} /* namespace js */

#endif /* ds_PageProtectingVector_h */