// 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 #include #elif V8_OS_MACOSX #include #include #elif V8_OS_WIN #include #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 info) { switch (info.param) { case kDefault: return "DefaultTrapHandler"; case kCallback: return "Callback"; } UNREACHABLE(); } class TrapHandlerTest : public TestWithIsolate, public ::testing::WithParamInterface { 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
(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
(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(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( trap_handler::GetThreadInWasmThreadLocalAddress()); } // Execute the code in buffer. void ExecuteBuffer() { buffer_->MakeExecutable(); GeneratedCode::FromAddress( i_isolate(), reinterpret_cast
(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::FromAddress(i_isolate(), reinterpret_cast
(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 buffer_; // Buffer for the code for the landing pad of the test handler. std::unique_ptr 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
(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
(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
(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
(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
(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
(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