// Copyright 2021 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef V8_SANDBOX_SANDBOX_H_
#define V8_SANDBOX_SANDBOX_H_

#include "include/v8-internal.h"
#include "include/v8-platform.h"
#include "include/v8config.h"
#include "src/base/bounds.h"
#include "src/common/globals.h"

#if V8_ENABLE_WEBASSEMBLY
#include "src/trap-handler/trap-handler.h"
#endif  // V8_ENABLE_WEBASSEMBLY

#include "testing/gtest/include/gtest/gtest_prod.h"  // nogncheck

namespace v8 {
namespace internal {

#ifdef V8_ENABLE_SANDBOX

/**
 * The V8 Sandbox.
 *
 * When enabled, V8 reserves a large region of virtual address space - the
 * sandbox - and places most of its objects inside of it. It is then assumed
 * that an attacker can, by exploiting a vulnerability in V8, corrupt memory
 * inside the sandbox arbitrarily and from different threads. The sandbox
 * attempts to stop an attacker from corrupting other memory in the process.
 *
 * The sandbox relies on a number of different mechanisms to achieve its goal.
 * For example, objects inside the sandbox can reference each other through
 * offsets from the start of the sandbox ("sandboxed pointers") instead of raw
 * pointers, and external objects can be referenced through indices into a
 * per-Isolate table of external pointers ("sandboxed external pointers").
 *
 * The pointer compression region, which contains most V8 objects, and inside
 * of which compressed (32-bit) pointers are used, is located at the start of
 * the sandbox. The remainder of the sandbox is mostly used for memory
 * buffers, in particular ArrayBuffer backing stores and WASM memory cages.
 *
 * As the embedder is responsible for providing ArrayBuffer allocators, V8
 * exposes the virtual address space backing the sandbox to the embedder.
 */
class V8_EXPORT_PRIVATE Sandbox {
 public:
  // +-  ~~~  -+----------------------------------------  ~~~  -+-  ~~~  -+
  // |  32 GB  |                 (Ideally) 1 TB                 |  32 GB  |
  // |         |                                                |         |
  // | Guard   |      4 GB      :  ArrayBuffer backing stores,  | Guard   |
  // | Region  |    V8 Heap     :  WASM memory buffers, and     | Region  |
  // | (front) |     Region     :  any other sandboxed objects. | (back)  |
  // +-  ~~~  -+----------------+-----------------------  ~~~  -+-  ~~~  -+
  //           ^                                                ^
  //           base                                             end
  //           < - - - - - - - - - - - size - - - - - - - - - - >
  // < - - - - - - - - - - - - - reservation_size - - - - - - - - - - - - >

  Sandbox() = default;

  Sandbox(const Sandbox&) = delete;
  Sandbox& operator=(Sandbox&) = delete;

  /*
   * Currently, if not enough virtual memory can be reserved for the sandbox,
   * we will fall back to a partially-reserved sandbox. This constant can be
   * used to determine if this fall-back is enabled.
   * */
  static constexpr bool kFallbackToPartiallyReservedSandboxAllowed = true;

  // The name for the virtual address space reservation backing the sandbox.
  static constexpr const char* kSandboxAddressSpaceName = "v8-sandbox";

  static constexpr size_t kSmiAddressRange = 4UL * GB;

  // We assume that the Smi<->HeapObject corruption can lead to accesses of
  // in-object properties. We add some padding to also catch these kinds of
  // accesses.
  static constexpr size_t kSmiAddressRangePadding = 4 * KB;

  // A heuristic used by the sandbox crash filter to identify crashes on
  // unaddressable accesses (e.g. on ARM64). Note that this is only a testing
  // heuristic and does not reflect the actual virtual address space size,
  // which is determined dynamically during sandbox initialization.
  static constexpr int kMaxVirtualAddressBitsForCrashFilter = 48;

