// 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 || V8_OS_FREEBSD
#include <signal.h>
#include <ucontext.h>
#elif V8_OS_DARWIN
#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/base/vector.h"
#include "src/codegen/assembler-inl.h"
#include "src/codegen/macro-assembler-inl.h"
#include "src/execution/simulator.h"
#include "src/objects/backing-store.h"
#include "src/trap-handler/trap-handler.h"
#include "src/utils/allocation.h"
#include "src/wasm/wasm-engine.h"
#include "test/common/assembler-tester.h"
#include "test/unittests/test-utils.h"

#if V8_TRAP_HANDLER_SUPPORTED

#if V8_HOST_ARCH_ARM64 && (!V8_OS_LINUX && !V8_OS_DARWIN && !V8_OS_WIN)
#error Unsupported platform
#endif

namespace v8 {
namespace internal {
namespace wasm {

namespace {
#if V8_HOST_ARCH_X64
constexpr Register scratch = r10;
#endif
bool g_test_handler_executed = false;
#if V8_OS_LINUX || V8_OS_DARWIN || V8_OS_FREEBSD
struct sigaction g_old_segv_action;
struct sigaction g_old_other_action;  // FPE or TRAP, depending on x64 or arm64.
struct sigaction g_old_bus_action;    // We get SIGBUS on Mac sometimes.
#elif V8_OS_WIN
void* g_registered_handler = nullptr;
#endif

// 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 TestWithPlatform,
                        public ::testing::WithParamInterface<TrapHandlerStyle> {
 protected:
  void SetUp() override {
    InstallFallbackHandler();
    // Set up the trap handler before allocating an isolate. Isolate allocation
    // registers the `WasmNull` unaccessible range with the trap handler *only*
    // if the trap handler is enabled at this point.
    SetupTrapHandler(GetParam());

    array_buffer_allocator_ = v8::ArrayBuffer::Allocator::NewDefaultAllocator();
    v8::Isolate::CreateParams create_params;
    create_params.array_buffer_allocator = array_buffer_allocator_;

    isolate_ = v8::Isolate::New(create_params);
    CHECK_NOT_NULL(isolate_);

    backing_store_ = BackingStore::AllocateWasmMemory(
        i_isolate(), 1, 1, WasmMemoryFlag::kWasmMemory32, SharedFlag{false});
    CHECK(backing_store_);
    EXPECT_TRUE(backing_store_->has_guard_regions());
    // The allocated backing store ends with a guard page.
    crash_address_ = reinterpret_cast<Address>(backing_store_->buffer_start()) +
                     backing_store_->byte_length() + 32;
    // Allocate a buffer for the generated code.
    buffer_ = AllocateAssemblerBuffer(AssemblerBase::kDefaultBufferSize,
                                      GetRandomMmapAddr());
  }

  void InstallFallbackHandler() {
#if V8_OS_LINUX || V8_OS_DARWIN || V8_OS_FREEBSD
    // 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 | SA_ONSTACK;
    // SIGSEGV happens for wasm oob memory accesses on Linux.
    EXPECT_EQ(0, sigaction(SIGSEGV, &action, &g_old_segv_action));
    // SIGBUS happens for wasm oob memory accesses on macOS.
    EXPECT_EQ(0, sigaction(SIGBUS, &action, &g_old_bus_action));
#if V8_HOST_ARCH_X64
    // SIGFPE to simulate crashes which are not handled by the trap handler.
    EXPECT_EQ(0, sigaction(SIGFPE, &action, &g_old_other_action));
#elif V8_HOST_ARCH_ARM64
    // SIGTRAP to simulate crashes which are not handled by the trap handler.
    EXPECT_EQ(0, sigaction(SIGTRAP, &action, &g_old_other_action));
#elif V8_HOST_ARCH_LOONG64
    // SIGTRAP to simulate crashes which are not handled by the trap handler.
    EXPECT_EQ(0, sigaction(SIGTRAP, &action, &g_old_other_action));
#elif V8_HOST_ARCH_RISCV64
    // SIGTRAP to simulate crashes which are not handled by the trap handler.
    EXPECT_EQ(0, sigaction(SIGTRAP, &action, &g_old_other_action));
#else
#error Unsupported platform
#endif
#elif V8_OS_WIN
    g_registered_handler =
        AddVectoredExceptionHandler(/*first=*/0, TestHandler);
#endif
  }

  void TearDown() override {
    buffer_.reset();
    recovery_buffer_.reset();
    backing_store_.reset();
    isolate_->Dispose();
    delete array_buffer_allocator_;

    // Clean up the trap handler
    trap_handler::RemoveTrapHandler();
    if (!g_test_handler_executed) {
#if V8_OS_LINUX || V8_OS_DARWIN || V8_OS_FREEBSD
      // 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.
      EXPECT_EQ(0, sigaction(SIGSEGV, &g_old_segv_action, nullptr));
      EXPECT_EQ(0, sigaction(SIGBUS, &g_old_bus_action, nullptr));
#if V8_HOST_ARCH_X64
      EXPECT_EQ(0, sigaction(SIGFPE, &g_old_other_action, nullptr));
#elif V8_HOST_ARCH_ARM64
      EXPECT_EQ(0, sigaction(SIGTRAP, &g_old_other_action, nullptr));
#elif V8_HOST_ARCH_LOONG64
      EXPECT_EQ(0, sigaction(SIGTRAP, &g_old_other_action, nullptr));
#elif V8_HOST_ARCH_RISCV64
      EXPECT_EQ(0, sigaction(SIGTRAP, &g_old_other_action, nullptr));
#else
#error Unsupported platform
#endif
#elif V8_OS_WIN
      RemoveVectoredExceptionHandler(g_registered_handler);
      g_registered_handler = nullptr;
#endif
    }
  }

  static void RecoveryHandler() { return; }

#if V8_OS_LINUX || V8_OS_DARWIN || V8_OS_FREEBSD
  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);
#if V8_HOST_ARCH_X64
    sigaction(SIGFPE, &g_old_other_action, nullptr);
#elif V8_HOST_ARCH_ARM64
    sigaction(SIGTRAP, &g_old_other_action, nullptr);
#elif V8_HOST_ARCH_LOONG64
    sigaction(SIGTRAP, &g_old_other_action, nullptr);
#elif V8_HOST_ARCH_RISCV64
    sigaction(SIGTRAP, &g_old_other_action, nullptr);
#else
#error Unsupported platform
#endif
    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);
    uintptr_t recovery_handler = reinterpret_cast<uintptr_t>(&RecoveryHandler);
#if V8_OS_DARWIN && V8_HOST_ARCH_ARM64
    uc->uc_mcontext->__ss.__pc = recovery_handler;
#elif V8_OS_DARWIN && V8_HOST_ARCH_X64
    uc->uc_mcontext->__ss.__rip = recovery_handler;
#elif V8_OS_LINUX && V8_HOST_ARCH_ARM64
    uc->uc_mcontext.pc = recovery_handler;
#elif V8_OS_LINUX && V8_HOST_ARCH_LOONG64
    uc->uc_mcontext.__pc = recovery_handler;
#elif V8_OS_LINUX && V8_HOST_ARCH_RISCV64
    uc->uc_mcontext.__gregs[REG_PC] = recovery_handler;
#elif V8_OS_LINUX && V8_HOST_ARCH_X64
    uc->uc_mcontext.gregs[REG_RIP] = recovery_handler;
#elif V8_OS_FREEBSD
    uc->uc_mcontext.mc_rip = recovery_handler;
#else
#error Unsupported platform
#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;
    uintptr_t recovery_handler = reinterpret_cast<uintptr_t>(&RecoveryHandler);
#if V8_HOST_ARCH_X64
    exception->ContextRecord->Rip = recovery_handler;
#elif V8_HOST_ARCH_ARM64
    exception->ContextRecord->Pc = recovery_handler;
#else
#error Unsupported architecture
#endif  // V8_HOST_ARCH_X64
    return EXCEPTION_CONTINUE_EXECUTION;
  }
#endif

  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));
  }

 public:
  Isolate* i_isolate() const { return reinterpret_cast<Isolate*>(isolate_); }

  // Execute the code in buffer.
  void ExecuteBuffer() {
    buffer_->MakeExecutable();
    GeneratedCode<void>::FromAddress(
        i_isolate(), reinterpret_cast<Address>(buffer_->start()))
        .Call();
    EXPECT_FALSE(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) {
    EXPECT_FALSE(g_test_handler_executed);
    buffer->MakeExecutable();
    GeneratedCode<void>::FromAddress(i_isolate(),
                                     reinterpret_cast<Address>(buffer->start()))
        .Call();
    EXPECT_TRUE(g_test_handler_executed);
    g_test_handler_executed = false;
  }

  bool test_handler_executed() { return g_test_handler_executed; }

  // The array buffer allocator and the isolate used in the tests.
  v8::ArrayBuffer::Allocator* array_buffer_allocator_;
  v8::Isolate* isolate_;

  // The backing store used for testing the trap handler.
  std::unique_ptr<BackingStore> backing_store_;

  // Address within the guard region of the wasm memory. Accessing this memory
  // address causes a signal or exception.
  Address crash_address_;

  // 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_;
};

