@ 261107.7
current
// Copyright 2022 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.

#include "src/sandbox/testing.h"

#include <atomic>
#include <cstring>
#include <vector>

#include "src/api/api-inl.h"
#include "src/api/api-natives.h"
#include "src/base/platform/mutex.h"
#include "src/base/strong-alias.h"
#include "src/base/virtual-address-space.h"
#include "src/builtins/builtins.h"
#include "src/common/globals.h"
#include "src/execution/isolate-inl.h"
#include "src/heap/factory.h"
#include "src/heap/heap.h"
#include "src/heap/read-only-spaces.h"
#include "src/objects/backing-store.h"
#include "src/objects/contexts.h"
#include "src/objects/feedback-vector.h"
#include "src/objects/fixed-array.h"
#include "src/objects/fixed-primitive-array-inl.h"
#include "src/objects/foreign.h"
#include "src/objects/instance-type.h"
#include "src/objects/js-array-buffer.h"
#include "src/objects/js-objects.h"
#include "src/objects/tagged-field-inl.h"
#include "src/objects/templates.h"
#include "src/sandbox/sandbox.h"
#include "src/sandbox/trusted-pointer-table-inl.h"

#ifdef V8_INTL_SUPPORT
#include "src/objects/js-segments.h"
#endif  // V8_INTL_SUPPORT

#ifdef V8_OS_LINUX
#include <signal.h>
#include <sys/mman.h>
#include <sys/ucontext.h>
#include <unistd.h>

#include "src/base/platform/platform-linux.h"
#endif  // V8_OS_LINUX

#if defined(V8_USE_ADDRESS_SANITIZER)
#include <sanitizer/asan_interface.h>
#endif

#if defined(V8_USE_ADDRESS_SANITIZER) || defined(V8_USE_MEMORY_SANITIZER) || \
    defined(V8_USE_UNDEFINED_BEHAVIOR_SANITIZER)
#define V8_USE_ANY_SANITIZER 1
#include <sanitizer/common_interface_defs.h>
#endif

#if defined(V8_ENABLE_SANDBOX) && defined(V8_ENABLE_MEMORY_CORRUPTION_API)
#include "src/sandbox/external-strings-cage.h"
#endif  // V8_ENABLE_SANDBOX && V8_ENABLE_MEMORY_CORRUPTION_API

