memory-protection-key.cc 7.64 KB
Newer Older
1 2 3 4 5 6
// 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"

7 8 9 10 11
#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

12
#include "src/base/build_config.h"
13
#include "src/base/logging.h"
14
#include "src/base/macros.h"
15
#include "src/base/platform/platform.h"
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

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

46 47 48 49 50 51 52 53 54 55 56 57 58
// 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
59 60 61 62 63
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);
64 65 66
  // Cache the {dlsym()} lookup in a {static} variable.
  static auto* pkey_alloc =
      bit_cast<pkey_alloc_t>(dlsym(RTLD_DEFAULT, "pkey_alloc"));
67 68 69 70 71 72 73 74 75 76 77 78 79 80
  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;
}

81
DISABLE_CFI_ICALL
82 83
void FreeMemoryProtectionKey(int key) {
  // Only free the key if one was allocated.
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
  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;
118
  }
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
  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
167
void SetPermissionsForMemoryProtectionKey(
168
    int key, MemoryProtectionKeyPermission permissions) {
169
  CHECK_NE(kNoMemoryProtectionKey, key);
170 171 172 173 174 175 176 177

#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);
178
  CHECK_EQ(0 /* success */, ret);
179
#else
180 181
  // On platforms without PKU support, we should have failed the CHECK above
  // because the key must be {kNoMemoryProtectionKey}.
182
  UNREACHABLE();
183 184 185 186 187 188
#endif
}

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