| // 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 |