// TODO(almuthanna): These tests were skipped because they cause a crash when
// they are ran on Fuchsia. This issue should be solved later on
// Ticket: https://crbug.com/1028617
#if !defined(V8_TARGET_OS_FUCHSIA)

namespace {

void (*landing_pad)() = nullptr;

DISABLE_CFI_ICALL void LandingPadTrampoline() { landing_pad(); }
}  // namespace

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(i_isolate(), AssemblerOptions{},
                      CodeObjectRequired{false}, buffer_->CreateView());
#if V8_HOST_ARCH_X64
  __ Move(scratch, crash_address_, RelocInfo::NO_INFO);
  uint32_t crash_offset = __ pc_offset();
  __ testl(MemOperand(scratch, 0), Immediate(1));
  uint32_t recovery_offset = __ pc_offset();
#elif V8_HOST_ARCH_ARM64
  UseScratchRegisterScope temps(&masm);
  Register scratch = temps.AcquireX();
  __ Mov(scratch, crash_address_);
  uint32_t crash_offset = __ pc_offset();
  __ Ldr(scratch, MemOperand(scratch));
  uint32_t recovery_offset = __ pc_offset();
#elif V8_HOST_ARCH_LOONG64
  UseScratchRegisterScope temps(&masm);
  Register scratch = temps.Acquire();
  __ li(scratch, static_cast<int64_t>(crash_address_));
  uint32_t crash_offset = __ pc_offset();
  __ Ld_d(scratch, MemOperand(scratch, 0));
  uint32_t recovery_offset = __ pc_offset();
