// 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 "src/wasm/memory-protection-key.h"

#if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
#include <sys/mman.h>  // For {mprotect()} protection macros.
#undef MAP_TYPE  // Conflicts with MAP_TYPE in Torque-generated instance-types.h
#endif

#include "src/base/build_config.h"
#include "src/base/logging.h"
#include "src/base/macros.h"
#include "src/base/platform/platform.h"

// Runtime-detection of PKU support with {dlsym()}.
//
// For now, we support memory protection keys/PKEYs/PKU only for Linux on x64
// based on glibc functions {pkey_alloc()}, {pkey_free()}, etc.
// Those functions are only available since glibc version 2.27:
// https://man7.org/linux/man-pages/man2/pkey_alloc.2.html
// However, if we check the glibc verison with V8_GLIBC_PREPREQ here at compile
// time, this causes two problems due to dynamic linking of glibc:
// 1) If the compiling system _has_ a new enough glibc, the binary will include
// calls to {pkey_alloc()} etc., and then the runtime system must supply a
// new enough glibc version as well. That is, this potentially breaks runtime
// compatability on older systems (e.g., Ubuntu 16.04 with glibc 2.23).
// 2) If the compiling system _does not_ have a new enough glibc, PKU support
// will not be compiled in, even though the runtime system potentially _does_
// have support for it due to a new enough Linux kernel and glibc version.
// That is, this results in non-optimal security (PKU available, but not used).
// Hence, we do _not_ check the glibc version during compilation, and instead
// only at runtime try to load {pkey_alloc()} etc. with {dlsym()}.
// TODO(dlehmann): Move this import and freestanding functions below to
// base/platform/platform.h {OS} (lower-level functions) and
// {base::PageAllocator} (exported API).
#if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
#include <dlfcn.h>
#endif

