// 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/allocation.h" #include "src/assembler-inl.h" #include "src/base/page-allocator.h" #include "src/macro-assembler-inl.h" #include "src/simulator.h" #include "src/trap-handler/trap-handler.h" #include "src/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. size_t buffer_size; byte* buffer = AllocateAssemblerBuffer( &buffer_size, AssemblerBase::kMinimalBufferSize, GetRandomMmapAddr()); buffer_ = Vector<byte>(buffer, buffer_size); 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()); FreeAssemblerBuffer(buffer_.start(), buffer_.size()); FreeAssemblerBuffer(recovery_buffer_.start(), recovery_buffer_.size()); // 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. size_t buffer_size; byte* buffer = AllocateAssemblerBuffer( &buffer_size, AssemblerBase::kMinimalBufferSize, GetRandomMmapAddr()); recovery_buffer_ = Vector<byte>(buffer, buffer_size); MacroAssembler masm(nullptr, AssemblerOptions{}, recovery_buffer_.start(), recovery_buffer_.length(), CodeObjectRequired::kNo); int recovery_offset = __ pc_offset(); __ Pop(scratch); __ Ret(); CodeDesc desc; masm.GetCode(nullptr, &desc); MakeAssemblerBufferExecutable(recovery_buffer_.start(), recovery_buffer_.size()); 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(Vector<byte> buffer) { MakeAssemblerBufferExecutable(buffer.start(), buffer.size()); 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(Vector<byte> buffer, bool check_wasm_flag = true) { CHECK(!g_test_handler_executed); MakeAssemblerBufferExecutable(buffer.start(), buffer.size()); 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. Vector<byte> buffer_; // Buffer for the code for the landing pad of the test handler. Vector<byte> 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{}, buffer_.start(), buffer_.length(), CodeObjectRequired::kNo); __ 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(buffer_); } 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{}, buffer_.start(), buffer_.length(), CodeObjectRequired::kNo); __ 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(buffer_); // Deregister from the trap handler. The trap handler should not do the // recovery now. trap_handler::ReleaseHandlerData(handler_id); ExecuteExpectCrash(buffer_); } 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{}, buffer_.start(), buffer_.length(), CodeObjectRequired::kNo); __ 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_); } 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{}, buffer_.start(), buffer_.length(), CodeObjectRequired::kNo); __ 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_); } 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{}, buffer_.start(), buffer_.length(), CodeObjectRequired::kNo); __ 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 // 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; #else bool check_wasm_flag = true; #endif ExecuteExpectCrash(buffer_, 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, Vector<byte> buffer) : Thread(Options("CodeRunner")), test_(test), buffer_(buffer) {} void Run() override { test_->ExecuteExpectCrash(buffer_); } private: TrapHandlerTest* test_; Vector<byte> 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{}, buffer_.start(), buffer_.length(), CodeObjectRequired::kNo); __ 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_); 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_CASE_P(/* no prefix */, TrapHandlerTest, ::testing::Values(kDefault, kCallback), PrintTrapHandlerTestParam); #undef __ } // namespace wasm } // namespace internal } // namespace v8