#elif V8_HOST_ARCH_RISCV64
  UseScratchRegisterScope temps(&masm);
  Register scratch = temps.Acquire();
  __ li(scratch, static_cast<int64_t>(crash_address_));
  uint32_t crash_offset = __ pc_offset();
  __ LoadWord(scratch, MemOperand(scratch, 0));
  uint32_t recovery_offset = __ pc_offset();
#else
#error Unsupported platform
#endif
  __ Ret();
  CodeDesc desc;
  masm.GetCode(static_cast<LocalIsolate*>(nullptr), &desc);

  trap_handler::TrappingInstructionData protected_instruction{crash_offset};
  trap_handler::RegisterHandlerData(reinterpret_cast<Address>(desc.buffer),
                                    desc.instr_size, 1, &protected_instruction);

  landing_pad =
      reinterpret_cast<void (*)()>(buffer_->start() + recovery_offset);
  trap_handler::SetLandingPad(
      reinterpret_cast<uintptr_t>(&LandingPadTrampoline));
  ExecuteBuffer();
  trap_handler::SetLandingPad(0);
}

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(i_isolate(), AssemblerOptions{},
                      CodeObjectRequired{false}, buffer_->CreateView());
#if V8_HOST_ARCH_X64
  __ Move(scratch, crash_address_, RelocInfo::NO_INFO);
  uint32_t crash_offset = __ pc_offset();
  __ testl(MemOperand(scratch, 0), Immediate(1));
  uint32_t recovery_offset = __ pc_offset();
#elif V8_HOST_ARCH_ARM64
  UseScratchRegisterScope temps(&masm);
  Register scratch = temps.AcquireX();
  __ Mov(scratch, crash_address_);
  uint32_t crash_offset = __ pc_offset();
  __ Ldr(scratch, MemOperand(scratch));
  uint32_t recovery_offset = __ pc_offset();
#elif V8_HOST_ARCH_LOONG64
  UseScratchRegisterScope temps(&masm);
  Register scratch = temps.Acquire();
  __ li(scratch, static_cast<int64_t>(crash_address_));
  uint32_t crash_offset = __ pc_offset();
  __ Ld_d(scratch, MemOperand(scratch, 0));
  uint32_t recovery_offset = __ pc_offset();
#elif V8_HOST_ARCH_RISCV64
  UseScratchRegisterScope temps(&masm);
  Register scratch = temps.Acquire();
  __ li(scratch, static_cast<int64_t>(crash_address_));
  uint32_t crash_offset = __ pc_offset();
  __ LoadWord(scratch, MemOperand(scratch, 0));
  uint32_t recovery_offset = __ pc_offset();