namespace v8 {
namespace internal {

#ifdef V8_ENABLE_SANDBOX

SandboxTesting::Mode SandboxTesting::mode_ = SandboxTesting::Mode::kDisabled;

namespace {
void ThrowTypeError(v8::Isolate* isolate, std::string_view message) {
  isolate->ThrowException(v8::Exception::TypeError(
      v8::String::NewFromUtf8(isolate, message.data(), NewStringType::kNormal,
                              static_cast<int>(message.size()))
          .ToLocalChecked()));
}
}  // namespace

#ifdef V8_ENABLE_MEMORY_CORRUPTION_API

namespace {
bool IsLocatedInMappedMemory(Address address, Heap* heap) {
#if CONTIGUOUS_COMPRESSED_READ_ONLY_SPACE_BOOL
  // Under contiguous compressed read-only space, any address inside the
  // contiguous read-only reservation belongs to the read-only space. This
  // check is 100% safe against crashes on arbitrary invalid addresses because
  // it performs only bitwise operations on integers without dereferencing any
  // memory.
  if ((address & kContiguousReadOnlySpaceMask) == 0) {
    return heap->read_only_space()->ContainsSlow(address);
  }
#else
  // Fallback check for read-only space when contiguous compression is not used.
  if (heap->read_only_space()->ContainsSlow(address)) {
    return true;
  }
#endif

  // Check the local memory allocator's normal and large pages.
  if (heap->memory_allocator()->LookupChunkContainingAddress(address) !=
      nullptr) {
    return true;
  }

  // Also check the shared heap memory allocator if this isolate uses a shared
  // space.
  if (heap->isolate()->has_shared_space() &&
      heap->isolate()
              ->shared_space_isolate()
              ->heap()
              ->memory_allocator()
              ->LookupChunkContainingAddress(address) != nullptr) {
    return true;
  }

  return false;
}

bool IsValidHeapObject(Address addr, Heap* heap) {
  Sandbox* sandbox = Sandbox::current();
  Address current = addr;
  // Simple heuristic: follow the Map chain three times until we find a MetaMap
  // (where the map pointer points to itself), or give up.
  for (int j = 0; j < 3; j++) {
    if (!IsLocatedInMappedMemory(current, heap)) {
      return false;
    }
    uint32_t map_word = *reinterpret_cast<uint32_t*>(current);
    if ((map_word & kHeapObjectTag) != kHeapObjectTag) {
      return false;
    }
    Address map_address = sandbox->base() + (map_word & ~kHeapObjectTagMask);
    if (map_address == current) {
      return true;
    }
    current = map_address;
  }
  return false;
}

// Sandbox.base
void SandboxGetBase(const v8::FunctionCallbackInfo<v8::Value>& info) {
  DCHECK(ValidateCallbackInfo(info));
  v8::Isolate* isolate = info.GetIsolate();
  double sandbox_base = Sandbox::current()->base();
  info.GetReturnValue().Set(v8::Number::New(isolate, sandbox_base));
}
// Sandbox.byteLength
void SandboxGetByteLength(const v8::FunctionCallbackInfo<v8::Value>& info) {
  DCHECK(ValidateCallbackInfo(info));
  v8::Isolate* isolate = info.GetIsolate();
  double sandbox_size = Sandbox::current()->size();
  info.GetReturnValue().Set(v8::Number::New(isolate, sandbox_size));
}

// new Sandbox.MemoryView(info) -> Sandbox.MemoryView
void SandboxMemoryView(const v8::FunctionCallbackInfo<v8::Value>& info) {
  DCHECK(ValidateCallbackInfo(info));
  v8::Isolate* isolate = info.GetIsolate();
  Local<v8::Context> context = isolate->GetCurrentContext();

  if (!info.IsConstructCall()) {
    isolate->ThrowError("Sandbox.MemoryView must be invoked with 'new'");
    return;
  }

  Local<v8::Integer> arg1, arg2;
  if (!info[0]->ToInteger(context).ToLocal(&arg1) ||
      !info[1]->ToInteger(context).ToLocal(&arg2)) {
    isolate->ThrowError("Expects two number arguments (start offset and size)");
    return;
  }

  Sandbox* sandbox = Sandbox::current();
  CHECK_LE(sandbox->size(), kMaxSafeIntegerUint64);

  uint64_t offset = arg1->Value();
  uint64_t size = arg2->Value();
  if (offset > sandbox->size() || size > sandbox->size() ||
      (offset + size) > sandbox->size()) {
    isolate->ThrowError(
        "The MemoryView must be entirely contained within the sandbox");
    return;
  }

  Factory* factory = reinterpret_cast<Isolate*>(isolate)->factory();
  std::unique_ptr<BackingStore> memory = BackingStore::WrapAllocation(
      reinterpret_cast<void*>(sandbox->base() + offset), size,
      v8::BackingStore::EmptyDeleter, nullptr, SharedFlag{false});
  if (!memory) {
    isolate->ThrowError("Out of memory: MemoryView backing store");
    return;
  }
  Handle<JSArrayBuffer> buffer = factory->NewJSArrayBuffer(std::move(memory));
  info.GetReturnValue().Set(Utils::ToLocal(buffer));
}

// The methods below either take a HeapObject or the address of a HeapObject as
// argument. These helper functions can be used to extract the argument object
// in both cases.
using ArgumentObjectExtractorFunction = std::function<bool(
    const v8::FunctionCallbackInfo<v8::Value>&, Tagged<HeapObject>* out)>;

static bool GetArgumentObjectPassedAsReference(
    const v8::FunctionCallbackInfo<v8::Value>& info, Tagged<HeapObject>* out) {
  v8::Isolate* isolate = info.GetIsolate();

  if (info.Length() == 0) {
    isolate->ThrowError("First argument must be provided");
    return false;
  }

  Handle<Object> arg = Utils::OpenHandle(*info[0]);
  if (!IsHeapObject(*arg)) {
    isolate->ThrowError("First argument must be a HeapObject");
    return false;
  }

  *out = Cast<HeapObject>(*arg);
  return true;
}

static bool GetArgumentObjectPassedAsAddress(
    const v8::FunctionCallbackInfo<v8::Value>& info, Tagged<HeapObject>* out) {
  Sandbox* sandbox = Sandbox::current();
  v8::Isolate* isolate = info.GetIsolate();
  Local<v8::Context> context = isolate->GetCurrentContext();

  if (info.Length() == 0) {
    isolate->ThrowError("First argument must be provided");
    return false;
  }

  Local<v8::Uint32> arg1;
  if (!info[0]->ToUint32(context).ToLocal(&arg1)) {
    isolate->ThrowError("First argument must be the address of a HeapObject");
    return false;
  }

  uint32_t address = arg1->Value();
  // Allow tagged addresses by removing the kHeapObjectTag and
  // kWeakHeapObjectTag. This allows clients to just read tagged pointers from
  // the heap and use them for these APIs.
  address &= ~kHeapObjectTagMask;
  *out = HeapObject::FromAddress(sandbox->base() + address);
  return true;
}

// Sandbox.getAddressOf(Object) -> Number
void SandboxGetAddressOf(const v8::FunctionCallbackInfo<v8::Value>& info) {
  DCHECK(ValidateCallbackInfo(info));
  v8::Isolate* isolate = info.GetIsolate();

  Tagged<HeapObject> obj;
  if (!GetArgumentObjectPassedAsReference(info, &obj)) {
    return;
  }

  // HeapObjects must be allocated inside the pointer compression cage so their
  // address relative to the start of the sandbox can be obtained simply by
  // taking the lowest 32 bits of the absolute address.
  uint32_t address = static_cast<uint32_t>(obj->address());
  info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, address));
}

// Sandbox.getObjectAt(Number) -> Object
void SandboxGetObjectAt(const v8::FunctionCallbackInfo<v8::Value>& info) {
  DCHECK(ValidateCallbackInfo(info));
  v8::Isolate* isolate = info.GetIsolate();

  Tagged<HeapObject> obj;
  if (!GetArgumentObjectPassedAsAddress(info, &obj)) {
    return;
  }

  Isolate* i_isolate = reinterpret_cast<Isolate*>(isolate);
  if (!IsValidHeapObject(obj.address(), i_isolate->heap())) return;
  Handle<Object> handle(obj, i_isolate);
  info.GetReturnValue().Set(ToApiHandle<v8::Value>(handle));
}

// Sandbox.isValidObjectAt(Address) -> Bool
void SandboxIsValidObjectAt(const v8::FunctionCallbackInfo<v8::Value>& info) {
  DCHECK(ValidateCallbackInfo(info));
  v8::Isolate* isolate = info.GetIsolate();
  Heap* heap = reinterpret_cast<Isolate*>(isolate)->heap();

  Tagged<HeapObject> obj;
  if (!GetArgumentObjectPassedAsAddress(info, &obj)) {
    return;
  }

  info.GetReturnValue().Set(IsValidHeapObject(obj.address(), heap));
}
static void SandboxIsWritableImpl(
    const v8::FunctionCallbackInfo<v8::Value>& info,
    ArgumentObjectExtractorFunction getArgumentObject) {
  DCHECK(ValidateCallbackInfo(info));

  Tagged<HeapObject> obj;
  if (!getArgumentObject(info, &obj)) return;

  Isolate* i_isolate = reinterpret_cast<Isolate*>(info.GetIsolate());
  bool is_writable = IsValidHeapObject(obj.address(), i_isolate->heap()) &&
                     BasePage::FromHeapObject(i_isolate, obj)->IsWritable();
  info.GetReturnValue().Set(is_writable);
}

// Sandbox.isWritable(Object) -> Bool
void SandboxIsWritable(const v8::FunctionCallbackInfo<v8::Value>& info) {
  SandboxIsWritableImpl(info, &GetArgumentObjectPassedAsReference);
}

// Sandbox.isWritableObjectAt(Number) -> Bool
void SandboxIsWritableObjectAt(
    const v8::FunctionCallbackInfo<v8::Value>& info) {
  SandboxIsWritableImpl(info, &GetArgumentObjectPassedAsAddress);
}

static void SandboxGetSizeOfImpl(
    const v8::FunctionCallbackInfo<v8::Value>& info,
    ArgumentObjectExtractorFunction getArgumentObject) {
  DCHECK(ValidateCallbackInfo(info));

  Tagged<HeapObject> obj;
  if (!getArgumentObject(info, &obj)) {
    return;
  }

  if (!IsValidHeapObject(obj.address(),
                         reinterpret_cast<Isolate*>(info.GetIsolate())->heap()))
    return;

  int size = obj->Size();
  info.GetReturnValue().Set(size);
}

// Sandbox.getSizeOf(Object) -> Number
void SandboxGetSizeOf(const v8::FunctionCallbackInfo<v8::Value>& info) {
  SandboxGetSizeOfImpl(info, &GetArgumentObjectPassedAsReference);
}

// Sandbox.getSizeOfObjectAt(Number) -> Number
void SandboxGetSizeOfObjectAt(const v8::FunctionCallbackInfo<v8::Value>& info) {
  SandboxGetSizeOfImpl(info, &GetArgumentObjectPassedAsAddress);
}

static void SandboxGetInstanceTypeOfImpl(
    const v8::FunctionCallbackInfo<v8::Value>& info,
    ArgumentObjectExtractorFunction getArgumentObject) {
  DCHECK(ValidateCallbackInfo(info));
  v8::Isolate* isolate = info.GetIsolate();

  Tagged<HeapObject> obj;
  if (!getArgumentObject(info, &obj)) {
    return;
  }

  if (!IsValidHeapObject(obj.address(),
                         reinterpret_cast<Isolate*>(info.GetIsolate())->heap()))
    return;

  InstanceType type = obj->map()->instance_type();
  std::stringstream out;
  out << type;
  MaybeLocal<v8::String> result =
      v8::String::NewFromUtf8(isolate, out.str().c_str());
  info.GetReturnValue().Set(result.ToLocalChecked());
}

// Sandbox.getInstanceTypeOf(Object) -> String
void SandboxGetInstanceTypeOf(const v8::FunctionCallbackInfo<v8::Value>& info) {
  SandboxGetInstanceTypeOfImpl(info, &GetArgumentObjectPassedAsReference);
}

// Sandbox.getInstanceTypeOfObjectAt(Number) -> String
void SandboxGetInstanceTypeOfObjectAt(
    const v8::FunctionCallbackInfo<v8::Value>& info) {
  SandboxGetInstanceTypeOfImpl(info, &GetArgumentObjectPassedAsAddress);
}

static void SandboxGetInstanceTypeIdOfImpl(
    const v8::FunctionCallbackInfo<v8::Value>& info,
    ArgumentObjectExtractorFunction getArgumentObject) {
  DCHECK(ValidateCallbackInfo(info));

  Tagged<HeapObject> obj;
  if (!getArgumentObject(info, &obj)) {
    return;
  }

  if (!IsValidHeapObject(obj.address(),
                         reinterpret_cast<Isolate*>(info.GetIsolate())->heap()))
    return;

  InstanceType type = obj->map()->instance_type();
  static_assert(std::is_same_v<std::underlying_type_t<InstanceType>, uint16_t>);
  if (type > LAST_TYPE) {
    // This can happen with corrupted objects. Canonicalize to a special
    // "unknown" instance type to indicate that this is an unknown type.
    const uint16_t kUnknownInstanceType = std::numeric_limits<uint16_t>::max();
    type = static_cast<InstanceType>(kUnknownInstanceType);
  }

  info.GetReturnValue().Set(type);
}

// Sandbox.getInstanceTypeIdOf(Object) -> Number
void SandboxGetInstanceTypeIdOf(
    const v8::FunctionCallbackInfo<v8::Value>& info) {
  SandboxGetInstanceTypeIdOfImpl(info, &GetArgumentObjectPassedAsReference);
}

// Sandbox.getInstanceTypeIdOfObjectAt(Number) -> Number
void SandboxGetInstanceTypeIdOfObjectAt(
    const v8::FunctionCallbackInfo<v8::Value>& info) {
  SandboxGetInstanceTypeIdOfImpl(info, &GetArgumentObjectPassedAsAddress);
}

// Sandbox.getInstanceTypeIdFor(String) -> Number
void SandboxGetInstanceTypeIdFor(
    const v8::FunctionCallbackInfo<v8::Value>& info) {
  DCHECK(ValidateCallbackInfo(info));
  v8::Isolate* isolate = info.GetIsolate();

  v8::String::Utf8Value type_name(isolate, info[0]);
  if (!*type_name) {
    isolate->ThrowError("First argument must be a string");
    return;
  }

  auto& all_types = SandboxTesting::GetInstanceTypeMap();
  if (all_types.find(*type_name) == all_types.end()) {
    isolate->ThrowError(
        "Unknown type name. If needed, add it in "
        "SandboxTesting::GetInstanceTypeMap");
    return;
  }

  InstanceType type_id = all_types[*type_name];
  info.GetReturnValue().Set(type_id);
}

// Obtain the offset of a field in an object.
//
// This can be used to obtain the offsets of internal object fields in order to
// avoid hardcoding offsets into testcases. It basically makes the various
// Foo::kBarOffset constants accessible from JavaScript. The main benefit of
// that is that testcases continue to work if the field offset changes.
// Additionally, if a field is removed, testcases that use it will fail and can
// then be deleted if they are no longer useful.
//
// TODO(saelo): instead of this, consider adding an API like
// `Sandbox.getTypeDescriptor(Number|String) -> Object` which, given an
// instance type id or name, returns an object containing the offset constants
// as properties as well as potentially other information such as the types of
// the object's fields.
//
// Sandbox.getFieldOffset(Number, String) -> Number
void SandboxGetFieldOffset(const v8::FunctionCallbackInfo<v8::Value>& info) {
  DCHECK(ValidateCallbackInfo(info));
  v8::Isolate* isolate = info.GetIsolate();
  Local<v8::Context> context = isolate->GetCurrentContext();

  if (!info[0]->IsInt32()) {
    isolate->ThrowError("First argument must be an integer");
    return;
  }

  int raw_type = info[0]->Int32Value(context).FromMaybe(-1);
  if (raw_type < FIRST_TYPE || raw_type > LAST_TYPE) {
    isolate->ThrowError("Invalid instance type");
    return;
  }
  InstanceType instance_type = static_cast<InstanceType>(raw_type);

  v8::String::Utf8Value field_name(isolate, info[1]);
  if (!*field_name) {
    isolate->ThrowError("Second argument must be a string");
    return;
  }

  if (std::optional<int> offset =
          SandboxTesting::GetFieldOffset(isolate, instance_type, *field_name)) {
    info.GetReturnValue().Set(offset.value());
  } else {
    DCHECK(isolate->HasPendingException());
  }
}

// Sandbox.unpublishTrustedHandle(Number) -> Void
void SandboxUnpublishTrustedHandle(
    const v8::FunctionCallbackInfo<v8::Value>& info) {
  DCHECK(ValidateCallbackInfo(info));
  v8::Isolate* isolate = info.GetIsolate();
  Isolate* i_isolate = reinterpret_cast<Isolate*>(isolate);

  if (info.Length() < 1) {
    ThrowTypeError(isolate, "First argument must be a handle");
    return;
  }
  uint32_t handle_value;
  if (!info[0]->Uint32Value(isolate->GetCurrentContext()).To(&handle_value)) {
    ThrowTypeError(isolate, "First argument must be a handle");
    return;
  }

  IndirectPointerHandle handle =
      static_cast<IndirectPointerHandle>(handle_value);
  i_isolate->trusted_pointer_table().Unpublish(handle);
}

// Returns an array of all builtin names, index of the name is the builtin id.
//
// This can be used to determine the id of a specific builtin for use with
// Sandbox.setFunctionCodeToBuiltin().
//
// Sandbox.getBuiltinNames() -> Array[String]
void SandboxGetBuiltinNames(const v8::FunctionCallbackInfo<v8::Value>& info) {
  DCHECK(ValidateCallbackInfo(info));
  v8::Isolate* isolate = info.GetIsolate();

  v8::LocalVector<v8::Value> names(isolate, Builtins::kBuiltinCount);
  for (Builtin i = Builtins::kFirst; i <= Builtins::kLast; ++i) {
    names[static_cast<uint32_t>(i)] =
        v8::String::NewFromUtf8(isolate, Builtins::name(i)).ToLocalChecked();
  }

  Local<v8::Array> result = v8::Array::New(isolate, names.data(), names.size());

  info.GetReturnValue().Set(result);
}

// Returns compressed Code object by a given builtin id.
//
// This can be useful for manipulating IC handlers represented by Code objects.
//
// Sandbox.getBuiltinCode(Number) -> Number
void SandboxGetBuiltinCode(const v8::FunctionCallbackInfo<v8::Value>& info) {
  DCHECK(ValidateCallbackInfo(info));
  v8::Isolate* isolate = info.GetIsolate();
  Local<v8::Context> context = isolate->GetCurrentContext();

  if (!info[0]->IsInt32()) {
    isolate->ThrowError("First argument must be an integer");
    return;
  }

  int raw_builtin_id = info[0]->Int32Value(context).FromMaybe(-1);
  if (!Builtins::IsBuiltinId(raw_builtin_id)) {
    isolate->ThrowError("Invalid builtin id");
    return;
  }
  Builtin builtin = static_cast<Builtin>(raw_builtin_id);

  Isolate* i_isolate = reinterpret_cast<Isolate*>(isolate);
  Tagged<Code> code = i_isolate->builtins()->code(builtin);
  // Update this code once we move builtins' Code objects from the main cage.
  // The Sandbox Api must not leak addresses outside of the main cage, so this
  // function should probably return a CodeWrapper or whatever we'll be using
  // as IC handlers representing builtins. If we decide to encode those
  // builtins into Smi handlers, then this function should probably be removed.
  CHECK(code.IsInMainCageBase());

  info.GetReturnValue().Set(V8HeapCompressionScheme::CompressAny(code.ptr()));
}

// Sets given function's code value to a given builtin's code object.
//
// This can be used to shortcut overwriting JSFunction's code in testcases.
//
// Sandbox.setFunctionCodeToBuiltin(Function, Number) -> Bool
void SandboxSetFunctionCodeToBuiltin(
    const v8::FunctionCallbackInfo<v8::Value>& info) {
  DCHECK(ValidateCallbackInfo(info));
  v8::Isolate* isolate = info.GetIsolate();
  Local<v8::Context> context = isolate->GetCurrentContext();

  if (!IsJSFunction(*v8::Utils::OpenHandle(*info[0]))) {
    isolate->ThrowError("First argument must be an function");
    return;
  }

  if (!info[1]->IsInt32()) {
    isolate->ThrowError("Second argument must be an integer");
    return;
  }

  int raw_builtin_id = info[1]->Int32Value(context).FromMaybe(-1);
  if (!Builtins::IsBuiltinId(raw_builtin_id)) {
    isolate->ThrowError("Invalid builtin id");
    return;
  }
  Builtin builtin = static_cast<Builtin>(raw_builtin_id);

  auto function = Cast<JSFunction>(v8::Utils::OpenDirectHandle(*info[0]));

  Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
  function->UpdateCode(i_isolate, i_isolate->builtins()->code(builtin));

  info.GetReturnValue().Set(true);
}

namespace {
struct ResolvedField {
  Tagged<HeapObject> holder;
  int offset = -1;
  int bit_size = 32;
};

bool ResolveObjectField(const v8::FunctionCallbackInfo<v8::Value>& info,
                        int bit_size_arg_index, ResolvedField* out) {
  v8::Isolate* isolate = info.GetIsolate();
  Local<v8::Context> context = isolate->GetCurrentContext();

  Tagged<HeapObject> obj;
  if (!GetArgumentObjectPassedAsReference(info, &obj)) return false;

  if (!IsValidHeapObject(obj.address(),
                         reinterpret_cast<Isolate*>(isolate)->heap()))
    return false;

  int offset;
  if (!info[1]->IsInt32() || !info[1]->Int32Value(context).To(&offset)) {
    v8::String::Utf8Value field_name(isolate, info[1]);
    if (!*field_name) {
      isolate->ThrowError("Second argument must be an integer or a string");
      return false;
    }

    InstanceType instance_type = obj->map()->instance_type();
    if (std::optional<int> offset_from_name = SandboxTesting::GetFieldOffset(
            isolate, instance_type, *field_name)) {
      offset = offset_from_name.value();
    } else {
      DCHECK(isolate->HasPendingException());
      return false;
    }
  }

  int bit_size = 32;
  if (info.Length() > bit_size_arg_index) {
    if (!info[bit_size_arg_index]->IsInt32() ||
        !info[bit_size_arg_index]->Int32Value(context).To(&bit_size)) {
      if (bit_size_arg_index == 2) {
        isolate->ThrowError("Third argument (bit size) must be an integer");
      } else {
        isolate->ThrowError("Fourth argument (bit size) must be an integer");
      }
      return false;
    }
    if (bit_size != 8 && bit_size != 16 && bit_size != 32 && bit_size != 64) {
      if (bit_size_arg_index == 2) {
        isolate->ThrowError(
            "Third argument (bit size) must be 8, 16, 32, or 64");
      } else {
        isolate->ThrowError(
            "Fourth argument (bit size) must be 8, 16, 32, or 64");
      }
      return false;
    }
  }

  int object_size = obj->Size();
  DCHECK_EQ(0, object_size % kTaggedSize);
  int byte_size = bit_size / 8;
  if (offset < 0 || offset > object_size - byte_size) {
    std::ostringstream error;
    error << "Second argument (offset=" << offset << ", size=" << byte_size
          << ") is out of bounds of the given object of size " << object_size;
    ThrowTypeError(isolate, error.view());
    return false;
  }
  // Enforce natural alignment up to kTaggedSize.
  int required_alignment = std::min(byte_size, kTaggedSize);
  if ((offset % required_alignment) != 0) {
    std::ostringstream error;
    error << "Second argument (offset=" << offset << ") is not "
          << required_alignment << "-byte-aligned";
    ThrowTypeError(isolate, error.view());
    return false;
  }

  out->holder = obj;
  out->offset = offset;
  out->bit_size = bit_size;
  return true;
}
}  // namespace

// Read one field of an object without setting up a memory view first.
//
//   let value = Sandbox.readObjectField(obj, offset, bit_size);
// emits a raw memory read, identical to
//   (new DataView(new Sandbox.MemoryView(0, 0x100000000))).getUint32(
//      Sandbox.getAddressOf(obj) + offset, true);
// (adjusted for the requested bit size)
//
// Alternatively, a field name can be passed as the second argument; the effect
// is identical to calling
// `Sandbox.getFieldOffset(Sandbox.getInstanceTypeIdOf(obj), field_name)` to
// resolve the offset first.
//
// Sandbox.readObjectField(Object, Number|String, [Number]) -> Number|BigInt
void SandboxReadObjectField(const v8::FunctionCallbackInfo<v8::Value>& info) {
  DCHECK(ValidateCallbackInfo(info));
  v8::Isolate* isolate = info.GetIsolate();

  ResolvedField field;
  if (!ResolveObjectField(info, 2, &field)) return;

  if (field.bit_size == 8) {
    uint8_t value = field.holder->ReadField<uint8_t>(field.offset);
    info.GetReturnValue().Set(value);
  } else if (field.bit_size == 16) {
    uint16_t value = field.holder->ReadField<uint16_t>(field.offset);
    info.GetReturnValue().Set(value);
  } else if (field.bit_size == 32) {
    uint32_t value = field.holder->ReadField<uint32_t>(field.offset);
    info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value));
  } else if (field.bit_size == 64) {
    uint64_t value = field.holder->ReadField<uint64_t>(field.offset);
    info.GetReturnValue().Set(v8::BigInt::NewFromUnsigned(isolate, value));
  }
}

