summaryrefslogtreecommitdiff
path: root/deps/v8/src/wasm/memory-protection-key.cc
blob: e8252cd9ce9c70d3f060750d0616526567a5b250 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// 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