namespace v8 {
namespace internal {
namespace wasm {

// TODO(dlehmann) Security: Are there alternatives to disabling CFI altogether
// for the functions below? Since they are essentially an arbitrary indirect
// call gadget, disabling CFI should be only a last resort. In Chromium, there
// was {base::ProtectedMemory} to protect the function pointer from being
// overwritten, but t seems it was removed to not begin used and AFAICT no such
// thing exists in V8 to begin with. See
// https://www.chromium.org/developers/testing/control-flow-integrity and
// https://crrev.com/c/1884819.
// What is the general solution for CFI + {dlsym()}?
// An alternative would be to not rely on glibc and instead implement PKEY
// directly on top of Linux syscalls + inline asm, but that is quite some low-
// level code (probably in the order of 100 lines).
DISABLE_CFI_ICALL
int AllocateMemoryProtectionKey() {
// See comment on the import on feature testing for PKEY support.
#if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
  // Try to to find {pkey_alloc()} support in glibc.
  typedef int (*pkey_alloc_t)(unsigned int, unsigned int);
  // Cache the {dlsym()} lookup in a {static} variable.
  static auto* pkey_alloc =
      bit_cast<pkey_alloc_t>(dlsym(RTLD_DEFAULT, "pkey_alloc"));
  if (pkey_alloc != nullptr) {
    // If there is support in glibc, try to allocate a new key.
    // This might still return -1, e.g., because the kernel does not support
    // PKU or because there is no more key available.
    // Different reasons for why {pkey_alloc()} failed could be checked with
    // errno, e.g., EINVAL vs ENOSPC vs ENOSYS. See manpages and glibc manual
    // (the latter is the authorative source):
    // https://www.gnu.org/software/libc/manual/html_mono/libc.html#Memory-Protection-Keys
    return pkey_alloc(/* flags, unused */ 0, kDisableAccess);
  }
#endif
  return kNoMemoryProtectionKey;
}

DISABLE_CFI_ICALL
void FreeMemoryProtectionKey(int key) {
  // Only free the key if one was allocated.
  if (key == kNoMemoryProtectionKey) return;

#if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
  typedef int (*pkey_free_t)(int);
  static auto* pkey_free =
      bit_cast<pkey_free_t>(dlsym(RTLD_DEFAULT, "pkey_free"));
  // If a valid key was allocated, {pkey_free()} must also be available.
  DCHECK_NOT_NULL(pkey_free);

  int ret = pkey_free(key);
  CHECK_EQ(/* success */ 0, ret);
#else
  // On platforms without PKU support, we should have already returned because
  // the key must be {kNoMemoryProtectionKey}.
  UNREACHABLE();
#endif
}

#if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
// TODO(dlehmann): Copied from base/platform/platform-posix.cc. Should be
// removed once this code is integrated in base/platform/platform-linux.cc.
int GetProtectionFromMemoryPermission(base::OS::MemoryPermission access) {
  switch (access) {
    case base::OS::MemoryPermission::kNoAccess:
    case base::OS::MemoryPermission::kNoAccessWillJitLater:
      return PROT_NONE;
    case base::OS::MemoryPermission::kRead:
      return PROT_READ;
    case base::OS::MemoryPermission::kReadWrite:
      return PROT_READ | PROT_WRITE;
    case base::OS::MemoryPermission::kReadWriteExecute:
      return PROT_READ | PROT_WRITE | PROT_EXEC;
    case base::OS::MemoryPermission::kReadExecute:
      return PROT_READ | PROT_EXEC;
  }
  UNREACHABLE();
}
#endif

DISABLE_CFI_ICALL
bool SetPermissionsAndMemoryProtectionKey(
    PageAllocator* page_allocator, base::AddressRegion region,
    PageAllocator::Permission page_permissions, int key) {
  DCHECK_NOT_NULL(page_allocator);

  void* address = reinterpret_cast<void*>(region.begin());
  size_t size = region.size();

#if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
  typedef int (*pkey_mprotect_t)(void*, size_t, int, int);
  static auto* pkey_mprotect =
      bit_cast<pkey_mprotect_t>(dlsym(RTLD_DEFAULT, "pkey_mprotect"));

  if (pkey_mprotect == nullptr) {
    // If there is no runtime support for {pkey_mprotect()}, no key should have
    // been allocated in the first place.
    DCHECK_EQ(kNoMemoryProtectionKey, key);

    // Without PKU support, fallback to regular {mprotect()}.
    return page_allocator->SetPermissions(address, size, page_permissions);
  }

  // Copied with slight modifications from base/platform/platform-posix.cc
  // {OS::SetPermissions()}.
  // TODO(dlehmann): Move this block into its own function at the right
  // abstraction boundary (likely some static method in platform.h {OS})
  // once the whole PKU code is moved into base/platform/.
  DCHECK_EQ(0, region.begin() % page_allocator->CommitPageSize());
  DCHECK_EQ(0, size % page_allocator->CommitPageSize());

  int protection = GetProtectionFromMemoryPermission(
      static_cast<base::OS::MemoryPermission>(page_permissions));

  int ret = pkey_mprotect(address, size, protection, key);

  return ret == /* success */ 0;
#else
  // Without PKU support, fallback to regular {mprotect()}.
  return page_allocator->SetPermissions(address, size, page_permissions);
#endif
}

DISABLE_CFI_ICALL
bool SetPermissionsForMemoryProtectionKey(
    int key, MemoryProtectionKeyPermission permissions) {
  if (key == kNoMemoryProtectionKey) return false;

#if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
  typedef int (*pkey_set_t)(int, unsigned int);
  static auto* pkey_set = bit_cast<pkey_set_t>(dlsym(RTLD_DEFAULT, "pkey_set"));
  // If a valid key was allocated, {pkey_set()} must also be available.
  DCHECK_NOT_NULL(pkey_set);

  int ret = pkey_set(key, permissions);

  return ret == /* success */ 0;
#else
  // On platforms without PKU support, we should have already returned because
  // the key must be {kNoMemoryProtectionKey}.
  UNREACHABLE();
#endif
}

}  // namespace wasm
}  // namespace internal
}  // namespace v8
