[sandbox] Add new Memory Corruption API

When enabled, this API exposes a new global 'Sandbox' object which
contains a number of functions and objects that in effect emulate
typical memory corruption primitives constructed by exploits. In
particular, the 'MemoryView' constructor can construct ArrayBuffers
instances that can corrupt arbitrary memory inside the sandbox. Further,
the getAddressOf(obj) and getSizeInBytesOf(obj) functions can be used
respectively to obtain the address (relative to the base of the sandbox)
and size of any HeapObject that can be accessed from JavaScript.

This API is useful for testing the sandbox, for example to
facilitate developing PoC sandbox escapes or writing regression tests.
In the future, it may also be used by custom V8 sandbox fuzzers.

Bug: v8:12878
Change-Id: I4e420b2ff28bd834b0693f1546942e51c71bfdda
Cq-Include-Trybots: luci.v8.try:v8_linux64_heap_sandbox_dbg_ng,v8_linux_arm64_sim_heap_sandbox_dbg_ng
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3650718
Reviewed-by: Igor Sheludko <ishell@chromium.org>
Commit-Queue: Samuel Groß <saelo@chromium.org>
Cr-Commit-Position: refs/heads/main@{#80659}
diff --git a/BUILD.bazel b/BUILD.bazel
index 42dd588..8d9f78c 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -2021,6 +2021,8 @@
         "src/sandbox/external-pointer-table.cc",
         "src/sandbox/external-pointer-table-inl.h",
         "src/sandbox/external-pointer-table.h",
+        "src/sandbox/testing.cc",
+        "src/sandbox/testing.h",
         "src/sandbox/sandbox.cc",
         "src/sandbox/sandbox.h",
         "src/sandbox/sandboxed-pointer-inl.h",
diff --git a/BUILD.gn b/BUILD.gn
index 1db8981..60924a1 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -315,6 +315,11 @@
   # Enable all available sandbox features. Implies v8_enable_sandbox.
   v8_enable_sandbox_future = false
 
+  # Expose the memory corruption API to JavaScript. Useful for testing the sandbox.
+  # WARNING This will expose builtins that (by design) cause memory corruption.
+  # Sets -DV8_EXPOSE_MEMORY_CORRUPTION_API
+  v8_expose_memory_corruption_api = false
+
   # Experimental feature for collecting per-class zone memory stats.
   # Requires use_rtti = true
   v8_enable_precise_zone_stats = false
@@ -549,6 +554,9 @@
 assert(!v8_enable_sandboxed_external_pointers || v8_enable_sandbox,
        "Sandboxed external pointers require the sandbox")
 
+assert(!v8_expose_memory_corruption_api || v8_enable_sandbox,
+       "The Memory Corruption API requires the sandbox")
+
 assert(
     !v8_enable_pointer_compression_shared_cage || v8_enable_pointer_compression,
     "Can't share a pointer compression cage if pointers aren't compressed")
@@ -1018,6 +1026,9 @@
   if (v8_fuchsia_use_vmex_resource) {
     defines += [ "V8_USE_VMEX_RESOURCE" ]
   }
+  if (v8_expose_memory_corruption_api) {
+    defines += [ "V8_EXPOSE_MEMORY_CORRUPTION_API" ]
+  }
 }
 
 config("toolchain") {
@@ -3418,6 +3429,7 @@
     "src/sandbox/sandbox.h",
     "src/sandbox/sandboxed-pointer-inl.h",
     "src/sandbox/sandboxed-pointer.h",
+    "src/sandbox/testing.h",
     "src/snapshot/code-serializer.h",
     "src/snapshot/context-deserializer.h",
     "src/snapshot/context-serializer.h",
@@ -4534,6 +4546,7 @@
     "src/runtime/runtime.cc",
     "src/sandbox/external-pointer-table.cc",
     "src/sandbox/sandbox.cc",
+    "src/sandbox/testing.cc",
     "src/snapshot/code-serializer.cc",
     "src/snapshot/context-deserializer.cc",
     "src/snapshot/context-serializer.cc",
diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index 7dd498f..700056f 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -24,6 +24,7 @@
 #include "src/logging/runtime-call-stats-scope.h"
 #include "src/objects/instance-type.h"
 #include "src/objects/objects.h"
+#include "src/sandbox/testing.h"
 #ifdef ENABLE_VTUNE_TRACEMARK
 #include "src/extensions/vtunedomain-support-extension.h"
 #endif  // ENABLE_VTUNE_TRACEMARK
@@ -5891,6 +5892,12 @@
   }
 #endif  // V8_ENABLE_WEBASSEMBLY
 
