//===-- Unit tests for pkey functions -------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "hdr/errno_macros.h"
#include "hdr/signal_macros.h"
#include "hdr/sys_mman_macros.h"
#include "hdr/types/size_t.h"
#include "src/sys/mman/mmap.h"
#include "src/sys/mman/munmap.h"
#include "src/sys/mman/pkey_alloc.h"
#include "src/sys/mman/pkey_free.h"
#include "src/sys/mman/pkey_get.h"
#include "src/sys/mman/pkey_mprotect.h"
#include "src/sys/mman/pkey_set.h"
#include "test/UnitTest/ErrnoCheckingTest.h"
#include "test/UnitTest/ErrnoSetterMatcher.h"
#include "test/UnitTest/LibcTest.h"
#include "test/UnitTest/TestLogger.h"

#include <linux/param.h> // For EXEC_PAGESIZE.

using LIBC_NAMESPACE::testing::tlog;
using LIBC_NAMESPACE::testing::ErrnoSetterMatcher::Fails;
using LIBC_NAMESPACE::testing::ErrnoSetterMatcher::Succeeds;

using LlvmLibcProtectionKeyTest = LIBC_NAMESPACE::testing::ErrnoCheckingTest;

constexpr size_t MMAP_SIZE = EXEC_PAGESIZE;

// Wrapper around a pkey to ensure it is freed.
class PKeyGuard {
public:
  int key;

  PKeyGuard() : key(-1) {}

  PKeyGuard(int key) : key(key) {}

  ~PKeyGuard() {
    if (key != -1) {
      LIBC_NAMESPACE::pkey_free(key);
    }
  }
};

// Wrapper around mmap to ensure munmap is called.
class MMapPageGuard {
public:
  void *addr = nullptr;
  size_t size = 0;

