Commit b984b70c authored by Eric Holk's avatar Eric Holk Committed by Commit Bot

[wasm] Fall back on bounds checks when guarded memory unavailable

This CL adds the simplest version of a trap handler fallback. At
instantiation time, we check whether the module was compiled to use
trap handlers and the memory is guarded. If the memory is not guarded
but the module is supposed to use trap handlers, we recompile the
module with bounds checks so that we can use an unguarded memory.

The compiled module is replaced with a bounds checking version, meaning
future instances from this module will also use bounds checks.

Some likely desirable features that are current missing but can be
added future CLs include:
* Disabling trap handler mode entirely.
* Recompiling all old instances so that trap handler and bounds checked
  code does not coexist in the same process.

Bug: v8:7143

Change-Id: I161fc0d544133b07dc4a93cc6af813369aaf3efe
Reviewed-on: https://chromium-review.googlesource.com/1018182
Commit-Queue: Eric Holk <eholk@chromium.org>
Reviewed-by: 's avatarMichael Starzinger <mstarzinger@chromium.org>
Reviewed-by: 's avatarAndreas Haas <ahaas@chromium.org>
Cr-Commit-Position: refs/heads/master@{#53566}
parent 93d757a0
......@@ -599,6 +599,8 @@ DEFINE_BOOL(wasm_no_stack_checks, false,
DEFINE_BOOL(wasm_trap_handler, true,
"use signal handlers to catch out of bounds memory access in wasm"
" (currently Linux x86_64 only)")
DEFINE_BOOL(wasm_trap_handler_fallback, false,
"Use bounds checks if guarded memory is not available")
DEFINE_BOOL(wasm_fuzzer_gen_test, false,
"Generate a test case when running a wasm fuzzer")
DEFINE_IMPLICATION(wasm_fuzzer_gen_test, single_threaded)
......
......@@ -1077,5 +1077,13 @@ RUNTIME_FUNCTION(Runtime_FreezeWasmLazyCompilation) {
return isolate->heap()->undefined_value();
}
RUNTIME_FUNCTION(Runtime_WasmMemoryHasFullGuardRegion) {
DCHECK_EQ(1, args.length());
DisallowHeapAllocation no_gc;
CONVERT_ARG_CHECKED(WasmMemoryObject, memory, 0);
return isolate->heap()->ToBoolean(memory->has_full_guard_region(isolate));
}
} // namespace internal
} // namespace v8
......@@ -556,7 +556,8 @@ namespace internal {
F(ValidateWasmInstancesChain, 2, 1) \
F(ValidateWasmModuleState, 1, 1) \
F(WasmNumInterpretedCalls, 1, 1) \
F(WasmTraceMemory, 1, 1)
F(WasmTraceMemory, 1, 1) \
F(WasmMemoryHasFullGuardRegion, 1, 1)
#define FOR_EACH_INTRINSIC_TYPEDARRAY(F) \
F(ArrayBufferNeuter, 1, 1) \
......
This diff is collapsed.
......@@ -282,7 +282,9 @@ class V8_EXPORT_PRIVATE NativeModule final {
WasmCode* runtime_stub(WasmCode::RuntimeStubId index) const {
DCHECK_LT(index, WasmCode::kRuntimeStubCount);
return runtime_stub_table_[index];
WasmCode* code = runtime_stub_table_[index];
DCHECK_NOT_NULL(code);
return code;
}
// Register/release the protected instructions in all code objects with the
......
......@@ -104,9 +104,11 @@ bool WasmMemoryTracker::ReserveAddressSpace(size_t num_bytes) {
#if V8_TARGET_ARCH_MIPS64
// MIPS64 has a user space of 2^40 bytes on most processors,
// address space limits needs to be smaller.
constexpr size_t kAddressSpaceLimit = 0x2000000000L; // 128 GiB
constexpr size_t kAddressSpaceLimit = 0x2100000000L; // 132 GiB
#elif V8_TARGET_ARCH_64_BIT
constexpr size_t kAddressSpaceLimit = 0x10000000000L; // 1 TiB
// We set the limit to 1 TiB + 4 GiB so that there is room for mini-guards
// once we fill everything up with full-sized guard regions.
constexpr size_t kAddressSpaceLimit = 0x10100000000L; // 1 TiB + 4GiB
#else
constexpr size_t kAddressSpaceLimit = 0x80000000; // 2 GiB
#endif
......@@ -182,6 +184,21 @@ bool WasmMemoryTracker::IsWasmMemory(const void* buffer_start) {
return allocations_.find(buffer_start) != allocations_.end();
}
bool WasmMemoryTracker::HasFullGuardRegions(const void* buffer_start) {
base::LockGuard<base::Mutex> scope_lock(&mutex_);
const auto allocation = allocations_.find(buffer_start);
if (allocation == allocations_.end()) {
return false;
}
Address start = reinterpret_cast<Address>(buffer_start);
Address limit =
reinterpret_cast<Address>(allocation->second.allocation_base) +
allocation->second.allocation_length;
return start + kWasmMaxHeapOffset < limit;
}
bool WasmMemoryTracker::FreeMemoryIfIsWasmMemory(const void* buffer_start) {
if (IsWasmMemory(buffer_start)) {
const AllocationData allocation = ReleaseAllocation(buffer_start);
......@@ -245,7 +262,7 @@ MaybeHandle<JSArrayBuffer> NewArrayBuffer(Isolate* isolate, size_t size,
void* memory = TryAllocateBackingStore(memory_tracker, isolate->heap(), size,
require_full_guard_regions,
&allocation_base, &allocation_length);
if (memory == nullptr && !trap_handler::IsTrapHandlerEnabled()) {
if (memory == nullptr && FLAG_wasm_trap_handler_fallback) {
// If we failed to allocate with full guard regions, fall back on
// mini-guards.
require_full_guard_regions = false;
......
......@@ -69,6 +69,10 @@ class WasmMemoryTracker {
bool IsWasmMemory(const void* buffer_start);
// Returns whether the given buffer is a Wasm memory with guard regions large
// enough to safely use trap handlers.
bool HasFullGuardRegions(const void* buffer_start);
// Returns a pointer to a Wasm buffer's allocation data, or nullptr if the
// buffer is not tracked.
const AllocationData* FindAllocationData(const void* buffer_start);
......@@ -132,6 +136,9 @@ class WasmMemoryTracker {
DISALLOW_COPY_AND_ASSIGN(WasmMemoryTracker);
};
// Attempts to allocate an array buffer with guard regions suitable for trap
// handling. If address space is not available, it will return a buffer with
// mini-guards that will require bounds checks.
MaybeHandle<JSArrayBuffer> NewArrayBuffer(
Isolate*, size_t size, SharedFlag shared = SharedFlag::kNotShared);
......
......@@ -502,6 +502,15 @@ MaybeHandle<JSArrayBuffer> GrowMemoryBuffer(Isolate* isolate,
if (!wasm::NewArrayBuffer(isolate, new_size).ToHandle(&new_buffer)) {
return {};
}
wasm::WasmMemoryTracker* const memory_tracker =
isolate->wasm_engine()->memory_tracker();
// If the old buffer had full guard regions, we can only safely use the new
// buffer if it also has full guard regions. Otherwise, we'd have to
// recompile all the instances using this memory to insert bounds checks.
if (memory_tracker->HasFullGuardRegions(old_mem_start) &&
!memory_tracker->HasFullGuardRegions(new_buffer->backing_store())) {
return {};
}
if (old_size == 0) return new_buffer;
memcpy(new_buffer->backing_store(), old_mem_start, old_size);
DCHECK(old_buffer.is_null() || !old_buffer->is_shared());
......@@ -565,6 +574,30 @@ uint32_t WasmMemoryObject::current_pages() {
return byte_length / wasm::kWasmPageSize;
}
bool WasmMemoryObject::has_full_guard_region(Isolate* isolate) {
const wasm::WasmMemoryTracker::AllocationData* allocation =
isolate->wasm_engine()->memory_tracker()->FindAllocationData(
array_buffer()->backing_store());
CHECK_NOT_NULL(allocation);
Address allocation_base =
reinterpret_cast<Address>(allocation->allocation_base);
Address buffer_start = reinterpret_cast<Address>(allocation->buffer_start);
// Return whether the allocation covers every possible Wasm heap index.
//
// We always have the following relationship:
// allocation_base <= buffer_start <= buffer_start + memory_size <=
// allocation_base + allocation_length
// (in other words, the buffer fits within the allocation)
//
// The space between buffer_start + memory_size and allocation_base +
// allocation_length is the guard region. Here we make sure the guard region
// is large enough for any Wasm heap offset.
return buffer_start + wasm::kWasmMaxHeapOffset <=
allocation_base + allocation->allocation_length;
}
void WasmMemoryObject::AddInstance(Isolate* isolate,
Handle<WasmMemoryObject> memory,
Handle<WasmInstanceObject> instance) {
......
......@@ -210,6 +210,10 @@ class WasmMemoryObject : public JSObject {
uint32_t current_pages();
inline bool has_maximum_pages();
// Return whether the underlying backing store has guard regions large enough
// to be used with trap handlers.
bool has_full_guard_region(Isolate* isolate);
V8_EXPORT_PRIVATE static Handle<WasmMemoryObject> New(
Isolate* isolate, MaybeHandle<JSArrayBuffer> buffer, int32_t maximum);
......
......@@ -174,22 +174,28 @@ function testOOBThrows() {
testOOBThrows();
function testAddressSpaceLimit() {
// 1TiB, see wasm-memory.h
const kMaxAddressSpace = 1 * 1024 * 1024 * 1024 * 1024;
// 1TiB + 4 GiB, see wasm-memory.h
const kMaxAddressSpace = 1 * 1024 * 1024 * 1024 * 1024
+ 4 * 1024 * 1024 * 1024;
const kAddressSpacePerMemory = 8 * 1024 * 1024 * 1024;
let last_memory;
try {
let memories = [];
let address_space = 0;
while (address_space <= kMaxAddressSpace + 1) {
memories.push(new WebAssembly.Memory({initial: 1}));
last_memory = new WebAssembly.Memory({initial: 1})
memories.push(last_memory);
address_space += kAddressSpacePerMemory;
}
} catch (e) {
assertTrue(e instanceof RangeError);
return;
}
failWithMessage("allocated too much memory");
// If we get here it's because our fallback behavior is working. We may not
// be using the fallback, in which case we would have thrown a RangeError in
// the previous block.
assertTrue(!%WasmMemoryHasFullGuardRegion(last_memory));
}
if(%IsWasmTrapHandlerEnabled()) {
......
// Copyright 2018 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.
// Flags: --allow-natives-syntax --wasm-trap-handler-fallback
load("test/mjsunit/wasm/wasm-constants.js");
load("test/mjsunit/wasm/wasm-module-builder.js");
// Make sure we can get at least one guard region if the trap handler is enabled.
(function CanGetGuardRegionTest() {
print("CanGetGuardRegionTest()");
const memory = new WebAssembly.Memory({initial: 1});
if (%IsWasmTrapHandlerEnabled()) {
assertTrue(%WasmMemoryHasFullGuardRegion(memory));
}
})();
// This test verifies that when we have too many outstanding memories to get
// another fast memory, we fall back on bounds checking rather than failing.
(function TrapHandlerFallbackTest() {
print("TrapHandlerFallbackTest()");
let builder = new WasmModuleBuilder();
builder.addImportedMemory("mod", "imported_mem", 1);
builder.addFunction("load", kSig_i_i)
.addBody([kExprGetLocal, 0, kExprI32LoadMem, 0, 0])
.exportFunc();
let memory;
let instance;
let instances = [];
let fallback_occurred = false;
// Create 135 instances. V8 limits wasm to slightly more than 1 TiB of address
// space per isolate (see kAddressSpaceLimit in wasm-memory.cc), which allows
// up to 128 fast memories. As long as we create more than that, we should
// trigger the fallback behavior.
for (var i = 0; i < 135; i++) {
memory = new WebAssembly.Memory({initial: 1});
instance = builder.instantiate({mod: {imported_mem: memory}});
instances.push(instance);
assertTraps(kTrapMemOutOfBounds, () => instance.exports.load(1 << 20));
fallback_occurred = fallback_occurred || !%WasmMemoryHasFullGuardRegion(memory);
if (fallback_occurred) {
break;
}
}
assertTrue(fallback_occurred);
})();
(function TrapHandlerFallbackTestZeroInitialMemory() {
print("TrapHandlerFallbackTestZeroInitialMemory()");
let builder = new WasmModuleBuilder();
builder.addImportedMemory("mod", "imported_mem", 0);
builder.addFunction("load", kSig_i_i)
.addBody([kExprGetLocal, 0, kExprI32LoadMem, 0, 0])
.exportFunc();
let memory;
let instance;
let instances = [];
let fallback_occurred = false;
// Create 135 instances. V8 limits wasm to slightly more than 1 TiB of address
// space per isolate (see kAddressSpaceLimit in wasm-memory.cc), which allows
// up to 128 fast memories. As long as we create more than that, we should
// trigger the fallback behavior.
for (var i = 0; i < 135; i++) {
memory = new WebAssembly.Memory({initial: 1});
instance = builder.instantiate({mod: {imported_mem: memory}});
instances.push(instance);
assertTraps(kTrapMemOutOfBounds, () => instance.exports.load(1 << 20));
fallback_occurred = fallback_occurred || !%WasmMemoryHasFullGuardRegion(memory);
if (fallback_occurred) {
break;
}
}
assertTrue(fallback_occurred);
})();
(function TrapHandlerFallbackTestGrowFromZero() {
print("TrapHandlerFallbackTestGrowFromZero()");
// Create a zero-length memory to make sure the empty backing store is created.
const zero_memory = new WebAssembly.Memory({initial: 0});
// Create enough memories to overflow the address space limit
let memories = []
for (var i = 0; i < 135; i++) {
memories.push(new WebAssembly.Memory({initial: 1}));
}
// Create a memory for the module. We'll grow this later.
let memory = new WebAssembly.Memory({initial: 0});
let builder = new WasmModuleBuilder();
builder.addImportedMemory("mod", "imported_mem", 0);
builder.addFunction("load", kSig_i_i)
.addBody([kExprGetLocal, 0, kExprI32LoadMem, 0, 0])
.exportFunc();
instance = builder.instantiate({mod: {imported_mem: memory}});
assertTraps(kTrapMemOutOfBounds, () => instance.exports.load(1 << 20));
try {
memory.grow(1);
} catch(e) {
if (typeof e == typeof new RangeError) {
return;
}
throw e;
}
assertTraps(kTrapMemOutOfBounds, () => instance.exports.load(1 << 20));
})();
// Like TrapHandlerFallbackTest, but allows the module to be reused, so we only
// have to recompile once.
(function TrapHandlerFallbackTestReuseModule() {
print("TrapHandlerFallbackTestReuseModule()");
let builder = new WasmModuleBuilder();
builder.addImportedMemory("mod", "imported_mem", 1);
builder.addFunction("load", kSig_i_i)
.addBody([kExprGetLocal, 0, kExprI32LoadMem, 0, 0])
.exportFunc();
let memory;
let instance;
let instances = [];
let fallback_occurred = false;
// Create 135 instances. V8 limits wasm to slightly more than 1 TiB of address
// space per isolate (see kAddressSpaceLimit in wasm-memory.cc), which allows
// up to 128 fast memories. As long as we create more than that, we should
// trigger the fallback behavior.
const module = builder.toModule();
for (var i = 0; i < 135; i++) {
memory = new WebAssembly.Memory({initial: 1});
instance = new WebAssembly.Instance(module, {mod: {imported_mem: memory}});
instances.push(instance);
assertTraps(kTrapMemOutOfBounds, () => instance.exports.load(1 << 20));
fallback_occurred = fallback_occurred || !%WasmMemoryHasFullGuardRegion(memory);
if (fallback_occurred) {
break;
}
}
assertTrue(fallback_occurred);
})();
// Make sure that a bounds checked instance still works when calling an
// imported unchecked function.
(function CallIndirectImportTest() {
print("CallIndirectImportTest()");
// Create an unchecked instance that calls a function through an indirect
// table.
const instance_a = (() => {
const builder = new WasmModuleBuilder();
builder.addMemory(1, 1, false);
builder.addFunction("read_mem", kSig_i_i)
.addBody([
kExprGetLocal, 0,
kExprI32LoadMem, 0, 0
]).exportAs("read_mem");
return builder.instantiate();
})();
// Create new memories until we get one that is unguarded
let memories = [];
let memory;
for (var i = 0; i < 135; i++) {
memory = new WebAssembly.Memory({initial: 1});
memories.push(memory);
if (!%WasmMemoryHasFullGuardRegion(memory)) {
break;
}
}
assertFalse(%WasmMemoryHasFullGuardRegion(memory));
// create a module that imports a function through a table
const instance_b = (() => {
const builder = new WasmModuleBuilder();
builder.addFunction("main", kSig_i_i)
.addBody([
kExprGetLocal, 0,
kExprI32Const, 0,
kExprCallIndirect, 0, kTableZero
]).exportAs("main");
builder.addImportedTable("env", "table", 1, 1);
const module = new WebAssembly.Module(builder.toBuffer());
const table = new WebAssembly.Table({
element: "anyfunc",
initial: 1, maximum: 1
});
// Hook the new instance's export into the old instance's table.
table.set(0, instance_a.exports.read_mem);
return new WebAssembly.Instance(module, {'env': { 'table': table }});
})();
// Make sure we get an out of bounds still.
assertTraps(kTrapMemOutOfBounds, () => instance_b.exports.main(100000));
})();
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment