// Copyright 2021 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 <optional>

#include "include/v8config.h"

// TODO(clemensb): Extend this to other OSes.
#if V8_OS_POSIX && !V8_OS_FUCHSIA
#include <signal.h>
#endif  // V8_OS_POSIX && !V8_OS_FUCHSIA

#include "src/base/macros.h"
#include "src/flags/flags.h"
#include "src/wasm/code-space-access.h"
#include "src/wasm/module-compiler.h"
#include "src/wasm/module-decoder.h"
#include "src/wasm/wasm-engine.h"
#include "src/wasm/wasm-features.h"
#include "src/wasm/wasm-opcodes.h"
#include "test/common/wasm/wasm-macro-gen.h"
#include "test/unittests/test-utils.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"

namespace v8::internal::wasm {

class MemoryProtectionTest : public TestWithNativeContext {
 public:
  void SetUp() override {
    v8_flags.wasm_lazy_compilation = false;
    // The key is initially write-protected.
    CHECK_IMPLIES(WasmCodeManager::HasMemoryProtectionKeySupport(),
                  !WasmCodeManager::MemoryProtectionKeyWritable());
  }

  void CompileModule() {
    CHECK_NULL(native_module_);
    native_module_ = CompileNativeModule();
    code_ = native_module_->GetCode(0);
  }

  NativeModule* native_module() const { return native_module_.get(); }

  WasmCode* code() const { return code_; }

  bool code_is_protected() {
    return V8_HAS_PTHREAD_JIT_WRITE_PROTECT ||
           V8_HAS_BECORE_JIT_WRITE_PROTECT || uses_pku();
  }

  void WriteToCode() { code_->instructions()[0] = 0; }

  void AssertCodeEventuallyProtected() {
    if (!code_is_protected()) {
      // Without protection, writing to code should always work.
      WriteToCode();
      return;
    }
    ASSERT_DEATH_IF_SUPPORTED(
        {
          WriteToCode();
          base::OS::Sleep(base::TimeDelta::FromMilliseconds(10));
        },
        "");
  }

  bool uses_pku() {
    // M1 always uses MAP_JIT.
    if (V8_HAS_PTHREAD_JIT_WRITE_PROTECT || V8_HAS_BECORE_JIT_WRITE_PROTECT) {
      return false;
    }
    return WasmCodeManager::HasMemoryProtectionKeySupport();
  }

 private:
  std::shared_ptr<NativeModule> CompileNativeModule() {
    // Define the bytes for a module with a single empty function.
    static const uint8_t module_bytes[] = {
        WASM_MODULE_HEADER, SECTION(Type, ENTRY_COUNT(1), SIG_ENTRY_v_v),
        SECTION(Function, ENTRY_COUNT(1), SIG_INDEX(0)),
        SECTION(Code, ENTRY_COUNT(1), ADD_COUNT(0 /* locals */, kExprEnd))};

    base::OwnedVector<const uint8_t> bytes = base::OwnedCopyOf(module_bytes);

    WasmDetectedFeatures detected_features;
    ModuleResult result =
        DecodeWasmModule(WasmEnabledFeatures::All(), bytes.as_vector(), false,
                         &detected_features);
    CHECK(result.ok());

    ErrorThrower thrower(isolate(), "");
    constexpr int kNoCompilationId = 0;
    constexpr ProfileInformation* kNoProfileInformation = nullptr;
    std::shared_ptr<NativeModule> native_module = CompileToNativeModule(
        isolate(), WasmEnabledFeatures::All(), detected_features,
        CompileTimeImports{}, &thrower, std::move(result).value(),
        std::move(bytes), kNoCompilationId,
        v8::metrics::Recorder::ContextId::Empty(), kNoProfileInformation);
    CHECK(!thrower.error());
    CHECK_NOT_NULL(native_module);

    return native_module;
  }