// Sandbox.dereferenceTaggedPointerField(Object, Number|String) -> Object
//
// Reads a tagged pointer field from an object and returns the pointed-to
// object. The second argument is the field offset (Number) or field name
// (String). This is a convenience method that combines reading the compressed
// pointer, decompressing it, and wrapping the result in a V8 handle.
// Note: this method specifically handles standard tagged (compressed) pointers.
// If used on fields containing ExternalPointers or TrustedPointerHandles, it
// will return incorrect results or null.
void SandboxDereferenceTaggedPointerField(
    const v8::FunctionCallbackInfo<v8::Value>& info) {
  DCHECK(ValidateCallbackInfo(info));
  v8::Isolate* isolate = info.GetIsolate();

  ResolvedField field;
  if (!ResolveObjectField(info, 2, &field)) return;

  Isolate* i_isolate = reinterpret_cast<Isolate*>(isolate);
  Tagged<Object> value = TaggedField<Object>::load(field.holder, field.offset);

  if (IsHeapObject(value)) {
    Handle<HeapObject> handle(Cast<HeapObject>(value), i_isolate);
    info.GetReturnValue().Set(ToApiHandle<v8::Value>(handle));
  } else {
    info.GetReturnValue().Set(v8::Null(isolate));
  }
}

// Corrupt one field of an object without setting up a memory view first.
//
//   Sandbox.corruptObjectField(obj, offset, value, bit_size);
// emits a raw memory write, identical to
//   (new DataView(new Sandbox.MemoryView(0, 0x100000000))).setUint32(
//      Sandbox.getAddressOf(obj) + offset, value, true);
// (adjusted for the requested bit size)
//
// Note that values are written directly as raw bytes without Smi tagging.
// Alternatively, a field name can be passed as the second argument; the effect
// is identical to calling
// `Sandbox.getFieldOffset(Sandbox.getInstanceTypeIdOf(obj), field_name)` to
// resolve the offset first.
//
// Sandbox.corruptObjectField(Object, Number|String, Number|BigInt, [Number]) ->
// undefined
void SandboxCorruptObjectField(
    const v8::FunctionCallbackInfo<v8::Value>& info) {
  DCHECK(ValidateCallbackInfo(info));
  v8::Isolate* isolate = info.GetIsolate();
  Local<v8::Context> context = isolate->GetCurrentContext();

  ResolvedField field;
  if (!ResolveObjectField(info, 3, &field)) return;

  uint64_t value = 0;
  if (field.bit_size != 64) {
    if (!info[2]->IsInt32() && !info[2]->IsUint32()) {
      isolate->ThrowError("Third argument must be an integer");
      return;
    }
    int64_t val64;
    CHECK(info[2]->IntegerValue(context).To(&val64));
    if (field.bit_size == 8) {
      if (val64 < -128 || val64 > 255) {
        isolate->ThrowError("Third argument must fit in 8-bit integer range");
        return;
      }
    } else if (field.bit_size == 16) {
      if (val64 < -32768 || val64 > 65535) {
        isolate->ThrowError("Third argument must fit in 16-bit integer range");
        return;
      }
    }
    value = static_cast<uint64_t>(val64);
  } else {
    if (info[2]->IsBigInt()) {
      value = info[2].As<v8::BigInt>()->Uint64Value();
    } else if (info[2]->IsNumber()) {
      int64_t val64;
      if (info[2]->IntegerValue(context).To(&val64)) {
        value = static_cast<uint64_t>(val64);
      } else {
        isolate->ThrowError(
            "Third argument must be a 64-bit integer or BigInt");
        return;
      }
    } else {
      isolate->ThrowError("Third argument must be a 64-bit integer or BigInt");
      return;
    }
  }

  if (field.bit_size == 8) {
    field.holder->WriteField<uint8_t>(field.offset,
                                      static_cast<uint8_t>(value));
  } else if (field.bit_size == 16) {
    field.holder->WriteField<uint16_t>(field.offset,
                                       static_cast<uint16_t>(value));
  } else if (field.bit_size == 32) {
    field.holder->WriteField<uint32_t>(field.offset,
                                       static_cast<uint32_t>(value));
  } else if (field.bit_size == 64) {
    field.holder->WriteField<uint64_t>(field.offset, value);
  }
}

