blob: 16593709995009ad44b8ca7ecd695b4a7090334b [file] [log] [blame]
// 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.
#include "include/v8config.h"
#if V8_OS_LINUX
#include <signal.h>
#include <ucontext.h>
#elif V8_OS_MACOSX
#include <signal.h>
#include <sys/ucontext.h>
#elif V8_OS_WIN
#include <windows.h>
#endif
#include "testing/gtest/include/gtest/gtest.h"
#if V8_OS_POSIX
#include "include/v8-wasm-trap-handler-posix.h"
#elif V8_OS_WIN
#include "include/v8-wasm-trap-handler-win.h"
#endif
#include "src/base/page-allocator.h"
#include "src/codegen/assembler-inl.h"
#include "src/codegen/macro-assembler-inl.h"
#include "src/execution/simulator.h"
#include "src/trap-handler/trap-handler.h"
#include "src/utils/allocation.h"
#include "src/utils/vector.h"
#include "src/wasm/wasm-engine.h"
#include "src/wasm/wasm-memory.h"
#include "test/common/assembler-tester.h"
#include "test/unittests/test-utils.h"
namespace v8 {
namespace internal {
namespace wasm {
namespace {
constexpr Register scratch = r10;
bool g_test_handler_executed = false;
#if V8_OS_LINUX || V8_OS_MACOSX
struct sigaction g_old_segv_action;
struct sigaction g_old_fpe_action;
struct sigaction g_old_bus_action; // We get SIGBUS on Mac sometimes.
#elif V8_OS_WIN
void* g_registered_handler = nullptr;
#endif
// The recovery address allows us to recover from an intentional crash.
Address g_recovery_address;
// Flag to indicate if the test handler should call the trap handler as a first
// chance handler.
bool g_use_as_first_chance_handler = false;
} // namespace
#define __ masm.
enum TrapHandlerStyle : int {
// The test uses the default trap handler of V8.
kDefault = 0,
// The test installs the trap handler callback in its own test handler.
kCallback = 1
};
std::string PrintTrapHandlerTestParam(
::testing::TestParamInfo<TrapHandlerStyle> info) {
switch (info.param) {
case kDefault:
return "DefaultTrapHandler";
case kCallback:
return "Callback";
}
UNREACHABLE();
}
class TrapHandlerTest : public TestWithIsolate,
public ::testing::WithParamInterface<TrapHandlerStyle> {
protected:
void SetUp() override {
void* base = nullptr;
size_t length = 0;
accessible_memory_start_ =
i_isolate()
->wasm_engine()
->memory_tracker()
->TryAllocateBackingStoreForTesting(
i_isolate()->heap(), 1 * kWasmPageSize, &base, &length);
memory_buffer_ =
base::AddressRegion(reinterpret_cast<Address>(base), length);
// The allocated memory buffer ends with a guard page.
crash_address_ = memory_buffer_.end() - 32;
// Allocate a buffer for the generated code.
buffer_ = AllocateAssemblerBuffer(AssemblerBase::kMinimalBufferSize,
GetRandomMmapAddr());
InitRecoveryCode();
#if V8_OS_LINUX || V8_OS_MACOSX
// Set up a signal handler to recover from the expected crash.
struct sigaction action;
action.sa_sigaction = SignalHandler;
sigemptyset(&action.sa_mask);
action.sa_flags = SA_SIGINFO;
// SIGSEGV happens for wasm oob memory accesses on Linux.
CHECK_EQ(0, sigaction(SIGSEGV, &action, &g_old_segv_action));
// SIGBUS happens for wasm oob memory accesses on macOS.
CHECK_EQ(0, sigaction(SIGBUS, &action, &g_old_bus_action));
// SIGFPE to simulate crashes which are not handled by the trap handler.
CHECK_EQ(0, sigaction(SIGFPE, &action, &g_old_fpe_action));
#elif V8_OS_WIN
g_registered_handler =
AddVectoredExceptionHandler(/*first=*/0, TestHandler);
#endif
}
void TearDown() override {
// We should always have left wasm code.
CHECK(!GetThreadInWasmFlag());
buffer_.reset();
recovery_buffer_.reset();
// Free the allocated backing store.
i_isolate()->wasm_engine()->memory_tracker()->FreeBackingStoreForTesting(
memory_buffer_, accessible_memory_start_);
// Clean up the trap handler
trap_handler::RemoveTrapHandler();
if (!g_test_handler_executed) {
#if V8_OS_LINUX || V8_OS_MACOSX
// The test handler cleans up the signal handler setup in the test. If the
// test handler was not called, we have to do the cleanup ourselves.
CHECK_EQ(0, sigaction(SIGSEGV, &g_old_segv_action, nullptr));
CHECK_EQ(0, sigaction(SIGFPE, &g_old_fpe_action, nullptr));
CHECK_EQ(0, sigaction(SIGBUS, &g_old_bus_action, nullptr));
#elif V8_OS_WIN
RemoveVectoredExceptionHandler(g_registered_handler);
g_registered_handler = nullptr;
#endif
}
}
void InitRecoveryCode() {
// Create a code snippet where we can jump to to recover from a signal or
// exception. The code snippet only consists of a return statement.
recovery_buffer_ = AllocateAssemblerBuffer(
AssemblerBase::kMinimalBufferSize, GetRandomMmapAddr());
MacroAssembler masm(nullptr, AssemblerOptions{}, CodeObjectRequired::kNo,
recovery_buffer_->CreateView());
int recovery_offset = __ pc_offset();
__ Pop(scratch);
__ Ret();
CodeDesc desc;
masm.GetCode(nullptr, &desc);
recovery_buffer_->MakeExecutable();
g_recovery_address =
reinterpret_cast<Address>(desc.buffer + recovery_offset);
}
#if V8_OS_LINUX || V8_OS_MACOSX
static void SignalHandler(int signal, siginfo_t* info, void* context) {
if (g_use_as_first_chance_handler) {
if (v8::TryHandleWebAssemblyTrapPosix(signal, info, context)) {
return;
}
}
// Reset the signal handler, to avoid that this signal handler is called
// repeatedly.
sigaction(SIGSEGV, &g_old_segv_action, nullptr);
sigaction(SIGFPE, &g_old_fpe_action, nullptr);
sigaction(SIGBUS, &g_old_bus_action, nullptr);
g_test_handler_executed = true;
// Set the $rip to the recovery code.
ucontext_t* uc = reinterpret_cast<ucontext_t*>(context);
#if V8_OS_LINUX
uc->uc_mcontext.gregs[REG_RIP] = g_recovery_address;
#else // V8_OS_MACOSX
uc->uc_mcontext->__ss.__rip = g_recovery_address;
#endif
}
#endif
#if V8_OS_WIN
static LONG WINAPI TestHandler(EXCEPTION_POINTERS* exception) {
if (g_use_as_first_chance_handler) {
if (v8::TryHandleWebAssemblyTrapWindows(exception)) {
return EXCEPTION_CONTINUE_EXECUTION;
}
}
RemoveVectoredExceptionHandler(g_registered_handler);
g_registered_handler = nullptr;
g_test_handler_executed = true;
exception->ContextRecord->Rip = g_recovery_address;
return EXCEPTION_CONTINUE_EXECUTION;
}
#endif
public:
void SetupTrapHandler(TrapHandlerStyle style) {
bool use_default_handler = style == kDefault;
g_use_as_first_chance_handler = !use_default_handler;
CHECK(v8::V8::EnableWebAssemblyTrapHandler(use_default_handler));
}
void GenerateSetThreadInWasmFlagCode(MacroAssembler* masm) {
masm->Move(scratch,
i_isolate()->thread_local_top()->thread_in_wasm_flag_address_,
RelocInfo::NONE);
masm->movl(MemOperand(scratch, 0), Immediate(1));
}
void GenerateResetThreadInWasmFlagCode(MacroAssembler* masm) {
masm->Move(scratch,
i_isolate()->thread_local_top()->thread_in_wasm_flag_address_,
RelocInfo::NONE);
masm->movl(MemOperand(scratch, 0), Immediate(0));
}
bool GetThreadInWasmFlag() {
return *reinterpret_cast<int*>(
trap_handler::GetThreadInWasmThreadLocalAddress());
}
// Execute the code in buffer.
void ExecuteBuffer() {
buffer_->MakeExecutable();
GeneratedCode<void>::FromAddress(
i_isolate(), reinterpret_cast<Address>(buffer_->start()))
.Call();
CHECK(!g_test_handler_executed);
}
// Execute the code in buffer. We expect a crash which we recover from in the
// test handler.
void ExecuteExpectCrash(TestingAssemblerBuffer* buffer,
bool check_wasm_flag = true) {
CHECK(!g_test_handler_executed);
buffer->MakeExecutable();
GeneratedCode<void>::FromAddress(i_isolate(),
reinterpret_cast<Address>(buffer->start()))
.Call();
CHECK(g_test_handler_executed);
g_test_handler_executed = false;
if (check_wasm_flag) CHECK(!GetThreadInWasmFlag());
}
bool test_handler_executed() { return g_test_handler_executed; }
// Allocated memory which corresponds to wasm memory with guard regions.
base::AddressRegion memory_buffer_;
// Address within the guard region of the wasm memory. Accessing this memory
// address causes a signal or exception.
Address crash_address_;
// The start of the accessible region in the allocated memory. This pointer is
// needed to de-register the memory from the wasm memory tracker again.
void* accessible_memory_start_;
// Buffer for generated code.
std::unique_ptr<TestingAssemblerBuffer> buffer_;
// Buffer for the code for the landing pad of the test handler.
std::unique_ptr<TestingAssemblerBuffer> recovery_buffer_;
};
TEST_P(TrapHandlerTest, TestTrapHandlerRecovery) {
// Test that the wasm trap handler can recover a memory access violation in
// wasm code (we fake the wasm code and the access violation).
MacroAssembler masm(nullptr, AssemblerOptions{}, CodeObjectRequired::kNo,
buffer_->CreateView());
__ Push(scratch);
GenerateSetThreadInWasmFlagCode(&masm);
__ Move(scratch, crash_address_, RelocInfo::NONE);
int crash_offset = __ pc_offset();
__ testl(MemOperand(scratch, 0), Immediate(1));
int recovery_offset = __ pc_offset();
GenerateResetThreadInWasmFlagCode(&masm);
__ Pop(scratch);
__ Ret();
CodeDesc desc;
masm.GetCode(nullptr, &desc);
SetupTrapHandler(GetParam());
trap_handler::ProtectedInstructionData protected_instruction{crash_offset,
recovery_offset};
trap_handler::RegisterHandlerData(reinterpret_cast<Address>(desc.buffer),
desc.instr_size, 1, &protected_instruction);
ExecuteBuffer();
}
TEST_P(TrapHandlerTest, TestReleaseHandlerData) {
// Test that after we release handler data in the trap handler, it cannot
// recover from the specific memory access violation anymore.
MacroAssembler masm(nullptr, AssemblerOptions{}, CodeObjectRequired::kNo,
buffer_->CreateView());
__ Push(scratch);
GenerateSetThreadInWasmFlagCode(&masm);
__ Move(scratch, crash_address_, RelocInfo::NONE);
int crash_offset = __ pc_offset();
__ testl(MemOperand(scratch, 0), Immediate(1));
int recovery_offset = __ pc_offset();
GenerateResetThreadInWasmFlagCode(&masm);
__ Pop(scratch);
__ Ret();
CodeDesc desc;
masm.GetCode(nullptr, &desc);
trap_handler::ProtectedInstructionData protected_instruction{crash_offset,
recovery_offset};
int handler_id = trap_handler::RegisterHandlerData(
reinterpret_cast<Address>(desc.buffer), desc.instr_size, 1,
&protected_instruction);
SetupTrapHandler(GetParam());
ExecuteBuffer();
// Deregister from the trap handler. The trap handler should not do the
// recovery now.
trap_handler::ReleaseHandlerData(handler_id);
ExecuteExpectCrash(buffer_.get());
}
TEST_P(TrapHandlerTest, TestNoThreadInWasmFlag) {
// That that if the thread_in_wasm flag is not set, the trap handler does not
// get active.
MacroAssembler masm(nullptr, AssemblerOptions{}, CodeObjectRequired::kNo,
buffer_->CreateView());
__ Push(scratch);
__ Move(scratch, crash_address_, RelocInfo::NONE);
int crash_offset = __ pc_offset();
__ testl(MemOperand(scratch, 0), Immediate(1));
int recovery_offset = __ pc_offset();
__ Pop(scratch);
__ Ret();
CodeDesc desc;
masm.GetCode(nullptr, &desc);
trap_handler::ProtectedInstructionData protected_instruction{crash_offset,
recovery_offset};
trap_handler::RegisterHandlerData(reinterpret_cast<Address>(desc.buffer),
desc.instr_size, 1, &protected_instruction);
SetupTrapHandler(GetParam());
ExecuteExpectCrash(buffer_.get());
}
TEST_P(TrapHandlerTest, TestCrashInWasmNoProtectedInstruction) {
// Test that if the crash in wasm happened at an instruction which is not
// protected, then the trap handler does not handle it.
MacroAssembler masm(nullptr, AssemblerOptions{}, CodeObjectRequired::kNo,
buffer_->CreateView());
__ Push(scratch);
GenerateSetThreadInWasmFlagCode(&masm);
int no_crash_offset = __ pc_offset();
__ Move(scratch, crash_address_, RelocInfo::NONE);
__ testl(MemOperand(scratch, 0), Immediate(1));
// Offset where the crash is not happening.
int recovery_offset = __ pc_offset();
GenerateResetThreadInWasmFlagCode(&masm);
__ Pop(scratch);
__ Ret();
CodeDesc desc;
masm.GetCode(nullptr, &desc);
trap_handler::ProtectedInstructionData protected_instruction{no_crash_offset,
recovery_offset};
trap_handler::RegisterHandlerData(reinterpret_cast<Address>(desc.buffer),
desc.instr_size, 1, &protected_instruction);
SetupTrapHandler(GetParam());
ExecuteExpectCrash(buffer_.get());
}
TEST_P(TrapHandlerTest, TestCrashInWasmWrongCrashType) {
// Test that if the crash reason is not a memory access violation, then the
// wasm trap handler does not handle it.
MacroAssembler masm(nullptr, AssemblerOptions{}, CodeObjectRequired::kNo,
buffer_->CreateView());
__ Push(scratch);
GenerateSetThreadInWasmFlagCode(&masm);
__ xorq(scratch, scratch);
int crash_offset = __ pc_offset();
__ divq(scratch);
// Offset where the crash is not happening.
int recovery_offset = __ pc_offset();
GenerateResetThreadInWasmFlagCode(&masm);
__ Pop(scratch);
__ Ret();
CodeDesc desc;
masm.GetCode(nullptr, &desc);
trap_handler::ProtectedInstructionData protected_instruction{crash_offset,
recovery_offset};
trap_handler::RegisterHandlerData(reinterpret_cast<Address>(desc.buffer),
desc.instr_size, 1, &protected_instruction);
SetupTrapHandler(GetParam());
#if V8_OS_POSIX
// On Posix, the V8 default trap handler does not register for SIGFPE,
// therefore the thread-in-wasm flag is never reset in this test. We
// therefore do not check the value of this flag.
bool check_wasm_flag = GetParam() != kDefault;
#elif V8_OS_WIN
// On Windows, the trap handler returns immediately if not an exception of
// interest.
bool check_wasm_flag = false;
#else
bool check_wasm_flag = true;
#endif
ExecuteExpectCrash(buffer_.get(), check_wasm_flag);
if (!check_wasm_flag) {
// Reset the thread-in-wasm flag because it was probably not reset in the
// trap handler.
*trap_handler::GetThreadInWasmThreadLocalAddress() = 0;
}
}
class CodeRunner : public v8::base::Thread {
public:
CodeRunner(TrapHandlerTest* test, TestingAssemblerBuffer* buffer)
: Thread(Options("CodeRunner")), test_(test), buffer_(buffer) {}
void Run() override { test_->ExecuteExpectCrash(buffer_); }
private:
TrapHandlerTest* test_;
TestingAssemblerBuffer* buffer_;
};
TEST_P(TrapHandlerTest, TestCrashInOtherThread) {
// Test setup:
// The current thread enters wasm land (sets the thread_in_wasm flag)
// A second thread crashes at a protected instruction without having the flag
// set.
MacroAssembler masm(nullptr, AssemblerOptions{}, CodeObjectRequired::kNo,
buffer_->CreateView());
__ Push(scratch);
__ Move(scratch, crash_address_, RelocInfo::NONE);
int crash_offset = __ pc_offset();
__ testl(MemOperand(scratch, 0), Immediate(1));
int recovery_offset = __ pc_offset();
__ Pop(scratch);
__ Ret();
CodeDesc desc;
masm.GetCode(nullptr, &desc);
trap_handler::ProtectedInstructionData protected_instruction{crash_offset,
recovery_offset};
trap_handler::RegisterHandlerData(reinterpret_cast<Address>(desc.buffer),
desc.instr_size, 1, &protected_instruction);
SetupTrapHandler(GetParam());
CodeRunner runner(this, buffer_.get());
CHECK(!GetThreadInWasmFlag());
// Set the thread-in-wasm flag manually in this thread.
*trap_handler::GetThreadInWasmThreadLocalAddress() = 1;
runner.Start();
runner.Join();
CHECK(GetThreadInWasmFlag());
// Reset the thread-in-wasm flag.
*trap_handler::GetThreadInWasmThreadLocalAddress() = 0;
}
INSTANTIATE_TEST_SUITE_P(/* no prefix */, TrapHandlerTest,
::testing::Values(kDefault, kCallback),
PrintTrapHandlerTestParam);
#undef __
} // namespace wasm
} // namespace internal
} // namespace v8