  std::shared_ptr<NativeModule> native_module_;
  WasmCodeRefScope code_refs_;
  WasmCode* code_;
};

TEST_F(MemoryProtectionTest, CodeNotWritableAfterCompilation) {
  CompileModule();
  AssertCodeEventuallyProtected();
}

TEST_F(MemoryProtectionTest, CodeWritableWithinScope) {
  CompileModule();
  CodeSpaceWriteScope write_scope;
  WriteToCode();
}

TEST_F(MemoryProtectionTest, CodeNotWritableAfterScope) {
  CompileModule();
  {
    CodeSpaceWriteScope write_scope;
    WriteToCode();
  }
  AssertCodeEventuallyProtected();
}

#if V8_OS_POSIX && !V8_OS_FUCHSIA
class ParameterizedMemoryProtectionTestWithSignalHandling
    : public MemoryProtectionTest,
      public ::testing::WithParamInterface<std::tuple<bool, bool>> {
 public:
  class SignalHandlerScope {
   public:
    SignalHandlerScope() {
      CHECK_NULL(current_handler_scope_);
      current_handler_scope_ = this;
      struct sigaction sa;
      sa.sa_sigaction = &HandleSignal;
      sigemptyset(&sa.sa_mask);
      sa.sa_flags = SA_RESTART | SA_SIGINFO | SA_ONSTACK;
      CHECK_EQ(0, sigaction(SIGPROF, &sa, &old_signal_handler_));
    }

    ~SignalHandlerScope() {
      CHECK_EQ(current_handler_scope_, this);
      current_handler_scope_ = nullptr;
      sigaction(SIGPROF, &old_signal_handler_, nullptr);
    }

    void SetAddressToWriteToOnSignal(uint8_t* address) {
      CHECK_NULL(code_address_);
      CHECK_NOT_NULL(address);
      code_address_ = address;
    }

    int num_handled_signals() const { return handled_signals_; }

   private:
    static void HandleSignal(int signal, siginfo_t*, void*) {
      // We execute on POSIX only, so we just directly use {printf} and friends.
      if (signal == SIGPROF) {
        printf("Handled SIGPROF.\n");
      } else {
        printf("Handled unknown signal: %d.\n", signal);
      }
      CHECK_NOT_NULL(current_handler_scope_);
      current_handler_scope_->handled_signals_ += 1;
      if (uint8_t* write_address = current_handler_scope_->code_address_) {
        // Print to the error output such that we can check against this message
        // in the ASSERT_DEATH_IF_SUPPORTED below.
        fprintf(stderr, "Writing to code.\n");
        // This write will crash if code is protected.
        *write_address = 0;
        fprintf(stderr, "Successfully wrote to code.\n");
      }
    }

    struct sigaction old_signal_handler_;
    int handled_signals_ = 0;
    uint8_t* code_address_ = nullptr;

    // These are accessed from the signal handler.
    static SignalHandlerScope* current_handler_scope_;
  };
};

// static
ParameterizedMemoryProtectionTestWithSignalHandling::SignalHandlerScope*
    ParameterizedMemoryProtectionTestWithSignalHandling::SignalHandlerScope::
        current_handler_scope_ = nullptr;

std::string PrintMemoryProtectionAndSignalHandlingTestParam(
    ::testing::TestParamInfo<std::tuple<bool, bool>> info) {
  const bool write_in_signal_handler = std::get<0>(info.param);
  const bool open_write_scope = std::get<1>(info.param);
  return std::string(write_in_signal_handler ? "Write" : "NoWrite") + "_" +
         (open_write_scope ? "WithScope" : "NoScope");
}

INSTANTIATE_TEST_SUITE_P(MemoryProtection,
                         ParameterizedMemoryProtectionTestWithSignalHandling,
                         ::testing::Combine(::testing::Bool(),
                                            ::testing::Bool()),
                         PrintMemoryProtectionAndSignalHandlingTestParam);

TEST_P(ParameterizedMemoryProtectionTestWithSignalHandling, TestSignalHandler) {
  // We must run in the "threadsafe" mode in order to make the spawned process
  // for the death test(s) re-execute the whole unit test up to the point of the
  // death test. Otherwise we would not really test the signal handling setup
  // that we use in the wild.
  // (see https://google.github.io/googletest/reference/assertions.html)
  CHECK_EQ("threadsafe", GTEST_FLAG_GET(death_test_style));

  // This test isn't currently compatible with sandbox hardware support, mostly
  // because the signal handler attempts to write to stack memory which is now
  // protected with a PKEY to which the handler has no access.
  // TODO(428680013): if we use an untrusted stack for sandboxed execution
  // mode, then this test should just work again.
#ifdef V8_ENABLE_SANDBOX_HARDWARE_SUPPORT
  if (SandboxHardwareSupport::IsActive()) return;
#endif  // V8_ENABLE_SANDBOX_HARDWARE_SUPPORT

  const bool write_in_signal_handler = std::get<0>(GetParam());
  const bool open_write_scope = std::get<1>(GetParam());
  CompileModule();
  SignalHandlerScope signal_handler_scope;

  CHECK_EQ(0, signal_handler_scope.num_handled_signals());
  pthread_kill(pthread_self(), SIGPROF);
  CHECK_EQ(1, signal_handler_scope.num_handled_signals());

  uint8_t* code_start_ptr = &code()->instructions()[0];
  uint8_t code_start = *code_start_ptr;
  CHECK_NE(0, code_start);
  if (write_in_signal_handler) {
    signal_handler_scope.SetAddressToWriteToOnSignal(code_start_ptr);
  }

  // If the signal handler writes to protected code we expect a crash.
  // An exception is M1, where an open scope still has an effect in the signal
  // handler.
  bool expect_crash = write_in_signal_handler && code_is_protected() &&
                      ((!V8_HAS_PTHREAD_JIT_WRITE_PROTECT &&
                        !V8_HAS_BECORE_JIT_WRITE_PROTECT) ||
                       !open_write_scope);
  if (expect_crash) {
    // Avoid {ASSERT_DEATH_IF_SUPPORTED}, because it only accepts a regex as
    // second parameter, and not a matcher as {ASSERT_DEATH}.
#if GTEST_HAS_DEATH_TEST
    ASSERT_DEATH(
        {
          std::optional<CodeSpaceWriteScope> write_scope;
          if (open_write_scope) write_scope.emplace();
          pthread_kill(pthread_self(), SIGPROF);
          base::OS::Sleep(base::TimeDelta::FromMilliseconds(10));
        },
        // Check that the subprocess tried to write, but did not succeed.
        ::testing::AnyOf(
            // non-sanitizer builds:
            ::testing::EndsWith("Writing to code.\n"),
            // ASan:
            ::testing::HasSubstr("Writing to code.\n"
                                 "AddressSanitizer:DEADLYSIGNAL"),
            // MSan:
            ::testing::HasSubstr("Writing to code.\n"
                                 "MemorySanitizer:DEADLYSIGNAL"),
            // UBSan:
            ::testing::HasSubstr("Writing to code.\n"
                                 "UndefinedBehaviorSanitizer:DEADLYSIGNAL")));
#endif  // GTEST_HAS_DEATH_TEST
  } else {
    std::optional<CodeSpaceWriteScope> write_scope;
    if (open_write_scope) write_scope.emplace();
    // The signal handler does not write or code is not protected, hence this
    // should succeed.
    pthread_kill(pthread_self(), SIGPROF);

    CHECK_EQ(2, signal_handler_scope.num_handled_signals());
    CHECK_EQ(write_in_signal_handler ? 0 : code_start, *code_start_ptr);
  }
}
#endif  // V8_OS_POSIX && !V8_OS_FUCHSIA

}  // namespace v8::internal::wasm
