• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2021 the V8 project authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "src/wasm/memory-protection-key.h"
6 
7 #if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
8 #include <sys/mman.h>     // For {mprotect()} protection macros.
9 #include <sys/utsname.h>  // For {uname()}.
10 #undef MAP_TYPE  // Conflicts with MAP_TYPE in Torque-generated instance-types.h
11 #endif
12 
13 #include "src/base/build_config.h"
14 #include "src/base/logging.h"
15 #include "src/base/macros.h"
16 #include "src/base/platform/platform.h"
17 
18 // Runtime-detection of PKU support with {dlsym()}.
19 //
20 // For now, we support memory protection keys/PKEYs/PKU only for Linux on x64
21 // based on glibc functions {pkey_alloc()}, {pkey_free()}, etc.
22 // Those functions are only available since glibc version 2.27:
23 // https://man7.org/linux/man-pages/man2/pkey_alloc.2.html
24 // However, if we check the glibc verison with V8_GLIBC_PREPREQ here at compile
25 // time, this causes two problems due to dynamic linking of glibc:
26 // 1) If the compiling system _has_ a new enough glibc, the binary will include
27 // calls to {pkey_alloc()} etc., and then the runtime system must supply a
28 // new enough glibc version as well. That is, this potentially breaks runtime
29 // compatability on older systems (e.g., Ubuntu 16.04 with glibc 2.23).
30 // 2) If the compiling system _does not_ have a new enough glibc, PKU support
31 // will not be compiled in, even though the runtime system potentially _does_
32 // have support for it due to a new enough Linux kernel and glibc version.
33 // That is, this results in non-optimal security (PKU available, but not used).
34 // Hence, we do _not_ check the glibc version during compilation, and instead
35 // only at runtime try to load {pkey_alloc()} etc. with {dlsym()}.
36 // TODO(dlehmann): Move this import and freestanding functions below to
37 // base/platform/platform.h {OS} (lower-level functions) and
38 // {base::PageAllocator} (exported API).
39 #if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
40 #include <dlfcn.h>
41 #endif
42 
43 namespace v8 {
44 namespace internal {
45 namespace wasm {
46 
47 namespace {
48 using pkey_alloc_t = int (*)(unsigned, unsigned);
49 using pkey_free_t = int (*)(int);
50 using pkey_mprotect_t = int (*)(void*, size_t, int, int);
51 using pkey_get_t = int (*)(int);
52 using pkey_set_t = int (*)(int, unsigned);
53 
54 pkey_alloc_t pkey_alloc = nullptr;
55 pkey_free_t pkey_free = nullptr;
56 pkey_mprotect_t pkey_mprotect = nullptr;
57 pkey_get_t pkey_get = nullptr;
58 pkey_set_t pkey_set = nullptr;
59 
60 #ifdef DEBUG
61 bool pkey_initialized = false;
62 #endif
63 }  // namespace
64 
InitializeMemoryProtectionKeySupport()65 void InitializeMemoryProtectionKeySupport() {
66   // Flip {pkey_initialized} (in debug mode) and check the new value.
67   DCHECK_EQ(true, pkey_initialized = !pkey_initialized);
68 #if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
69   // PKU was broken on Linux kernels before 5.13 (see
70   // https://lore.kernel.org/all/20210623121456.399107624@linutronix.de/).
71   // A fix is also included in the 5.4.182 and 5.10.103 versions ("x86/fpu:
72   // Correct pkru/xstate inconsistency" by Brian Geffon <bgeffon@google.com>).
73   // Thus check the kernel version we are running on, and bail out if does not
74   // contain the fix.
75   struct utsname uname_buffer;
76   CHECK_EQ(0, uname(&uname_buffer));
77   int kernel, major, minor;
78   // Conservatively return if the release does not match the format we expect.
79   if (sscanf(uname_buffer.release, "%d.%d.%d", &kernel, &major, &minor) != 3) {
80     return;
81   }
82   bool kernel_has_pkru_fix =
83       kernel > 5 || (kernel == 5 && major >= 13) ||   // anything >= 5.13
84       (kernel == 5 && major == 4 && minor >= 182) ||  // 5.4 >= 5.4.182
85       (kernel == 5 && major == 10 && minor >= 103);   // 5.10 >= 5.10.103
86   if (!kernel_has_pkru_fix) return;
87 
88   // Try to find the pkey functions in glibc.
89   void* pkey_alloc_ptr = dlsym(RTLD_DEFAULT, "pkey_alloc");
90   if (!pkey_alloc_ptr) return;
91 
92   // If {pkey_alloc} is available, the others must also be available.
93   void* pkey_free_ptr = dlsym(RTLD_DEFAULT, "pkey_free");
94   void* pkey_mprotect_ptr = dlsym(RTLD_DEFAULT, "pkey_mprotect");
95   void* pkey_get_ptr = dlsym(RTLD_DEFAULT, "pkey_get");
96   void* pkey_set_ptr = dlsym(RTLD_DEFAULT, "pkey_set");
97   CHECK(pkey_free_ptr && pkey_mprotect_ptr && pkey_get_ptr && pkey_set_ptr);
98 
99   pkey_alloc = reinterpret_cast<pkey_alloc_t>(pkey_alloc_ptr);
100   pkey_free = reinterpret_cast<pkey_free_t>(pkey_free_ptr);
101   pkey_mprotect = reinterpret_cast<pkey_mprotect_t>(pkey_mprotect_ptr);
102   pkey_get = reinterpret_cast<pkey_get_t>(pkey_get_ptr);
103   pkey_set = reinterpret_cast<pkey_set_t>(pkey_set_ptr);
104 #endif
105 }
106 
107 // TODO(dlehmann) Security: Are there alternatives to disabling CFI altogether
108 // for the functions below? Since they are essentially an arbitrary indirect
109 // call gadget, disabling CFI should be only a last resort. In Chromium, there
110 // was {base::ProtectedMemory} to protect the function pointer from being
111 // overwritten, but t seems it was removed to not begin used and AFAICT no such
112 // thing exists in V8 to begin with. See
113 // https://www.chromium.org/developers/testing/control-flow-integrity and
114 // https://crrev.com/c/1884819.
115 // What is the general solution for CFI + {dlsym()}?
116 // An alternative would be to not rely on glibc and instead implement PKEY
117 // directly on top of Linux syscalls + inline asm, but that is quite some low-
118 // level code (probably in the order of 100 lines).
119 DISABLE_CFI_ICALL
AllocateMemoryProtectionKey()120 int AllocateMemoryProtectionKey() {
121   DCHECK(pkey_initialized);
122   if (!pkey_alloc) return kNoMemoryProtectionKey;
123 
124   // If there is support in glibc, try to allocate a new key.
125   // This might still return -1, e.g., because the kernel does not support
126   // PKU or because there is no more key available.
127   // Different reasons for why {pkey_alloc()} failed could be checked with
128   // errno, e.g., EINVAL vs ENOSPC vs ENOSYS. See manpages and glibc manual
129   // (the latter is the authorative source):
130   // https://www.gnu.org/software/libc/manual/html_mono/libc.html#Memory-Protection-Keys
131   STATIC_ASSERT(kNoMemoryProtectionKey == -1);
132   return pkey_alloc(/* flags, unused */ 0, kDisableAccess);
133 }
134 
135 DISABLE_CFI_ICALL
FreeMemoryProtectionKey(int key)136 void FreeMemoryProtectionKey(int key) {
137   DCHECK(pkey_initialized);
138   // Only free the key if one was allocated.
139   if (key == kNoMemoryProtectionKey) return;
140 
141   // On platforms without PKU support, we should have already returned because
142   // the key must be {kNoMemoryProtectionKey}.
143   DCHECK_NOT_NULL(pkey_free);
144   CHECK_EQ(/* success */ 0, pkey_free(key));
145 }
146 
GetProtectionFromMemoryPermission(PageAllocator::Permission permission)147 int GetProtectionFromMemoryPermission(PageAllocator::Permission permission) {
148 #if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
149   // Mappings for PKU are either RWX (on this level) or no access.
150   switch (permission) {
151     case PageAllocator::kNoAccess:
152       return PROT_NONE;
153     case PageAllocator::kReadWriteExecute:
154       return PROT_READ | PROT_WRITE | PROT_EXEC;
155     default:
156       UNREACHABLE();
157   }
158 #endif
159   // Other platforms do not use PKU.
160   UNREACHABLE();
161 }
162 
163 DISABLE_CFI_ICALL
SetPermissionsAndMemoryProtectionKey(PageAllocator * page_allocator,base::AddressRegion region,PageAllocator::Permission page_permissions,int key)164 bool SetPermissionsAndMemoryProtectionKey(
165     PageAllocator* page_allocator, base::AddressRegion region,
166     PageAllocator::Permission page_permissions, int key) {
167   DCHECK(pkey_initialized);
168 
169   void* address = reinterpret_cast<void*>(region.begin());
170   size_t size = region.size();
171 
172   if (pkey_mprotect) {
173     // Copied with slight modifications from base/platform/platform-posix.cc
174     // {OS::SetPermissions()}.
175     // TODO(dlehmann): Move this block into its own function at the right
176     // abstraction boundary (likely some static method in platform.h {OS})
177     // once the whole PKU code is moved into base/platform/.
178     DCHECK_EQ(0, region.begin() % page_allocator->CommitPageSize());
179     DCHECK_EQ(0, size % page_allocator->CommitPageSize());
180 
181     int protection = GetProtectionFromMemoryPermission(page_permissions);
182 
183     int ret = pkey_mprotect(address, size, protection, key);
184 
185     if (ret == 0 && page_permissions == PageAllocator::kNoAccess) {
186       // Similar to {OS::SetPermissions}, also discard the pages after switching
187       // to no access. This is advisory; ignore errors and continue execution.
188       USE(page_allocator->DiscardSystemPages(address, size));
189     }
190 
191     return ret == /* success */ 0;
192   }
193 
194   // If there is no runtime support for {pkey_mprotect()}, no key should have
195   // been allocated in the first place.
196   DCHECK_EQ(kNoMemoryProtectionKey, key);
197 
198   // Without PKU support, fallback to regular {mprotect()}.
199   return page_allocator->SetPermissions(address, size, page_permissions);
200 }
201 
202 DISABLE_CFI_ICALL
SetPermissionsForMemoryProtectionKey(int key,MemoryProtectionKeyPermission permissions)203 void SetPermissionsForMemoryProtectionKey(
204     int key, MemoryProtectionKeyPermission permissions) {
205   DCHECK(pkey_initialized);
206   DCHECK_NE(kNoMemoryProtectionKey, key);
207 
208   // If a valid key was allocated, {pkey_set()} must also be available.
209   DCHECK_NOT_NULL(pkey_set);
210 
211   CHECK_EQ(0 /* success */, pkey_set(key, permissions));
212 }
213 
214 DISABLE_CFI_ICALL
GetMemoryProtectionKeyPermission(int key)215 MemoryProtectionKeyPermission GetMemoryProtectionKeyPermission(int key) {
216   DCHECK(pkey_initialized);
217   DCHECK_NE(kNoMemoryProtectionKey, key);
218 
219   // If a valid key was allocated, {pkey_get()} must also be available.
220   DCHECK_NOT_NULL(pkey_get);
221 
222   int permission = pkey_get(key);
223   CHECK(permission == kNoRestrictions || permission == kDisableAccess ||
224         permission == kDisableWrite);
225   return static_cast<MemoryProtectionKeyPermission>(permission);
226 }
227 
228 }  // namespace wasm
229 }  // namespace internal
230 }  // namespace v8
231