Handle<FunctionTemplateInfo> NewFunctionTemplate(
    Isolate* isolate, FunctionCallback func,
    ConstructorBehavior constructor_behavior) {
  // Use the API functions here as they are more convenient to use.
  v8::Isolate* api_isolate = reinterpret_cast<v8::Isolate*>(isolate);
  Local<FunctionTemplate> function_template =
      FunctionTemplate::New(api_isolate, func, {}, {}, 0, constructor_behavior,
                            SideEffectType::kHasSideEffect);
  return v8::Utils::OpenHandle(*function_template);
}

Handle<JSFunction> CreateFunc(Isolate* isolate, FunctionCallback func,
                              Handle<String> name, bool is_constructor) {
  ConstructorBehavior constructor_behavior = is_constructor
                                                 ? ConstructorBehavior::kAllow
                                                 : ConstructorBehavior::kThrow;
  Handle<FunctionTemplateInfo> function_template =
      NewFunctionTemplate(isolate, func, constructor_behavior);
  return ApiNatives::InstantiateFunction(isolate, function_template, name)
      .ToHandleChecked();
}

void InstallFunc(Isolate* isolate, Handle<JSObject> holder,
                 FunctionCallback func, const char* name, int num_parameters,
                 bool is_constructor) {
  Factory* factory = isolate->factory();
  Handle<String> function_name = factory->NewStringFromAsciiChecked(name);
  Handle<JSFunction> function =
      CreateFunc(isolate, func, function_name, is_constructor);
  function->shared()->set_length(num_parameters);
  JSObject::AddProperty(isolate, holder, function_name, function, NONE);
}

void InstallGetter(Isolate* isolate, Handle<JSObject> object,
                   FunctionCallback func, const char* name) {
  Factory* factory = isolate->factory();
  Handle<String> property_name = factory->NewStringFromAsciiChecked(name);
  Handle<JSFunction> getter = CreateFunc(isolate, func, property_name, false);
  Handle<Object> setter = factory->null_value();
  JSObject::DefineOwnAccessorIgnoreAttributes(object, property_name, getter,
                                              setter, FROZEN);
}

void InstallFunction(Isolate* isolate, Handle<JSObject> holder,
                     FunctionCallback func, const char* name,
                     int num_parameters) {
  InstallFunc(isolate, holder, func, name, num_parameters, false);
}

// Sandbox.getMetadata() -> Object
void SandboxGetMetadata(const v8::FunctionCallbackInfo<v8::Value>& info) {
  DCHECK(ValidateCallbackInfo(info));
  v8::Isolate* isolate = info.GetIsolate();
  Isolate* i_isolate = reinterpret_cast<Isolate*>(isolate);
  Factory* factory = i_isolate->factory();

  Handle<JSObject> metadata =
      factory->NewJSObject(i_isolate->object_function());
  JSObject::AddProperty(
      i_isolate, metadata,
      factory->NewStringFromAsciiChecked("trustedPointerHandleShift"),
      factory->NewNumberFromUint(kTrustedPointerHandleShift), NONE);

  info.GetReturnValue().Set(Utils::ToLocal(metadata));
}

void InstallConstructor(Isolate* isolate, Handle<JSObject> holder,
                        FunctionCallback func, const char* name,
                        int num_parameters) {
  InstallFunc(isolate, holder, func, name, num_parameters, true);
}
}  // namespace

void SandboxTesting::InstallMemoryCorruptionApi(Isolate* isolate) {
#ifndef V8_ENABLE_MEMORY_CORRUPTION_API
#error "This function should not be available in any shipping build "          \
       "where it could potentially be abused to facilitate exploitation."
#endif

  CHECK(Sandbox::current()->is_initialized());

  // Create the special Sandbox object that provides read/write access to the
  // sandbox address space alongside other miscellaneous functionality.
  Handle<JSObject> sandbox = isolate->factory()->NewJSObject(
      isolate->object_function(), AllocationType::kOld);

  InstallGetter(isolate, sandbox, SandboxGetBase, "base");
  InstallGetter(isolate, sandbox, SandboxGetByteLength, "byteLength");
  InstallConstructor(isolate, sandbox, SandboxMemoryView, "MemoryView", 2);
  InstallFunction(isolate, sandbox, SandboxGetMetadata, "getMetadata", 0);
  InstallFunction(isolate, sandbox, SandboxGetAddressOf, "getAddressOf", 1);
  InstallFunction(isolate, sandbox, SandboxGetObjectAt, "getObjectAt", 1);
  InstallFunction(isolate, sandbox, SandboxIsValidObjectAt, "isValidObjectAt",
                  1);
  InstallFunction(isolate, sandbox, SandboxIsWritable, "isWritable", 1);
  InstallFunction(isolate, sandbox, SandboxIsWritableObjectAt,
                  "isWritableObjectAt", 1);
  InstallFunction(isolate, sandbox, SandboxGetSizeOf, "getSizeOf", 1);
  InstallFunction(isolate, sandbox, SandboxGetSizeOfObjectAt,
                  "getSizeOfObjectAt", 1);
  InstallFunction(isolate, sandbox, SandboxGetInstanceTypeOf,
                  "getInstanceTypeOf", 1);
  InstallFunction(isolate, sandbox, SandboxGetInstanceTypeOfObjectAt,
                  "getInstanceTypeOfObjectAt", 1);
  InstallFunction(isolate, sandbox, SandboxGetInstanceTypeIdOf,
                  "getInstanceTypeIdOf", 1);
  InstallFunction(isolate, sandbox, SandboxGetInstanceTypeIdOfObjectAt,
                  "getInstanceTypeIdOfObjectAt", 1);
  InstallFunction(isolate, sandbox, SandboxGetInstanceTypeIdFor,
                  "getInstanceTypeIdFor", 1);
  InstallFunction(isolate, sandbox, SandboxGetFieldOffset, "getFieldOffset", 2);
  InstallFunction(isolate, sandbox, SandboxUnpublishTrustedHandle,
                  "unpublishTrustedHandle", 1);

  InstallFunction(isolate, sandbox, SandboxGetBuiltinNames, "getBuiltinNames",
                  0);
  InstallFunction(isolate, sandbox, SandboxGetBuiltinCode, "getBuiltinCode", 1);
  InstallFunction(isolate, sandbox, SandboxSetFunctionCodeToBuiltin,
                  "setFunctionCodeToBuiltin", 2);
  InstallFunction(isolate, sandbox, SandboxReadObjectField, "readObjectField",
                  2);
  InstallFunction(isolate, sandbox, SandboxDereferenceTaggedPointerField,
                  "dereferenceTaggedPointerField", 2);
  InstallFunction(isolate, sandbox, SandboxCorruptObjectField,
                  "corruptObjectField", 3);

  // Install the Sandbox object as property on the global object.
  Handle<JSGlobalObject> global = isolate->global_object();
  Handle<String> name =
      isolate->factory()->NewStringFromAsciiChecked("Sandbox");
  JSObject::AddProperty(isolate, global, name, sandbox, DONT_ENUM);
}

