blob: fe18387b2bd6b7fecb66393bf938e1db7df0a6ab [file] [log] [blame]
// 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"
#ifdef V8_OS_LINUX
#include <signal.h>
#include <sys/mman.h>
#include <unistd.h>
#endif // V8_OS_LINUX
#ifdef V8_USE_ADDRESS_SANITIZER
#include <sanitizer/asan_interface.h>
#endif // V8_USE_ADDRESS_SANITIZER
namespace v8 {
namespace internal {
#ifdef V8_ENABLE_SANDBOX
SandboxTesting::Mode SandboxTesting::mode_ = SandboxTesting::Mode::kDisabled;
Address SandboxTesting::target_page_base_ = kNullAddress;
Address SandboxTesting::target_page_size_ = 0;
#ifdef V8_ENABLE_MEMORY_CORRUPTION_API
namespace {
// Sandbox.base
void SandboxGetBase(const v8::FunctionCallbackInfo<v8::Value>& info) {
DCHECK(ValidateCallbackInfo(info));
v8::Isolate* isolate = info.GetIsolate();
double sandbox_base = GetProcessWideSandbox()->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 = GetProcessWideSandbox()->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 = 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));
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 = GetProcessWideSandbox();
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);
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();
Tagged<HeapObject> obj;
if (!GetArgumentObjectPassedAsAddress(info, &obj)) {
return;
}
Heap* heap = reinterpret_cast<Isolate*>(isolate)->heap();
auto IsLocatedInMappedMemory = [&](Tagged<HeapObject> obj) {
// Note that IsOutsideAllocatedSpace is imprecise and may return false for
// some addresses outside the allocated space. However, it's probably good
// enough for our purposes.
return !heap->memory_allocator()->IsOutsideAllocatedSpace(obj.address());
};
bool is_valid = false;
if (IsLocatedInMappedMemory(obj)) {
Tagged<Map> map = obj->map();
if (IsLocatedInMappedMemory(map)) {
is_valid = IsMap(map);
}
}
info.GetReturnValue().Set(is_valid);
}
static void SandboxIsWritableImpl(
const v8::FunctionCallbackInfo<v8::Value>& info,
ArgumentObjectExtractorFunction getArgumentObject) {
DCHECK(ValidateCallbackInfo(info));
Tagged<HeapObject> obj;
if (!getArgumentObject(info, &obj)) {
return;
}
MemoryChunkMetadata* chunk = MemoryChunkMetadata::FromHeapObject(obj);
bool is_writable = chunk->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;
}
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;
}
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;
}
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);
}
// 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.
void SandboxGetFieldOffsetImpl(
const v8::FunctionCallbackInfo<v8::Value>& info,
ArgumentObjectExtractorFunction getArgumentObject) {
DCHECK(ValidateCallbackInfo(info));
v8::Isolate* isolate = info.GetIsolate();
Tagged<HeapObject> obj;
if (!getArgumentObject(info, &obj)) {
return;
}
InstanceType instance_type = Cast<HeapObject>(*obj)->map()->instance_type();
v8::String::Utf8Value field_name(isolate, info[1]);
if (!*field_name) {
isolate->ThrowError("Second argument must be a string");
return;
}
auto& all_fields = SandboxTesting::GetFieldOffsetMap();
if (all_fields.find(instance_type) == all_fields.end()) {
isolate->ThrowError(
"Unknown object type. If needed, add it in "
"SandboxTesting::GetFieldOffsetMap");
return;
}
auto& obj_fields = all_fields[instance_type];
if (obj_fields.find(*field_name) == obj_fields.end()) {
isolate->ThrowError(
"Unknown field. If needed, add it in "
"SandboxTesting::GetFieldOffsetMap");
return;
}
int offset = obj_fields[*field_name];
info.GetReturnValue().Set(offset);
}
// Sandbox.getFieldOffsetOf(Object, String) -> Number
void SandboxGetFieldOffsetOf(const v8::FunctionCallbackInfo<v8::Value>& info) {
SandboxGetFieldOffsetImpl(info, &GetArgumentObjectPassedAsReference);
}
// Sandbox.getFieldOffsetOfObjectAt(Number, String) -> Number
void SandboxGetFieldOffsetOfObjectAt(
const v8::FunctionCallbackInfo<v8::Value>& info) {
SandboxGetFieldOffsetImpl(info, &GetArgumentObjectPassedAsAddress);
}
// Sandbox.targetPage
void SandboxGetTargetPage(const v8::FunctionCallbackInfo<v8::Value>& info) {
DCHECK(ValidateCallbackInfo(info));
v8::Isolate* isolate = info.GetIsolate();
Address page = SandboxTesting::target_page_base();
CHECK_NE(page, kNullAddress);
info.GetReturnValue().Set(v8::Number::New(isolate, page));
}
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);
}
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::InstallMemoryCorruptionApiIfEnabled(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
if (!IsEnabled()) return;
CHECK(GetProcessWideSandbox()->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, 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, SandboxGetFieldOffsetOf, "getFieldOffsetOf",
2);
InstallFunction(isolate, sandbox, SandboxGetFieldOffsetOfObjectAt,
"getFieldOffsetOfObjectAt", 2);
if (mode() == Mode::kForTesting) {
InstallGetter(isolate, sandbox, SandboxGetTargetPage, "targetPage");
}
// 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);
}
#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);
}
[[noreturn]] void FilterCrash(const char* reason) {
// NOTE: This code MUST be async-signal safe.
// NO malloc or stdio is allowed here.
PrintToStderr(reason);
// 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);
}
// Signal handler checking whether a memory access violation happened inside or
// outside of the sandbox address space. If inside, the signal is ignored and
// the process terminated normally, in the latter case the original signal
// handler is restored and the signal delivered again.
struct sigaction g_old_sigabrt_handler, g_old_sigtrap_handler,
g_old_sigbus_handler, g_old_sigsegv_handler;
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.
sigaction(SIGABRT, &g_old_sigabrt_handler, nullptr);
sigaction(SIGTRAP, &g_old_sigtrap_handler, nullptr);
sigaction(SIGBUS, &g_old_sigbus_handler, nullptr);
sigaction(SIGSEGV, &g_old_sigsegv_handler, nullptr);
// We should also uninstall the sanitizer death callback as our crash filter
// may hand a crash over to ASan, which should then not enter our crash
// filtering logic a second time.
#ifdef V8_USE_ADDRESS_SANITIZER
__sanitizer_set_death_callback(nullptr);
#endif
}
void CrashFilter(int signal, siginfo_t* info, void* void_context) {
// NOTE: This code MUST be async-signal safe.
// NO malloc or stdio is allowed here.
if (signal == SIGABRT) {
// SIGABRT typically indicates a failed CHECK or similar, which is harmless.
FilterCrash("Caught harmless signal (SIGABRT). Exiting process...\n");
}
if (signal == SIGTRAP) {
// Similarly, SIGTRAP may for example indicate UNREACHABLE code.
FilterCrash("Caught harmless signal (SIGTRAP). Exiting process...\n");
}
Address faultaddr = reinterpret_cast<Address>(info->si_addr);
if (GetProcessWideSandbox()->Contains(faultaddr)) {
FilterCrash(
"Caught harmless memory access violaton (inside sandbox address "
"space). Exiting process...\n");
}
if (info->si_code == SI_KERNEL && faultaddr == 0) {
// This combination appears to indicate a crash at a non-canonical address
// on Linux. 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 violaton (non-canonical address). "
"Exiting process...\n");
}
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 violatation (kernel space address). "
"Exiting process...\n");
}
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 violaton (nullptr dereference). Exiting "
"process...\n");
}
if (faultaddr < 4ULL * GB) {
// Currently we also ignore access violations in the first 4GB of the
// virtual address space. See crbug.com/1470641 for more details.
FilterCrash(
"Caught harmless memory access violaton (first 4GB of virtual address "
"space). Exiting process...\n");
}
// 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. Exiting process...\n");
}
}
if (info->si_code == SEGV_ACCERR &&
!SandboxTesting::IsInsideTargetPage(faultaddr)) {
// This indicates an access to a valid mapping but with insufficient
// permissions, for example accessing a region mapped with PROT_NONE, or
// writing to a read-only mapping.
//
// The sandbox relies on such accesses crashing in a safe way in some
// cases. For example, the accesses into the various pointer tables are not
// bounds checked, but instead it is guaranteed that an out-of-bounds
// access will hit a PROT_NONE mapping.
//
// Memory accesses that _always_ cause such a permission violation are not
// exploitable and the crashes are therefore filtered out here. However,
// testcases need to be written with this behavior in mind and should
// typically try to access non-existing memory to demonstrate the ability
// to escape from the sandbox.
//
// An exception to this rule is the target page: in sandbox testing mode a
// write access to this read-only page is considered a sandbox bypass.
FilterCrash(
"Caught harmless memory access violaton (memory permission violation). "
"Exiting process...\n");
}
if (SandboxTesting::mode() == SandboxTesting::Mode::kForTesting) {
// In sandbox testing mode, the access violation must happen on a specific
// page and it must be a write access. Check for this now.
// As the target page is mapped as read-only, a SEGV will only be raised
// for a write (or execute) access, so we do not need to check for this.
if (!SandboxTesting::IsInsideTargetPage(faultaddr)) {
FilterCrash(
"Detected invalid sandbox violation (not on target page). Exiting "
"process...\n");
}
}
// Otherwise it's a sandbox violation, so restore the original signal
// handlers, then return from this handler. The faulting instruction will be
// re-executed and will again trigger the access violation, but now the
// signal will be handled by the original signal handler.
UninstallCrashFilter();
PrintToStderr("\n## V8 sandbox violation detected!\n\n");
}
#ifdef V8_USE_ADDRESS_SANITIZER
void AsanFaultHandler() {
Address faultaddr = reinterpret_cast<Address>(__asan_get_report_address());
if (faultaddr == kNullAddress) {
FilterCrash(
"Caught ASan fault without a fault address. Ignoring it as we cannot "
"check if it is a sandbox violation. Exiting process...\n");
}
if (GetProcessWideSandbox()->Contains(faultaddr)) {
FilterCrash(
"Caught harmless ASan fault (inside sandbox address space). Exiting "
"process...\n");
}
// Asan may report the failure via abort(), so we should also restore the
// original signal handlers here.
UninstallCrashFilter();
PrintToStderr("\n## V8 sandbox violation detected!\n\n");
}
#endif // V8_USE_ADDRESS_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.
VirtualAddressSpace* vas = GetPlatformVirtualAddressSpace();
Address alternate_stack =
vas->AllocatePages(VirtualAddressSpace::kNoHint, SIGSTKSZ,
vas->page_size(), PagePermissions::kReadWrite);
CHECK_NE(alternate_stack, kNullAddress);
stack_t signalstack = {
.ss_sp = reinterpret_cast<void*>(alternate_stack),
.ss_flags = 0,
.ss_size = static_cast<size_t>(SIGSTKSZ),
};
CHECK_EQ(sigaltstack(&signalstack, nullptr), 0);
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;
success &= (sigaction(SIGABRT, &action, &g_old_sigabrt_handler) == 0);
success &= (sigaction(SIGTRAP, &action, &g_old_sigtrap_handler) == 0);
success &= (sigaction(SIGBUS, &action, &g_old_sigbus_handler) == 0);
success &= (sigaction(SIGSEGV, &action, &g_old_sigsegv_handler) == 0);
CHECK(success);
#if defined(V8_USE_ADDRESS_SANITIZER)
__sanitizer_set_death_callback(&AsanFaultHandler);
#elif defined(V8_USE_MEMORY_SANITIZER) || \
defined(V8_USE_UNDEFINED_BEHAVIOR_SANITIZER)
// TODO(saelo): can we also test for the other sanitizers here somehow?
FATAL("The sandbox crash filter currently only supports AddressSanitizer");
#endif
}
#endif // V8_OS_LINUX
} // namespace
void SandboxTesting::Enable(Mode mode) {
CHECK_EQ(mode_, Mode::kDisabled);
CHECK_NE(mode, Mode::kDisabled);
CHECK(GetProcessWideSandbox()->is_initialized());
mode_ = mode;
if (mode == Mode::kForTesting) {
#ifdef V8_USE_ADDRESS_SANITIZER
// This doesn't make sense: ASan would catch many bugs early on, before
// they can be turned into an arbitrary write primitive.
FATAL(
"The sandbox testing mode is currently incompatible with "
"AddressSanitizer");
#else
// Map the target address that must be written to to demonstrate a sandbox
// bypass. A simple way to enforce that the access is a write (or execute)
// access is by mapping the page readable. That way, read accesses do not
// cause a crash and so won't be seen by the crash filter at all.
VirtualAddressSpace* vas = GetPlatformVirtualAddressSpace();
target_page_size_ = vas->page_size();
target_page_base_ =
vas->AllocatePages(vas->RandomPageAddress(), target_page_size_,
target_page_size_, PagePermissions::kRead);
CHECK_NE(target_page_base_, kNullAddress);
fprintf(stderr,
"Sandbox testing mode is enabled. Write to the page starting at "
"0x%" V8PRIxPTR
" (available from JavaScript as `Sandbox.targetPage`) to "
"demonstrate a sandbox bypass.\n",
target_page_base_);
#endif // V8_USE_ADDRESS_SANITIZER
} else {
fprintf(stderr,
"Sandbox fuzzing mode is enabled. Only sandbox violations will be "
"reported, all other crashes will be ignored.\n");
}
#ifdef V8_OS_LINUX
InstallCrashFilter();
#else
FATAL("The sandbox crash filter is currently only available on Linux");
#endif // V8_OS_LINUX
}
bool SandboxTesting::IsInsideTargetPage(Address faultaddr) {
return base::IsInHalfOpenRange(faultaddr, target_page_base(),
target_page_base() + target_page_size());
}
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[JS_ARRAY_TYPE]["length"] = JSArray::kLengthOffset;
fields[SLICED_ONE_BYTE_STRING_TYPE]["parent"] =
offsetof(SlicedString, parent_);
}
return fields;
}
#endif // V8_ENABLE_SANDBOX
} // namespace internal
} // namespace v8