#else
#error Unsupported platform
#endif
  __ Ret();
  CodeDesc desc;
  masm.GetCode(static_cast<LocalIsolate*>(nullptr), &desc);

  trap_handler::TrappingInstructionData protected_instruction{crash_offset};
  int handler_id = trap_handler::RegisterHandlerData(
      reinterpret_cast<Address>(desc.buffer), desc.instr_size, 1,
      &protected_instruction);

  landing_pad =
      reinterpret_cast<void (*)()>(buffer_->start() + recovery_offset);
  trap_handler::SetLandingPad(
      reinterpret_cast<uintptr_t>(&LandingPadTrampoline));
  ExecuteBuffer();
  // Deregister from the trap handler. The trap handler should not do the
  // recovery now.
  trap_handler::ReleaseHandlerData(handler_id);

  ExecuteExpectCrash(buffer_.get());
  trap_handler::SetLandingPad(0);
}

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(i_isolate(), AssemblerOptions{},
                      CodeObjectRequired{false}, buffer_->CreateView());
#if V8_HOST_ARCH_X64
  uint32_t no_crash_offset = __ pc_offset();
  __ Move(scratch, crash_address_, RelocInfo::NO_INFO);
  __ testl(MemOperand(scratch, 0), Immediate(1));
#elif V8_HOST_ARCH_ARM64
  UseScratchRegisterScope temps(&masm);
  Register scratch = temps.AcquireX();
  uint32_t no_crash_offset = __ pc_offset();
  __ Mov(scratch, crash_address_);
  __ Ldr(scratch, MemOperand(scratch));
#elif V8_HOST_ARCH_LOONG64
  UseScratchRegisterScope temps(&masm);
  Register scratch = temps.Acquire();
  uint32_t no_crash_offset = __ pc_offset();
  __ li(scratch, static_cast<int64_t>(crash_address_));
  __ Ld_d(scratch, MemOperand(scratch, 0));
#elif V8_HOST_ARCH_RISCV64
  UseScratchRegisterScope temps(&masm);
  Register scratch = temps.Acquire();
  uint32_t no_crash_offset = __ pc_offset();
  __ li(scratch, static_cast<int64_t>(crash_address_));
  __ LoadWord(scratch, MemOperand(scratch, 0));
#else
#error Unsupported platform
#endif
  __ Ret();
  CodeDesc desc;
  masm.GetCode(static_cast<LocalIsolate*>(nullptr), &desc);

  trap_handler::TrappingInstructionData protected_instruction{no_crash_offset};
  trap_handler::RegisterHandlerData(reinterpret_cast<Address>(desc.buffer),
                                    desc.instr_size, 1, &protected_instruction);

  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(i_isolate(), AssemblerOptions{},
                      CodeObjectRequired{false}, buffer_->CreateView());
#if V8_HOST_ARCH_X64
  __ xorq(scratch, scratch);
  uint32_t crash_offset = __ pc_offset();
  __ divq(scratch);
#elif V8_HOST_ARCH_ARM64
  UseScratchRegisterScope temps(&masm);
  uint32_t crash_offset = __ pc_offset();
  __ Trap();
#elif V8_HOST_ARCH_LOONG64
  UseScratchRegisterScope temps(&masm);
  uint32_t crash_offset = __ pc_offset();
  __ Trap();
#elif V8_HOST_ARCH_RISCV64
  UseScratchRegisterScope temps(&masm);
  uint32_t crash_offset = __ pc_offset();
  __ Trap();
#else
#error Unsupported platform
#endif
  __ Ret();
  CodeDesc desc;
  masm.GetCode(static_cast<LocalIsolate*>(nullptr), &desc);

  trap_handler::TrappingInstructionData protected_instruction{crash_offset};
  trap_handler::RegisterHandlerData(reinterpret_cast<Address>(desc.buffer),
                                    desc.instr_size, 1, &protected_instruction);

#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);
}
#endif

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_;
};

#if !V8_OS_FUCHSIA
INSTANTIATE_TEST_SUITE_P(Traps, TrapHandlerTest,
                         ::testing::Values(kDefault, kCallback),
                         PrintTrapHandlerTestParam);
#endif  // !V8_OS_FUCHSIA

#undef __
}  // namespace wasm
}  // namespace internal
}  // namespace v8

#endif