  static MMapPageGuard mmap(int prot) {
    void *addr = LIBC_NAMESPACE::mmap(nullptr, MMAP_SIZE, prot,
                                      MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (addr == MAP_FAILED) {
      return MMapPageGuard(nullptr, 0);
    }
    return MMapPageGuard(addr, MMAP_SIZE);
  }

  MMapPageGuard(void *addr, size_t size) : addr(addr), size(size) {}

  ~MMapPageGuard() {
    if (addr != nullptr) {
      LIBC_NAMESPACE::munmap(addr, size);
    }
  }
};

bool protection_keys_supported() {
  static bool supported = []() {
    PKeyGuard pkey(LIBC_NAMESPACE::pkey_alloc(0, 0));
    int err = libc_errno;
    libc_errno = 0;

    if (pkey.key < 0 || (err == ENOSPC || err == ENOSYS || err == EINVAL)) {
      tlog << "pkey_alloc failed with errno=" << err << "\n";
      return false;
    }

    int access_rights = LIBC_NAMESPACE::pkey_get(pkey.key);
    err = libc_errno;
    libc_errno = 0;
    if (access_rights < 0 || err == ENOSYS) {
      tlog << "pkey_get failed with errno=" << err << "\n";
      return false;
    }

    return true;
  }();
  return supported;
}

TEST_F(LlvmLibcProtectionKeyTest, MProtectWithPKeyDisablesWrite) {
  if (!protection_keys_supported()) {
    tlog << "Skipping test: pkey is not available\n";
    return;
  }

  PKeyGuard pkey(LIBC_NAMESPACE::pkey_alloc(0, PKEY_DISABLE_WRITE));
  ASSERT_NE(pkey.key, -1);

  MMapPageGuard page = MMapPageGuard::mmap(PROT_READ | PROT_WRITE);
  ASSERT_NE(page.addr, nullptr);

  volatile char *data = (char *)page.addr;
  data[0] = 'a';

  EXPECT_THAT(LIBC_NAMESPACE::pkey_mprotect(page.addr, page.size,
                                            PROT_READ | PROT_WRITE, pkey.key),
              Succeeds());

  // Read is still allowed.
  EXPECT_EQ(data[0], 'a');

  // Write is not allowed.
  EXPECT_DEATH([&data]() { data[0] = 'b'; }, WITH_SIGNAL(SIGSEGV));
}

TEST_F(LlvmLibcProtectionKeyTest, PKeySetChangesAccessRights) {
  if (!protection_keys_supported()) {
    tlog << "Skipping test: pkey is not available\n";
    return;
  }

  PKeyGuard pkey(LIBC_NAMESPACE::pkey_alloc(0, 0));
  ASSERT_NE(pkey.key, -1);

  MMapPageGuard page = MMapPageGuard::mmap(PROT_READ | PROT_WRITE);
  ASSERT_NE(page.addr, nullptr);

  EXPECT_THAT(LIBC_NAMESPACE::pkey_mprotect(page.addr, page.size,
                                            PROT_READ | PROT_WRITE, pkey.key),
              Succeeds());

  // Write is allowed by default.
  volatile char *data = (char *)page.addr;
  data[0] = 'a';

  EXPECT_THAT(LIBC_NAMESPACE::pkey_set(pkey.key, PKEY_DISABLE_WRITE),
              Succeeds());

  // Now read is allowed but write is not.
  EXPECT_EQ(data[0], 'a');
  EXPECT_DEATH([&data]() { data[0] = 'b'; }, WITH_SIGNAL(SIGSEGV));

  // Now neither read nor write is allowed.
  EXPECT_THAT(LIBC_NAMESPACE::pkey_set(pkey.key, PKEY_DISABLE_ACCESS |
                                                     PKEY_DISABLE_WRITE),
              Succeeds());
  EXPECT_DEATH([&data]() { (void)data[0]; }, WITH_SIGNAL(SIGSEGV));
  EXPECT_DEATH([&data]() { data[0] = 'b'; }, WITH_SIGNAL(SIGSEGV));
}

TEST_F(LlvmLibcProtectionKeyTest, FallsBackToMProtectForInvalidPKey) {
  MMapPageGuard page = MMapPageGuard::mmap(PROT_READ | PROT_WRITE);
  ASSERT_NE(page.addr, nullptr);

  volatile char *data = (char *)page.addr;
  data[0] = 'a';

  EXPECT_THAT(
      LIBC_NAMESPACE::pkey_mprotect(page.addr, page.size, PROT_READ, -1),
      Succeeds());

  // Read is still allowed.
  EXPECT_EQ(data[0], 'a');

  // Write is not allowed.
  EXPECT_DEATH([&data]() { data[0] = 'b'; }, WITH_SIGNAL(SIGSEGV));
}

TEST_F(LlvmLibcProtectionKeyTest, ExhaustedKeysFailsWithENOSPC) {
  if (!protection_keys_supported()) {
    tlog << "Skipping test: pkey is not available\n";
    return;
  }

  // Use an unreasonably large limit to ensure test is cross-platform.
  // This limit is intended to be much larger than the actual hardware limit.
  constexpr int MAX_PKEYS = 64;
  PKeyGuard pkeys[MAX_PKEYS];
  for (int i = 0; i < MAX_PKEYS; ++i) {
    pkeys[i].key = LIBC_NAMESPACE::pkey_alloc(0, 0);
  }

  // pkey allocation should eventually fail with ENOSPC.
  PKeyGuard pkey(LIBC_NAMESPACE::pkey_alloc(0, 0));
  EXPECT_THAT(pkey.key, Fails(ENOSPC));
  libc_errno = 0;
}

TEST_F(LlvmLibcProtectionKeyTest, Accessors) {
  if (!protection_keys_supported()) {
    tlog << "Skipping test: pkey is not available\n";
    return;
  }

  PKeyGuard pkey(LIBC_NAMESPACE::pkey_alloc(0, PKEY_DISABLE_WRITE));
  ASSERT_NE(pkey.key, -1);

  // Check that pkey_alloc sets the access rights.
  EXPECT_EQ(LIBC_NAMESPACE::pkey_get(pkey.key), PKEY_DISABLE_WRITE);

  // Check that pkey_set changes the access rights.
  EXPECT_THAT(LIBC_NAMESPACE::pkey_set(pkey.key, PKEY_DISABLE_ACCESS),
              Succeeds());
  EXPECT_EQ(LIBC_NAMESPACE::pkey_get(pkey.key), PKEY_DISABLE_ACCESS);
}

TEST_F(LlvmLibcProtectionKeyTest, AccessorsErrorForInvalidValues) {
  if (!protection_keys_supported()) {
    tlog << "Skipping test: pkey is not available\n";
    return;
  }

  PKeyGuard pkey(LIBC_NAMESPACE::pkey_alloc(0, PKEY_DISABLE_WRITE));
  ASSERT_NE(pkey.key, -1);

  // Pkey is out of bounds in pkey_get.
  EXPECT_THAT(LIBC_NAMESPACE::pkey_get(100), Fails(EINVAL));
  EXPECT_THAT(LIBC_NAMESPACE::pkey_get(-1234), Fails(EINVAL));

  // Pkey is out of bounds in pkey_set.
  EXPECT_THAT(LIBC_NAMESPACE::pkey_set(100, PKEY_DISABLE_ACCESS),
              Fails(EINVAL));
  EXPECT_THAT(LIBC_NAMESPACE::pkey_set(-1234, PKEY_DISABLE_ACCESS),
              Fails(EINVAL));

  // Non-zero flags are not supported in pkey_alloc.
  EXPECT_THAT(LIBC_NAMESPACE::pkey_alloc(123, PKEY_DISABLE_WRITE),
              Fails(EINVAL));

  // Access rights are out of bounds.
  EXPECT_THAT(LIBC_NAMESPACE::pkey_alloc(0, 1000), Fails(EINVAL));
  EXPECT_THAT(LIBC_NAMESPACE::pkey_set(pkey.key, 1000), Fails(EINVAL));
}