namespace {
struct SafeRegion {
  base::AddressRegion region;
  SandboxTesting::MemoryAccessTypes safe_access_types;
};

base::LazyMutex g_safe_region_mutex = LAZY_MUTEX_INITIALIZER;

std::vector<SafeRegion>& GetSafeRegions() {
  g_safe_region_mutex.Pointer()->AssertHeld();
  static base::LeakyObject<std::vector<SafeRegion>> g_safe_regions;
  return *g_safe_regions.get();
}
}  // namespace

void SandboxTesting::RegisterSafeMemoryRegion(
    Address start, size_t size, MemoryAccessTypes safe_access_types) {
  base::MutexGuard guard(g_safe_region_mutex.Pointer());
  GetSafeRegions().push_back({{start, size}, safe_access_types});
}

void SandboxTesting::UnregisterSafeMemoryRegion(Address start) {
  base::MutexGuard guard(g_safe_region_mutex.Pointer());
  size_t num_removed = std::erase_if(
      GetSafeRegions(),
      [start](const auto& entry) { return entry.region.begin() == start; });
  CHECK_EQ(num_removed, 1);
}

#endif  // V8_ENABLE_MEMORY_CORRUPTION_API

namespace {
#ifdef V8_OS_LINUX

void PrintToStderr(const char* output) {
  // NOTE: This code MUST be async-signal safe.
  // NO malloc or stdio is allowed here.
  ssize_t return_val = write(STDERR_FILENO, output, strlen(output));
  USE(return_val);
}

using MemoryAccessType = SandboxTesting::MemoryAccessType;

MemoryAccessType GetAccessType(void* context) {
#ifdef V8_HOST_ARCH_X64
  ucontext_t* ctx = reinterpret_cast<ucontext_t*>(context);
  // See the X86-64 architecture manual for the error code bits.
  static constexpr greg_t kWriteAccessBit = 1;
  static constexpr greg_t kInstructionFetchBit = 4;
  const greg_t err = ctx->uc_mcontext.gregs[REG_ERR];
  if (err & (1 << kWriteAccessBit)) return MemoryAccessType::kWrite;
  if (err & (1 << kInstructionFetchBit)) return MemoryAccessType::kExecute;
  return MemoryAccessType::kRead;
#else   // V8_HOST_ARCH_X64
  // Conservatively assume it's a write.
  return MemoryAccessType::kWrite;
#endif  // V8_HOST_ARCH_X64
}

bool IsCrashInSafeMemoryRegion(Address faultaddr,
                               MemoryAccessType access_type) {
#ifdef V8_ENABLE_MEMORY_CORRUPTION_API
  if (g_safe_region_mutex.Pointer()->TryLock()) {
    bool is_safe = false;
    for (const auto& entry : GetSafeRegions()) {
      if (entry.region.contains(faultaddr)) {
        is_safe = entry.safe_access_types.contains(access_type);
        break;
      }
    }
    g_safe_region_mutex.Pointer()->Unlock();
    return is_safe;
  }
#endif

  // If we don't have the known-safe memory regions (or cannot access them,
  // which is very unlikely), we need to rely on data from /proc/self/maps,
  // which is much less accurate.
  PrintToStderr(
      "Cannot check if faulting address lies inside a know-safe memory region. "
      "Falling back to generic checks. Results will be inaccurate\n");

  // We never expect to fault on an instruction fetch.
  if (access_type == MemoryAccessType::kExecute) return false;

  base::SignalSafeMapsParser parser;
  if (!parser.IsValid()) {
    PrintToStderr(
        "Could not access /proc/self/maps so cannot determine if access "
        "violation is safe.\n");
    return false;
  }

  while (auto entry = parser.Next()) {
    if (faultaddr >= entry->start && faultaddr < entry->end) {
      // With in-sandbox corruption it is possible to cause (safe) access
      // violations inside the pointer table memory mappings. Unfortunately,
      // these can be both PROT_NONE and PROT_READ mappings (as some table have
      // RO segments), so we need to treat both of these as safe. However, we
      // should never see access violations on non-anonymous mappings, so we
      // can check for that here. Anonymous mappings will either not have a
      // name/path at all, or it will be something like [anon:foo-bar].
      if (entry->pathname[0] != '\0' && entry->pathname[0] != '[') return false;
      return entry->permissions == PagePermissions::kNoAccess ||
             entry->permissions == PagePermissions::kRead;
    }
  }

  PrintToStderr(
      "Could not find faulting address in /proc/self/maps so cannot "
      "determine if access violation is safe.\n");
  return false;
}

[[noreturn]]
void FilterCrash(const char* reason) {
  // NOTE: This code MUST be async-signal safe.
  // NO malloc or stdio is allowed here.
  PrintToStderr(reason);
  PrintToStderr(" Exiting process...\n");
  // In sandbox fuzzing mode, we want to exit with a non-zero status to
  // indicate to the fuzzer that the sample "failed" (ran into an unrecoverable
  // error) and should probably not be mutated further. Otherwise, we exit with
  // zero, which is for example needed for regression tests to make them "pass"
  // when no sandbox violation is detected.
  int status =
      SandboxTesting::mode() == SandboxTesting::Mode::kForFuzzing ? -1 : 0;
  _exit(status);
}

struct sigaction g_old_handlers[NSIG];
constexpr int kSignalsToHandle[] = {SIGABRT, SIGTRAP, SIGBUS, SIGILL, SIGSEGV};

std::atomic<bool> g_is_sandbox_violation{false};
#ifdef V8_USE_ADDRESS_SANITIZER
bool g_is_asan_fault_harmless = false;
#endif

void UninstallCrashFilter() {
  // NOTE: This code MUST be async-signal safe.
  // NO malloc or stdio is allowed here.

  // It's important that we always restore all signal handlers. For example, if
  // we forward a SIGSEGV to Asan's signal handler, that signal handler may
  // terminate the process with SIGABRT, which we must then *not* ignore.
  //
  // Should any of the sigaction calls below ever fail, the default signal
  // handler will be invoked (due to SA_RESETHAND) and will terminate the
  // process, so there's no need to attempt to handle that condition.
  for (int signal : kSignalsToHandle) {
    sigaction(signal, &g_old_handlers[signal], nullptr);
  }

  // We should also uninstall the sanitizer death callback as our crash filter
  // may hand a crash over to sanitizers, which should then not print the
  // sandbox violation message a second time.
#ifdef V8_USE_ANY_SANITIZER
  __sanitizer_set_death_callback(nullptr);
#endif  // V8_USE_ANY_SANITIZER
}

void ForwardToOldHandler(int signal, siginfo_t* info, void* context) {
  if (g_old_handlers[signal].sa_flags & SA_SIGINFO) {
    g_old_handlers[signal].sa_sigaction(signal, info, context);
  } else {
    auto handler = g_old_handlers[signal].sa_handler;
    if (handler != SIG_DFL && handler != SIG_IGN) {
      handler(signal);
    } else {
      // In this case we simply return, let the faulting instruction re-trigger
      // the crash, and then let the kernel handle the signal appropriately.
      return;
    }
  }
}

// Signal handler to check if a crash represents a sandbox violation or is a
// safe crash (e.g. because an access violation happened inside the sandbox).
void CrashFilter(int signal, siginfo_t* info, void* context) {
  // NOTE: This code MUST be async-signal safe.
  // NO malloc or stdio is allowed here.

#if V8_HAS_PKU_SUPPORT
  base::MemoryProtectionKey::SetDefaultPermissionsForAllKeysInSignalHandler();
#endif  // V8_HAS_PKU_SUPPORT

  Address faultaddr = reinterpret_cast<Address>(info->si_addr);
  // Conservatively assume we're dealing with a write access. We'll determine
  // the actual access type when we know the type of signal we caught.
  MemoryAccessType access_type = MemoryAccessType::kWrite;

  switch (signal) {
    case SIGABRT:
      // SIGABRT often indicates a failed CHECK or similar, which is harmless.
      FilterCrash("Caught harmless signal (SIGABRT).");
    case SIGTRAP:
      // Similarly, SIGTRAP may for example indicate UNREACHABLE code.
      FilterCrash("Caught harmless signal (SIGTRAP).");
    case SIGILL: {
      access_type = MemoryAccessType::kExecute;
      // In the case of SIGILL, faultaddr will point to the faulting
      // instruction.
      //
      // In general, SIGILL can be caused by either:
      // * A release-mode assertion fail (e.g. __builtin_unreachable()) for
      //   which the compiler generates a `udX` instruction. This is harmless.
      // * A bug causing us to execute random invalid machine code. This is bad.
      //
      // Here we try to detect which of these cases happened by looking at the
      // faulting instruction. This is a little sketchy as the read could fail.
      // However, the CPU has just attempted to execute the instruction, so it
      // _should_ be readable. If we ever see segfaults here, we could change it
      // to a "safe" read by e.g. using pipes and the read/write syscalls.
#ifdef V8_HOST_ARCH_X64
      uint8_t* code = reinterpret_cast<uint8_t*>(faultaddr);
      // "ud1" is 0x0f 0xb9 [...].
      if (code[0] == 0x0f && code[1] == 0xb9) {
        FilterCrash("Caught harmless signal (SIGILL caused by ud1).");
      }
      // "ud1" with any prefix (e.g. 0x67) is [Prefix] 0x0f 0xb9 [...].
      if (code[1] == 0x0f && code[2] == 0xb9) {
        FilterCrash("Caught harmless signal (SIGILL caused by ud1).");
      }
      // "ud2" is 0x0f 0x0b.
      if (code[0] == 0x0f && code[1] == 0x0b) {
        FilterCrash("Caught harmless signal (SIGILL caused by ud2).");
      }
#else
      PrintToStderr(
          "Cannot check for harmless SIGILL on this architecture. Please "
          "implement support for it.\n");
#endif  // V8_HOST_ARCH_X64
      break;
    }
    case SIGBUS:
    case SIGSEGV: {
      access_type = GetAccessType(context);

      // If we want to, we could merge most of the checks below (apart from the
      // check for non-canonical addresses) into the safe-region check.
      // However, the way they currently are is a bit more explicit and makes
      // it very clear what we consider to be "safe" crashes.

      if (Sandbox::current()->Contains(faultaddr)) {
        FilterCrash(
            "Caught harmless memory access violation (inside sandbox).");
      }

      if (info->si_code == SI_KERNEL && faultaddr == 0) {
        // This combination indicates a crash at a non-canonical address on some
        // architectures on Linux (e.g., x64). Crashes at non-canonical
        // addresses are for example caused by failed external pointer type
        // checks. Memory accesses that _always_ land at a non-canonical address
        // are not exploitable and so these are filtered out here. However,
        // testcases need to be written with this in mind and must cause crashes
        // at valid addresses.
        FilterCrash(
            "Caught harmless memory access violation (non-canonical address).");
      }

      if (faultaddr >= 0x8000'0000'0000'0000ULL) {
        // On Linux, it appears that the kernel will still report valid (i.e.
        // canonical) kernel space addresses via the si_addr field, so we need
        // to handle these separately. We've already filtered out non-canonical
        // addresses above, so here we can just test if the most-significant bit
        // of the address is set, and if so assume that it's a kernel address.
        FilterCrash(
            "Caught harmless memory access violation (kernel space address).");
      }

      if ((faultaddr >> Sandbox::kMaxVirtualAddressBitsForCrashFilter) != 0) {
        // On some architectures (e.g., ARM64) unaddressable dereferences
        // trigger SEGV_MAPERR and the actual faultaddr (instead of SI_KERNEL
        // with nullptr). Filter out similarly to the non-canonical case above.
        FilterCrash(
            "Caught harmless memory access violation (unaddressable access).");
      }

      if (faultaddr < 0x1000) {
        // Nullptr dereferences are harmless as nothing can be mapped there. We
        // use the typical page size (which is also the default value of
        // mmap_min_addr on Linux) to determine what counts as a nullptr
        // dereference here.
        FilterCrash(
            "Caught harmless memory access violation (nullptr dereference).");
      }

      if (faultaddr <
          (Sandbox::kSmiAddressRange + Sandbox::kSmiAddressRangePadding)) {
        // Currently we also ignore access violations in the first 4GB of the
        // virtual address space. See crbug.com/1470641 for more details.
        // We need to add some "padding" to the 4GB since we might access a
        // pointer that's < 4GB at an offset that makes the final address go
        // slightly above 4GB.
        FilterCrash(
            "Caught harmless memory access violation (first 4GB of virtual "
            "address space).");
      }

      if (IsCrashInSafeMemoryRegion(faultaddr, access_type)) {
        // There are a number of other memory regions where crashes are safe
        // (and can be caused with in-sandbox corruption). Examples include the
        // various pointer tables, where a (safe) OOB crash can be triggered by
        // corrupting an in-sandbox table handle. These regions are registered
        // with the crash filter and if we get here, we crashed in one of them.
        // TODO(42202821): consider printing more information here, for example
        // the address range and the name of the safe mapping.
        FilterCrash("Caught harmless memory access violation (safe region).");
      }

      // Stack overflow detection.
      //
      // On Linux, we generally have two types of stacks:
      //  1. The main thread's stack, allocated by the kernel, and
      //  2. The stacks of any other thread, allocated by the application
      //
      // These stacks differ in some ways, and that affects the way stack
      // overflows (caused e.g. by unbounded recursion) materialize: for (1) the
      // kernel will use a "gap" region below the stack segment, i.e. an
      // unmapped area into which the kernel itself will not place any mappings
      // and into which the stack cannot grow. A stack overflow therefore
      // crashes with a SEGV_MAPERR. On the other hand, for (2) the application
      // is responsible for allocating the stack and therefore also for
      // allocating any guard regions around it. As these guard regions must be
      // regular mappings (with PROT_NONE), a stack overflow will crash with a
      // SEGV_ACCERR.
      //
      // It's relatively hard to reliably and accurately detect stack overflow,
      // so here we use a simple heuristic: did we crash on any kind of access
      // violation on an address just below the current thread's stack region.
      // This may cause both false positives (e.g. an access not through the
      // stack pointer register that happens to also land just below the stack)
      // and false negatives (e.g. a stack overflow on the main thread that
      // "jumps over" the first page of the gap region), but is probably good
      // enough in practice.
      pthread_attr_t attr;
      int pthread_error = pthread_getattr_np(pthread_self(), &attr);
      if (!pthread_error) {
        uintptr_t stack_base;
        size_t stack_size;
        pthread_error = pthread_attr_getstack(
            &attr, reinterpret_cast<void**>(&stack_base), &stack_size);
        // The main thread's stack on Linux typically has a fairly large gap
        // region (1MB by default), but other thread's stacks usually have
        // smaller guard regions so here we're conservative and assume that the
        // guard region consists only of a single page.
        const size_t kMinStackGuardRegionSize = sysconf(_SC_PAGESIZE);
        uintptr_t stack_guard_region_start =
            stack_base - kMinStackGuardRegionSize;
        uintptr_t stack_guard_region_end = stack_base;
        if (!pthread_error && stack_guard_region_start <= faultaddr &&
            faultaddr < stack_guard_region_end) {
          FilterCrash("Caught harmless stack overflow.");
        }
      }

      break;
    }
    default:
      // This might happen if we add support for more signals, so report this
      // as a sandbox violation so it gets looked into.
      PrintToStderr("Unhandled signal");
      break;
  }

  // If we get here, we've detected a sandbox violation.
  PrintToStderr("\n## V8 sandbox violation detected!\n\n");

  g_is_sandbox_violation = true;

  if (access_type == MemoryAccessType::kRead) {
    PrintToStderr(
        "The sandbox violation was a *read* access which is technically not a "
        "sandbox violation. This requires manual investigation.\n");
  }

  // Restore the original signal handlers now so we don't get invoked again.
  // For example, the next signal handler in the chain (e.g. the ASAN handler)
  // might decide to terminate the process with abort() and so we must not be
  // catching (and ignoring) SIGABRT from now on.
  UninstallCrashFilter();

  // Forward to the previous handler. We could also just return now and let the
  // application crash again (assuming we return to the same, crashing
  // instruction). However, that doesn't work in combination with sandbox
  // hardware support: ASAN's crash handler will not know about the PKEY
  // permissions and that it must be doing something equivalent to
  // SetDefaultPermissionsForAllKeysInSignalHandler() so it would quickly crash
  // when e.g. trying to unwind the crashing thread's stack.
  ForwardToOldHandler(signal, info, context);

  // The old handler might itself decide to return to the faulting instruction
  // (after uninstalling itself), so we need to allow for that.
}

#ifdef V8_USE_ADDRESS_SANITIZER
namespace {

bool IsHarmlessMemcpyParamOverlap() {
  const void* src_addr = nullptr;
  size_t src_size = 0;
  const void* dest_addr = nullptr;
  size_t dest_size = 0;
  if (!__asan_get_report_src_address(&src_addr, &src_size) ||
      !__asan_get_report_dest_address(&dest_addr, &dest_size)) {
    PrintToStderr(
        "Warning: ASan report indicates a memcpy-param-overlap, but we "
        "couldn't obtain the src/dest ranges.\n");
    return false;
  }

  if (src_size == 0 || dest_size == 0) {
    PrintToStderr(
        "Warning: ASan report indicates a memcpy-param-overlap, but "
        "one or both of the sizes is 0.\n");
    return false;
  }

  Address src_begin = reinterpret_cast<Address>(src_addr);
  Address dest_begin = reinterpret_cast<Address>(dest_addr);
  Address src_last = src_begin + src_size - 1;
  Address dest_last = dest_begin + dest_size - 1;

  Sandbox* sandbox = Sandbox::current();
  return src_begin <= src_last && dest_begin <= dest_last &&
         sandbox->ReservationContains(src_begin) &&
         sandbox->ReservationContains(src_last) &&
         sandbox->ReservationContains(dest_begin) &&
         sandbox->ReservationContains(dest_last);
}

bool IsHarmlessASanFault(const char* description, Address faultaddr,
                         MemoryAccessType access_type) {
  if (description && strcmp(description, "memcpy-param-overlap") == 0) {
    if (IsHarmlessMemcpyParamOverlap()) {
      PrintToStderr(
          "Caught harmless ASan fault (overlapping memcpy safely contained "
          "in the sandbox).\n");
      return true;
    }
    // Otherwise, treat it as a sandbox violation.
    return false;
  }

  if (faultaddr == kNullAddress) {
    PrintToStderr(
        "Caught ASan fault without a fault address. Ignoring it as we cannot "
        "check if it is a sandbox violation.\n");
    return true;
  }

  if (IsCrashInSafeMemoryRegion(faultaddr, access_type)) {
    PrintToStderr("Caught harmless ASan fault (inside safe region).\n");
    return true;
  }

  return false;
}
}  // namespace

extern "C" V8_EXPORT_PRIVATE void __asan_on_error() {
  if (SandboxTesting::mode() == SandboxTesting::Mode::kDisabled) return;

  if (!__asan_report_present()) {
    // Should not occur normally, but falling back to treating this as an error
    // as defense-in-depth.
    g_is_sandbox_violation = true;
    return;
  }

  // Prevent unnecessary/confusing reporting if we already know the crash
  // classification.
  if (g_is_sandbox_violation) return;

  const char* const description = __asan_get_report_description();
  const Address faultaddr =
      reinterpret_cast<Address>(__asan_get_report_address());
  const MemoryAccessType access_type = __asan_get_report_access_type() == 0
                                           ? MemoryAccessType::kRead
                                           : MemoryAccessType::kWrite;
  if (IsHarmlessASanFault(description, faultaddr, access_type)) {
    g_is_asan_fault_harmless = true;
  } else {
    g_is_sandbox_violation = true;
  }
}
#endif  // V8_USE_ADDRESS_SANITIZER

#ifdef V8_USE_ANY_SANITIZER
void SanitizerFaultHandler() {
#ifdef V8_USE_ADDRESS_SANITIZER
  if (!g_is_sandbox_violation && !g_is_asan_fault_harmless) {
    PrintToStderr(
        "Warning: ASan death callback triggered before the fault could be "
        "classified. Falling back to treating it as a sandbox violation.\n");
  }

  if (g_is_asan_fault_harmless && !g_is_sandbox_violation) {
    PrintToStderr("Exiting process after harmless fault...\n");
    int status =
        SandboxTesting::mode() == SandboxTesting::Mode::kForFuzzing ? -1 : 0;
    _exit(status);
  }
#endif  // V8_USE_ADDRESS_SANITIZER

  // Sanitizers may report the failure via abort(), so we should also restore
  // the original signal handlers here.
  UninstallCrashFilter();

  // In case of a sanitizer issue we opt for conservatively reporting a sandbox
  // violation that needs to be investigated.
  PrintToStderr("\n## V8 sandbox violation detected!\n\n");
}
#endif  // V8_USE_ANY_SANITIZER

void InstallCrashFilter() {
  // Register an alternate stack for signal delivery so that signal handlers
  // can run properly even if for example the stack pointer has been corrupted
  // or the stack has overflowed.
  // Note that the alternate stack is currently only registered for the main
  // thread. Stack pointer corruption or stack overflows on background threads
  // may therefore still cause the signal handler to crash.
  base::OS::EnsureAlternativeSignalStackIsAvailableForCurrentThread();

  struct sigaction action;
  memset(&action, 0, sizeof(action));
  action.sa_flags = SA_SIGINFO | SA_ONSTACK;
  action.sa_sigaction = &CrashFilter;
  sigemptyset(&action.sa_mask);

  bool success = true;
  for (int signal : kSignalsToHandle) {
    success &= (sigaction(signal, &action, &g_old_handlers[signal]) == 0);
  }
  CHECK(success);

#ifdef V8_USE_ANY_SANITIZER
  // We install a sanitizer death callback. For ASan, this will check the flag
  // set by __asan_on_error to determine if the fault was harmless.
  //
  // The crash handler also resets the signal handler as sanitizer may use
  // `abort()` via `abort_on_error=1` option to signal problems.
  __sanitizer_set_death_callback(&SanitizerFaultHandler);
#endif  // V8_USE_ANY_SANITIZER
}

#endif  // V8_OS_LINUX
}  // namespace