  /**
   * Initializes this sandbox.
   *
   * This will allocate the virtual address subspace for the sandbox inside the
   * provided virtual address space. If a subspace of the required size cannot
   * be allocated, this method will instead initialize this sandbox as a
   * partially-reserved sandbox. In that case, a smaller virtual address space
   * reservation will be used and an EmulatedVirtualAddressSubspace instance
   * will be created on top of it to back the sandbox. If not enough virtual
   * address space can be allocated for even a partially-reserved sandbox, then
   * this method will fail with an OOM crash.
   */
  void Initialize(v8::Platform* platform, v8::VirtualAddressSpace* vas);

  /**
   * Tear down this sandbox.
   *
   * This will free the virtual address subspace backing this sandbox.
   */
  void TearDown();

  /**
   * Returns true if this sandbox has been initialized successfully.
   */
  bool is_initialized() const { return initialized_; }

  /**
   * Returns true if this sandbox is a partially-reserved sandbox.
   *
   * A partially-reserved sandbox is backed by a virtual address space
   * reservation that is smaller than its size. It also does not have guard
   * regions surrounding it. A partially-reserved sandbox is usually created if
   * not enough virtual address space could be reserved for the sandbox during
   * initialization. In such a configuration, unrelated memory mappings may end
   * up inside the sandbox, which affects its security properties.
   */
  bool is_partially_reserved() const { return reservation_size_ < size_; }

  /**
   * Returns true if the first four GB of the address space are inaccessible.
   *
   * During initialization, the sandbox will also attempt to create an
   * inaccessible mapping in the first four GB of the address space. This is
   * useful to mitigate Smi<->HeapObject confusion issues, in which a (32-bit)
   * Smi is treated as a pointer and dereferenced.
   */
  bool smi_address_range_is_inaccessible() const {
    return smi_address_range_reserved_;
  }

  /**
   * The base address of the sandbox.
   *
   * This is the start of the address space region that is directly addressable
   * by V8. In practice, this means the start of the part of the sandbox
   * address space between the surrounding guard regions.
   */
  Address base() const { return base_; }

  /**
   * The address right after the end of the sandbox.
   *
   * This is equal to |base| + |size|.
   */
  Address end() const { return end_; }

  /**
   * The size of the sandbox in bytes.
   */
  size_t size() const { return size_; }

  /**
   * The size of the virtual address space reservation backing the sandbox.
   *
   * This can be larger than |size| as it contains the surrounding guard
   * regions as well, or can be smaller than |size| in the case of a
   * partially-reserved sandbox.
   */
  size_t reservation_size() const { return reservation_size_; }

  /**
   * The virtual address subspace backing this sandbox.
   *
   * This can be used to allocate and manage memory pages inside the sandbox.
   */
  v8::VirtualAddressSpace* address_space() const {
    return address_space_.get();
  }

  /**
   * Returns a PageAllocator instance that allocates pages inside the sandbox.
   */
  v8::PageAllocator* page_allocator() const {
    return sandbox_page_allocator_.get();
  }

  /**
   * Returns a PageAllocator weak pointer instance that allocates pages inside
   * the sandbox. This version is for BackingStores that can outlive sandbox.
   */
  std::weak_ptr<v8::PageAllocator> page_allocator_weak() const {
    return sandbox_page_allocator_;
  }

  /**
   * Returns true if the given address lies within the sandbox address space.
   */
  bool Contains(Address addr) const {
    return base::IsInHalfOpenRange(addr, base_, base_ + size_);
  }

  /**
   * Returns true if the given pointer points into the sandbox address space.
   */
  bool Contains(void* ptr) const {
    return Contains(reinterpret_cast<Address>(ptr));
  }