+#ifdef V8_EXPOSE_MEMORY_CORRUPTION_API
+  if (GetProcessWideSandbox()->is_initialized()) {
+    MemoryCorruptionApi::Install(isolate);
+  }
+#endif  // V8_EXPOSE_MEMORY_CORRUPTION_API
+
   return true;
 }
 
diff --git a/src/sandbox/sandbox.h b/src/sandbox/sandbox.h
index b358e46..077ce45 100644
--- a/src/sandbox/sandbox.h
+++ b/src/sandbox/sandbox.h
@@ -12,7 +12,6 @@
 #include "testing/gtest/include/gtest/gtest_prod.h"  // nogncheck
 
 namespace v8 {
-
 namespace internal {
 
 #ifdef V8_ENABLE_SANDBOX
diff --git a/src/sandbox/testing.cc b/src/sandbox/testing.cc
new file mode 100644
index 0000000..753343d
--- /dev/null
+++ b/src/sandbox/testing.cc
@@ -0,0 +1,194 @@
+// 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 "src/api/api-inl.h"
+#include "src/api/api-natives.h"
+#include "src/common/globals.h"
+#include "src/execution/isolate-inl.h"
+#include "src/heap/factory.h"
+#include "src/objects/backing-store.h"
+#include "src/objects/js-objects.h"
+#include "src/objects/templates.h"
+#include "src/sandbox/sandbox.h"
+
+namespace v8 {
+namespace internal {
+
+#ifdef V8_EXPOSE_MEMORY_CORRUPTION_API
+
+namespace {
+
+// Sandbox.byteLength
+void SandboxGetByteLength(const v8::FunctionCallbackInfo<v8::Value>& args) {
+  v8::Isolate* isolate = args.GetIsolate();
+  double sandbox_size = GetProcessWideSandbox()->size();
+  args.GetReturnValue().Set(v8::Number::New(isolate, sandbox_size));
+}
+
+// new Sandbox.MemoryView(args) -> Sandbox.MemoryView
+void SandboxMemoryView(const v8::FunctionCallbackInfo<v8::Value>& args) {
+  v8::Isolate* isolate = args.GetIsolate();
+  Local<v8::Context> context = isolate->GetCurrentContext();
+
+  if (!args.IsConstructCall()) {
+    isolate->ThrowError("Sandbox.MemoryView must be invoked with 'new'");
+    return;
+  }
+
+  Local<v8::Integer> arg1, arg2;
+  if (!args[0]->ToInteger(context).ToLocal(&arg1) ||
+      !args[1]->ToInteger(context).ToLocal(&arg2)) {
+    isolate->ThrowError("Expects two number arguments (start offset and size)");
+    return;
+  }
+
+  Sandbox* sandbox = GetProcessWideSandbox();
+  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::kNotShared);
+  if (!memory) {
+    isolate->ThrowError("Out of memory: MemoryView backing store");
+    return;
+  }
+  Handle<JSArrayBuffer> buffer = factory->NewJSArrayBuffer(std::move(memory));
+  args.GetReturnValue().Set(Utils::ToLocal(buffer));
+}
+
+// Sandbox.getAddressOf(object) -> Number
+void SandboxGetAddressOf(const v8::FunctionCallbackInfo<v8::Value>& args) {
+  v8::Isolate* isolate = args.GetIsolate();
+
+  if (args.Length() == 0) {
+    isolate->ThrowError("First argument must be provided");
+    return;
+  }
+
+  Handle<Object> arg = Utils::OpenHandle(*args[0]);
+  if (!arg->IsHeapObject()) {
+    isolate->ThrowError("First argument must be a HeapObject");
+    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>(HeapObject::cast(*arg).address());
+  args.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, address));
+}
+
+// Sandbox.getSizeOf(object) -> Number
+void SandboxGetSizeOf(const v8::FunctionCallbackInfo<v8::Value>& args) {
+  v8::Isolate* isolate = args.GetIsolate();
+
+  if (args.Length() == 0) {
+    isolate->ThrowError("First argument must be provided");
+    return;
+  }
+
+  Handle<Object> arg = Utils::OpenHandle(*args[0]);
+  if (!arg->IsHeapObject()) {
+    isolate->ThrowError("First argument must be a HeapObject");
+    return;
+  }
+
+  int size = HeapObject::cast(*arg).Size();
+  args.GetReturnValue().Set(v8::Integer::New(isolate, size));
+}
+
+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(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::DefineAccessor(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);
+}
+
+void InstallConstructor(Isolate* isolate, Handle<JSObject> holder,
+                        FunctionCallback func, const char* name,
+                        int num_parameters) {
+  InstallFunc(isolate, holder, func, name, num_parameters, true);
+}
+
+}  // namespace
+
+// static
+void MemoryCorruptionApi::Install(Isolate* isolate) {
+  CHECK(GetProcessWideSandbox()->is_initialized());
+
+  Factory* factory = isolate->factory();
+
+  // Create the special Sandbox object that provides read/write access to the
+  // sandbox address space alongside other miscellaneous functionality.
+  Handle<JSObject> sandbox =
+      factory->NewJSObject(isolate->object_function(), AllocationType::kOld);
+
+  InstallGetter(isolate, sandbox, SandboxGetByteLength, "byteLength");
+  InstallConstructor(isolate, sandbox, SandboxMemoryView, "MemoryView", 2);
+  InstallFunction(isolate, sandbox, SandboxGetAddressOf, "getAddressOf", 1);
+  InstallFunction(isolate, sandbox, SandboxGetSizeOf, "getSizeOf", 1);
+
+  // Install the Sandbox object as property on the global object.
+  Handle<JSGlobalObject> global = isolate->global_object();
+  Handle<String> name = factory->NewStringFromAsciiChecked("Sandbox");
+  JSObject::AddProperty(isolate, global, name, sandbox, DONT_ENUM);
+}
+
+#endif  // V8_EXPOSE_MEMORY_CORRUPTION_API
+
+}  // namespace internal
+}  // namespace v8
diff --git a/src/sandbox/testing.h b/src/sandbox/testing.h
new file mode 100644
index 0000000..4ab7637
--- /dev/null
+++ b/src/sandbox/testing.h
@@ -0,0 +1,28 @@
+// 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.
+
+#ifndef V8_SANDBOX_TESTING_H_
+#define V8_SANDBOX_TESTING_H_
+
+#include "src/common/globals.h"
+
+namespace v8 {
+namespace internal {
+
+#ifdef V8_EXPOSE_MEMORY_CORRUPTION_API
+// A JavaScript API that emulates typical exploit primitives.
+//
+// This can be used for testing the sandbox, for example to write regression
+// tests for bugs in the sandbox or to develop fuzzers.
+class MemoryCorruptionApi {
+ public:
+  V8_EXPORT_PRIVATE static void Install(Isolate* isolate);
+};
+
+#endif  // V8_EXPOSE_MEMORY_CORRUPTION_API
+
+}  // namespace internal
+}  // namespace v8
+
+#endif  // V8_SANDBOX_TESTING_H_