void SandboxTesting::Enable(Mode mode) {
  CHECK_EQ(mode_, Mode::kDisabled);
  CHECK_NE(mode, Mode::kDisabled);
  CHECK(Sandbox::current()->is_initialized());

  mode_ = mode;

  fprintf(stderr,
          "Sandbox testing mode is enabled. Only sandbox violations will be "
          "reported, all other crashes will be ignored.\n");
  fprintf(stderr, "Sandbox bounds: [%p,%p)\n",
          reinterpret_cast<void*>(Sandbox::current()->base()),
          reinterpret_cast<void*>(Sandbox::current()->end()));

#ifdef V8_OS_LINUX
  InstallCrashFilter();
#else
  FATAL("The sandbox crash filter is currently only available on Linux");
#endif  // V8_OS_LINUX
}

void SandboxTesting::Disable() {
  if (mode_ == Mode::kDisabled) return;
  mode_ = Mode::kDisabled;

#ifdef V8_OS_LINUX
  UninstallCrashFilter();
#else
  FATAL("The sandbox crash filter is currently only available on Linux");
#endif  // V8_OS_LINUX
}

SandboxTesting::InstanceTypeMap& SandboxTesting::GetInstanceTypeMap() {
  // This mechanism is currently very crude and needs to be manually maintained
  // and extended (e.g. when adding a js test for the sandbox). In the future,
  // it would be nice to somehow automatically generate this map from the
  // object definitions and also support the class inheritance hierarchy.
  static base::LeakyObject<InstanceTypeMap> g_known_instance_types;
  auto& types = *g_known_instance_types.get();
  bool is_initialized = types.size() != 0;
  if (!is_initialized) {
    types["JS_OBJECT_TYPE"] = JS_OBJECT_TYPE;
    types["JS_FUNCTION_TYPE"] = JS_FUNCTION_TYPE;
    types["JS_BOUND_FUNCTION_TYPE"] = JS_BOUND_FUNCTION_TYPE;
    types["JS_ARRAY_TYPE"] = JS_ARRAY_TYPE;
    types["JS_ARRAY_BUFFER_TYPE"] = JS_ARRAY_BUFFER_TYPE;
    types["FOREIGN_TYPE"] = FOREIGN_TYPE;
    types["JS_REG_EXP_TYPE"] = JS_REG_EXP_TYPE;
    types["JS_TYPED_ARRAY_TYPE"] = JS_TYPED_ARRAY_TYPE;
    types["SEQ_ONE_BYTE_STRING_TYPE"] = SEQ_ONE_BYTE_STRING_TYPE;
    types["SEQ_TWO_BYTE_STRING_TYPE"] = SEQ_TWO_BYTE_STRING_TYPE;
    types["INTERNALIZED_ONE_BYTE_STRING_TYPE"] =
        INTERNALIZED_ONE_BYTE_STRING_TYPE;
    types["SLICED_ONE_BYTE_STRING_TYPE"] = SLICED_ONE_BYTE_STRING_TYPE;
    types["CONS_ONE_BYTE_STRING_TYPE"] = CONS_ONE_BYTE_STRING_TYPE;
    types["SHARED_FUNCTION_INFO_TYPE"] = SHARED_FUNCTION_INFO_TYPE;
    types["SCRIPT_TYPE"] = SCRIPT_TYPE;
    types["JS_PROMISE_TYPE"] = JS_PROMISE_TYPE;
    types["PROMISE_REACTION"] = PROMISE_REACTION_TYPE;
    types["JS_FUNCTION"] = JS_FUNCTION_TYPE;
    types["SHARED_FUNCTION_INFO"] = SHARED_FUNCTION_INFO_TYPE;
    types["FEEDBACK_CELL_TYPE"] = FEEDBACK_CELL_TYPE;
    types["FEEDBACK_VECTOR_TYPE"] = FEEDBACK_VECTOR_TYPE;
    types["FIXED_ARRAY_TYPE"] = FIXED_ARRAY_TYPE;
    types["FIXED_DOUBLE_ARRAY_TYPE"] = FIXED_DOUBLE_ARRAY_TYPE;
    types["WEAK_HOMOMORPHIC_FIXED_ARRAY_TYPE"] =
        WEAK_HOMOMORPHIC_FIXED_ARRAY_TYPE;
    types["NATIVE_CONTEXT_TYPE"] = NATIVE_CONTEXT_TYPE;
#ifdef V8_ENABLE_WEBASSEMBLY
    types["WASM_MODULE_OBJECT_TYPE"] = WASM_MODULE_OBJECT_TYPE;
    types["WASM_INSTANCE_OBJECT_TYPE"] = WASM_INSTANCE_OBJECT_TYPE;
    types["WASM_FUNC_REF_TYPE"] = WASM_FUNC_REF_TYPE;
    types["WASM_TABLE_OBJECT_TYPE"] = WASM_TABLE_OBJECT_TYPE;
    types["WASM_RESUME_DATA"] = WASM_RESUME_DATA_TYPE;
    types["WASM_TAG_OBJECT_TYPE"] = WASM_TAG_OBJECT_TYPE;
    types["WASM_GLOBAL_OBJECT_TYPE"] = WASM_GLOBAL_OBJECT_TYPE;
#endif  // V8_ENABLE_WEBASSEMBLY
  }
  return types;
}