  /**
   * Returns true if the given address lies within the sandbox reservation.
   *
   * This is a variant of Contains that checks whether the address lies within
   * the virtual address space reserved for the sandbox. In the case of a
   * fully-reserved sandbox (the default) this is essentially the same as
   * Contains but also includes the guard region. In the case of a
   * partially-reserved sandbox, this will only test against the address region
   * that was actually reserved.
   * This can be useful when checking that objects are *not* located within the
   * sandbox, as in the case of a partially-reserved sandbox, they may still
   * end up in the unreserved part.
   */
  bool ReservationContains(Address addr) const {
    return base::IsInHalfOpenRange(addr, reservation_base_,
                                   reservation_base_ + reservation_size_);
  }

  class SandboxedPointerConstants final {
   public:
    Address empty_backing_store_buffer() const {
      return empty_backing_store_buffer_;
    }
    Address empty_backing_store_buffer_address() const {
      return reinterpret_cast<Address>(&empty_backing_store_buffer_);
    }
    void set_empty_backing_store_buffer(Address value) {
      empty_backing_store_buffer_ = value;
    }

    void Reset() { empty_backing_store_buffer_ = 0; }

   private:
    Address empty_backing_store_buffer_ = 0;
  };
  const SandboxedPointerConstants& constants() const { return constants_; }

  Address base_address() const { return reinterpret_cast<Address>(&base_); }
  Address end_address() const { return reinterpret_cast<Address>(&end_); }
  Address size_address() const { return reinterpret_cast<Address>(&size_); }

  static void InitializeDefaultOncePerProcess(v8::Platform* platform,
                                              v8::VirtualAddressSpace* vas);
  static void TearDownDefault();

  // Create a new sandbox allocating a fresh pointer cage.
  // If new sandboxes cannot be created in this build configuration, abort.
  //
  static Sandbox* New(v8::Platform* platform, v8::VirtualAddressSpace* vas);

#ifdef V8_COMPRESS_POINTERS_IN_MULTIPLE_CAGES
#ifdef USING_V8_SHARED_PRIVATE
  static Sandbox* current() { return current_non_inlined(); }
  static void set_current(Sandbox* sandbox) {
    set_current_non_inlined(sandbox);
  }
#else   // !USING_V8_SHARED_PRIVATE
  static Sandbox* current() { return current_; }
  static void set_current(Sandbox* sandbox) { current_ = sandbox; }
#endif  // !USING_V8_SHARED_PRIVATE
#else   // !V8_COMPRESS_POINTERS_IN_MULTIPLE_CAGES
  static Sandbox* current() { return GetDefault(); }
#endif  // !V8_COMPRESS_POINTERS_IN_MULTIPLE_CAGES

  V8_INLINE static Sandbox* GetDefault() { return default_sandbox_; }

  // Allocator that can be used for in-sandbox allocations. The sandbox will
  // provide a default allocator which can be overridden with
  // `set_in_sandbox_allocator()`.
  Allocator* in_sandbox_allocator() { return in_sandbox_allocator_.get(); }

  // Sets a custom in-sandbox allocator that can be retrieved with
  // `in_sandbox_allocator()`.
  void set_in_sandbox_allocator(std::shared_ptr<Allocator> allocator);

 private:
  // The SequentialUnmapperTest calls the private Initialize method to create a
  // sandbox without guard regions, which would consume too much memory.
  friend class SequentialUnmapperTest;

  // These tests call the private Initialize methods below.
  FRIEND_TEST(SandboxTest, InitializationWithSize);
  FRIEND_TEST(SandboxTest, PartiallyReservedSandbox);

  // Default process-wide sandbox.
  static Sandbox* default_sandbox_;

  // We allow tests to disable the guard regions around the sandbox. This is
  // useful for example for tests like the SequentialUnmapperTest which track
  // page allocations and so would incur a large overhead from the guard
  // regions. The provided virtual address space must be able to allocate
  // subspaces. The size must be a multiple of the allocation granularity of the
  // virtual memory space.
  bool Initialize(v8::Platform* platform, v8::VirtualAddressSpace* vas,
                  size_t size, bool use_guard_regions);