SandboxTesting::FieldOffsetMap& SandboxTesting::GetFieldOffsetMap() {
  // This mechanism is currently very crude and needs to be manually maintained
  // and extended (e.g. when adding a js test for the sandbox). In the future,
  // it would be nice to somehow automatically generate this map from the
  // object definitions and also support the class inheritance hierarchy.
  static base::LeakyObject<FieldOffsetMap> g_known_fields;
  auto& fields = *g_known_fields.get();
  bool is_initialized = fields.size() != 0;
  if (!is_initialized) {
    fields[FIXED_DOUBLE_ARRAY_TYPE]["length"] =
        offsetof(FixedDoubleArray, length_);
    fields[FIXED_DOUBLE_ARRAY_TYPE]["data"] =
        FixedDoubleArray::OffsetOfElementAt(0);
    fields[JS_FUNCTION_TYPE]["dispatch_handle"] =
        offsetof(JSFunction, dispatch_handle_);
    fields[JS_FUNCTION_TYPE]["shared_function_info"] =
        offsetof(JSFunction, shared_function_info_);
    fields[JS_FUNCTION_TYPE]["feedback_cell"] =
        offsetof(JSFunction, feedback_cell_);
    fields[JS_FUNCTION_TYPE]["context"] = offsetof(JSFunction, context_);
    fields[JS_BOUND_FUNCTION_TYPE]["bound_arguments"] =
        offsetof(JSBoundFunction, bound_arguments_);
    fields[JS_ARRAY_TYPE]["elements"] = offsetof(JSObject, elements_);
    fields[JS_ARRAY_TYPE]["length"] = offsetof(JSArray, length_);
    fields[JS_ARRAY_BUFFER_TYPE]["extension"] =
        offsetof(JSArrayBuffer, extension_);
    fields[FOREIGN_TYPE]["foreign_address"] =
        offsetof(Foreign, foreign_address_);
    fields[JS_REG_EXP_TYPE]["data"] = offsetof(JSRegExp, data_);
    fields[JS_TYPED_ARRAY_TYPE]["byte_length"] =
        offsetof(JSArrayBufferView, raw_byte_length_);
    fields[JS_TYPED_ARRAY_TYPE]["byte_offset"] =
        offsetof(JSArrayBufferView, raw_byte_offset_);
    fields[JS_TYPED_ARRAY_TYPE]["external_pointer"] =
        offsetof(JSTypedArray, external_pointer_);
    fields[JS_TYPED_ARRAY_TYPE]["base_pointer"] =
        offsetof(JSTypedArray, base_pointer_);
    for (std::underlying_type_t<InstanceType> string_type = FIRST_STRING_TYPE;
         string_type <= LAST_STRING_TYPE; ++string_type) {
      InstanceType instance_type = static_cast<InstanceType>(string_type);
      fields[instance_type]["length"] = offsetof(String, length_);
      fields[instance_type]["hash"] = offsetof(String, raw_hash_field_);
      if (InstanceTypeChecker::IsExternalString(instance_type)) {
        fields[instance_type]["resource"] = offsetof(ExternalString, resource_);
        fields[instance_type]["resource_data"] =
            offsetof(ExternalString, resource_data_);
      }
    }
    fields[SLICED_ONE_BYTE_STRING_TYPE]["parent"] =
        offsetof(SlicedString, parent_);
    fields[SLICED_ONE_BYTE_STRING_TYPE]["offset"] =
        offsetof(SlicedString, offset_);
    fields[CONS_ONE_BYTE_STRING_TYPE]["first"] = offsetof(ConsString, first_);
    fields[CONS_ONE_BYTE_STRING_TYPE]["second"] = offsetof(ConsString, second_);
    fields[SHARED_FUNCTION_INFO_TYPE]["trusted_function_data"] =
        offsetof(SharedFunctionInfo, trusted_function_data_);
    fields[SHARED_FUNCTION_INFO_TYPE]["length"] =
        offsetof(SharedFunctionInfo, length_);
    fields[SHARED_FUNCTION_INFO_TYPE]["formal_parameter_count"] =
        offsetof(SharedFunctionInfo, formal_parameter_count_);
    fields[SHARED_FUNCTION_INFO_TYPE]["function_data"] =
        offsetof(SharedFunctionInfo, untrusted_function_data_);
    fields[SHARED_FUNCTION_INFO_TYPE]["script"] =
        offsetof(SharedFunctionInfo, script_);
    fields[SCRIPT_TYPE]["wasm_managed_native_module"] =
        offsetof(Script, eval_from_position_);
    fields[JS_PROMISE_TYPE]["reactions_or_result"] =
        offsetof(JSPromise, reactions_or_result_);
    fields[PROMISE_REACTION_TYPE]["fulfill_handler"] =
        offsetof(PromiseReaction, fulfill_handler_);
    fields[PROMISE_REACTION_TYPE]["reject_handler"] =
        offsetof(PromiseReaction, reject_handler_);
    fields[FEEDBACK_CELL_TYPE]["value"] = offsetof(FeedbackCell, value_);
    fields[FEEDBACK_VECTOR_TYPE]["length"] = offsetof(FeedbackVector, length_);
    fields[FEEDBACK_VECTOR_TYPE]["data"] =
        FeedbackVector::kRawFeedbackSlotsOffset;
    fields[FIXED_ARRAY_TYPE]["length"] = offsetof(FixedArray, length_);
    fields[FIXED_ARRAY_TYPE]["data"] = FixedArray::kHeaderSize;
    fields[WEAK_HOMOMORPHIC_FIXED_ARRAY_TYPE]["length"] =
        offsetof(WeakFixedArray, length_);
    fields[NATIVE_CONTEXT_TYPE]["microtask_queue"] =
        NativeContext::kMicrotaskQueueOffset;
#ifdef V8_INTL_SUPPORT
    fields[JS_SEGMENTS_TYPE]["icu_iterator_with_text"] =
        offsetof(JSSegments, icu_iterator_with_text_);
#endif  // V8_INTL_SUPPORT
#ifdef V8_ENABLE_WEBASSEMBLY
    fields[WASM_MODULE_OBJECT_TYPE]["managed_native_module"] =
        offsetof(WasmModuleObject, managed_native_module_);
    fields[WASM_MODULE_OBJECT_TYPE]["script"] =
        offsetof(WasmModuleObject, script_);
    fields[WASM_INSTANCE_OBJECT_TYPE]["module_object"] =
        offsetof(WasmInstanceObject, module_object_);
    fields[WASM_INSTANCE_OBJECT_TYPE]["trusted_data"] =
        offsetof(WasmInstanceObject, trusted_data_);
    fields[WASM_FUNC_REF_TYPE]["trusted_internal"] =
        offsetof(WasmFuncRef, trusted_internal_);
    fields[WASM_TABLE_OBJECT_TYPE]["entries"] =
        offsetof(WasmTableObject, entries_);
    fields[WASM_TABLE_OBJECT_TYPE]["current_length"] =
        offsetof(WasmTableObject, current_length_);
    fields[WASM_TABLE_OBJECT_TYPE]["maximum_length"] =
        offsetof(WasmTableObject, maximum_length_);
    fields[WASM_TABLE_OBJECT_TYPE]["raw_type"] =
        offsetof(WasmTableObject, raw_type_);
    fields[WASM_TABLE_OBJECT_TYPE]["trusted_dispatch_table"] =
        offsetof(WasmTableObject, trusted_dispatch_table_);
    fields[WASM_TABLE_OBJECT_TYPE]["trusted_data"] =
        offsetof(WasmTableObject, trusted_data_);
    fields[WASM_TAG_OBJECT_TYPE]["tag"] = offsetof(WasmTagObject, tag_);
    fields[WASM_GLOBAL_OBJECT_TYPE]["buffer"] =
        offsetof(WasmGlobalObject, buffer_);
    fields[WASM_GLOBAL_OBJECT_TYPE]["raw_type"] =
        offsetof(WasmGlobalObject, raw_type_);
    fields[WASM_RESUME_DATA_TYPE]["trusted_suspender"] =
        offsetof(WasmResumeData, trusted_suspender_);
#endif  // V8_ENABLE_WEBASSEMBLY
  }
  return fields;
}

std::optional<int> SandboxTesting::GetFieldOffset(
    v8::Isolate* isolate_for_errors, InstanceType instance_type,
    const std::string& field_name) {
  SandboxTesting::FieldOffsetMap& all_fields =
      SandboxTesting::GetFieldOffsetMap();
  auto fields_it = all_fields.find(instance_type);
  if (fields_it == all_fields.end()) {
    std::ostringstream error;
    error << "Unknown object type \"" << ToString(instance_type)
          << "\". If needed, add it in SandboxTesting::GetFieldOffsetMap";
    ThrowTypeError(isolate_for_errors, error.view());
    return std::nullopt;
  }

  SandboxTesting::FieldOffsets& obj_fields = fields_it->second;
  auto offset_it = obj_fields.find(field_name);
  if (offset_it == obj_fields.end()) {
    std::ostringstream error;
    error << "Unknown field \"" << field_name << "\" of instance type "
          << ToString(instance_type)
          << ". If needed, add it in SandboxTesting::GetFieldOffsetMap";
    ThrowTypeError(isolate_for_errors, error.view());
    return std::nullopt;
  }

  return offset_it->second;
}

#endif  // V8_ENABLE_SANDBOX

}  // namespace internal
}  // namespace v8