  // Used when reserving virtual memory is too expensive. A partially reserved
  // sandbox does not reserve all of its virtual memory and so doesn't have the
  // desired security properties as unrelated mappings could end up inside of
  // it and be corrupted. The size and size_to_reserve parameters must be
  // multiples of the allocation granularity of the virtual address space.
  bool InitializeAsPartiallyReservedSandbox(v8::Platform* platform,
                                            v8::VirtualAddressSpace* vas,
                                            size_t size,
                                            size_t size_to_reserve);

  // Performs final initialization steps after the sandbox address space has
  // been initialized. Called from the two Initialize variants above.
  void FinishInitialization();

  // Initialize the constant objects for this sandbox.
  void InitializeConstants();

#ifdef V8_COMPRESS_POINTERS_IN_MULTIPLE_CAGES
  // These non-inlined accessors to current_ field are used in component builds
  // where cross-component access to thread local variables is not allowed.
  static Sandbox* current_non_inlined();
  static void set_current_non_inlined(Sandbox* sandbox);
#endif

  Address base_ = kNullAddress;
  Address end_ = kNullAddress;
  size_t size_ = 0;

  // Base and size of the virtual memory reservation backing this sandbox.
  // These can be different from the sandbox base and size due to guard regions
  // or when a partially-reserved sandbox is used.
  Address reservation_base_ = kNullAddress;
  size_t reservation_size_ = 0;

  bool initialized_ = false;

#if V8_ENABLE_WEBASSEMBLY && V8_TRAP_HANDLER_SUPPORTED
  bool trap_handler_initialized_ = false;
#endif

  // The virtual address subspace backing the sandbox.
  std::unique_ptr<v8::VirtualAddressSpace> address_space_;

  // The page allocator instance for this sandbox.
  std::shared_ptr<v8::PageAllocator> sandbox_page_allocator_;

  // An allocator that can be used to allocate in-sandbox memory.
  std::shared_ptr<v8::Allocator> in_sandbox_allocator_;

  // Constant objects inside this sandbox.
  SandboxedPointerConstants constants_;

  // Besides the address space reservation for the sandbox, we also try to
  // reserve the first four gigabytes of the virtual address space (with an
  // inaccessible mapping). This for example mitigates Smi<->HeapObject
  // confusion bugs in which we treat a Smi value as a pointer and access it.
  static bool smi_address_range_reserved_;

#ifdef V8_COMPRESS_POINTERS_IN_MULTIPLE_CAGES
  thread_local static Sandbox* current_;
#endif  // V8_COMPRESS_POINTERS_IN_MULTIPLE_CAGES
};

#endif  // V8_ENABLE_SANDBOX

// Helper function that can be used to ensure that certain objects are not
// located inside the sandbox. Typically used for trusted objects.
// Returns true when the sandbox is disabled, or when the sandbox is partially
// reserved and the address is outside the reservation.
V8_INLINE bool OutsideSandbox(uintptr_t address) {
#ifdef V8_ENABLE_SANDBOX
  Sandbox* sandbox = Sandbox::current();
  // Use ReservationContains (instead of just Contains) to correctly handle the
  // case of partially-reserved sandboxes.
  return !sandbox->ReservationContains(address);
#else
  return true;
#endif
}

V8_INLINE bool InsideSandbox(uintptr_t address) {
#ifdef V8_ENABLE_SANDBOX
  Sandbox* sandbox = Sandbox::current();
  return sandbox->Contains(address);
#else
  return true;
#endif
}

V8_INLINE void* EmptyBackingStoreBuffer() {
#ifdef V8_ENABLE_SANDBOX
  return reinterpret_cast<void*>(
      Sandbox::current()->constants().empty_backing_store_buffer());
#else
  return nullptr;
#endif
}

}  // namespace internal
}  // namespace v8

#endif  // V8_SANDBOX_SANDBOX_H_
