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 #undef MAP_TYPE  // Conflicts with MAP_TYPE in Torque-generated instance-types.h
10 #endif
11 
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 // Runtime-detection of PKU support with {dlsym()}.
18 //
19 // For now, we support memory protection keys/PKEYs/PKU only for Linux on x64
20 // based on glibc functions {pkey_alloc()}, {pkey_free()}, etc.
21 // Those functions are only available since glibc version 2.27:
22 // https://man7.org/linux/man-pages/man2/pkey_alloc.2.html
23 // However, if we check the glibc verison with V8_GLIBC_PREPREQ here at compile
24 // time, this causes two problems due to dynamic linking of glibc:
25 // 1) If the compiling system _has_ a new enough glibc, the binary will include
26 // calls to {pkey_alloc()} etc., and then the runtime system must supply a
27 // new enough glibc version as well. That is, this potentially breaks runtime
28 // compatability on older systems (e.g., Ubuntu 16.04 with glibc 2.23).
29 // 2) If the compiling system _does not_ have a new enough glibc, PKU support
30 // will not be compiled in, even though the runtime system potentially _does_
31 // have support for it due to a new enough Linux kernel and glibc version.
32 // That is, this results in non-optimal security (PKU available, but not used).
33 // Hence, we do _not_ check the glibc version during compilation, and instead
34 // only at runtime try to load {pkey_alloc()} etc. with {dlsym()}.
35 // TODO(dlehmann): Move this import and freestanding functions below to
36 // base/platform/platform.h {OS} (lower-level functions) and
37 // {base::PageAllocator} (exported API).
38 #if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
39 #include <dlfcn.h>
40 #endif
41 
42 namespace v8 {
43 namespace internal {
44 namespace wasm {
45 
46 // TODO(dlehmann) Security: Are there alternatives to disabling CFI altogether
47 // for the functions below? Since they are essentially an arbitrary indirect
48 // call gadget, disabling CFI should be only a last resort. In Chromium, there
49 // was {base::ProtectedMemory} to protect the function pointer from being
50 // overwritten, but t seems it was removed to not begin used and AFAICT no such
51 // thing exists in V8 to begin with. See
52 // https://www.chromium.org/developers/testing/control-flow-integrity and
53 // https://crrev.com/c/1884819.
54 // What is the general solution for CFI + {dlsym()}?
55 // An alternative would be to not rely on glibc and instead implement PKEY
56 // directly on top of Linux syscalls + inline asm, but that is quite some low-
57 // level code (probably in the order of 100 lines).
58 DISABLE_CFI_ICALL
AllocateMemoryProtectionKey()59 int AllocateMemoryProtectionKey() {
60 // See comment on the import on feature testing for PKEY support.
61 #if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
62   // Try to to find {pkey_alloc()} support in glibc.
63   typedef int (*pkey_alloc_t)(unsigned int, unsigned int);
64   // Cache the {dlsym()} lookup in a {static} variable.
65   static auto* pkey_alloc =
66       bit_cast<pkey_alloc_t>(dlsym(RTLD_DEFAULT, "pkey_alloc"));
67   if (pkey_alloc != nullptr) {
68     // If there is support in glibc, try to allocate a new key.
69     // This might still return -1, e.g., because the kernel does not support
70     // PKU or because there is no more key available.
71     // Different reasons for why {pkey_alloc()} failed could be checked with
72     // errno, e.g., EINVAL vs ENOSPC vs ENOSYS. See manpages and glibc manual
73     // (the latter is the authorative source):
74     // https://www.gnu.org/software/libc/manual/html_mono/libc.html#Memory-Protection-Keys
75     return pkey_alloc(/* flags, unused */ 0, kDisableAccess);
76   }
77 #endif
78   return kNoMemoryProtectionKey;
79 }
80 
81 DISABLE_CFI_ICALL
FreeMemoryProtectionKey(int key)82 void FreeMemoryProtectionKey(int key) {
83   // Only free the key if one was allocated.
84   if (key == kNoMemoryProtectionKey) return;
85 
86 #if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
87   typedef int (*pkey_free_t)(int);
88   static auto* pkey_free =
89       bit_cast<pkey_free_t>(dlsym(RTLD_DEFAULT, "pkey_free"));
90   // If a valid key was allocated, {pkey_free()} must also be available.
91   DCHECK_NOT_NULL(pkey_free);
92 
93   int ret = pkey_free(key);
94   CHECK_EQ(/* success */ 0, ret);
95 #else
96   // On platforms without PKU support, we should have already returned because
97   // the key must be {kNoMemoryProtectionKey}.
98   UNREACHABLE();
99 #endif
100 }
101 
102 #if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
103 // TODO(dlehmann): Copied from base/platform/platform-posix.cc. Should be
104 // removed once this code is integrated in base/platform/platform-linux.cc.
GetProtectionFromMemoryPermission(base::OS::MemoryPermission access)105 int GetProtectionFromMemoryPermission(base::OS::MemoryPermission access) {
106   switch (access) {
107     case base::OS::MemoryPermission::kNoAccess:
108     case base::OS::MemoryPermission::kNoAccessWillJitLater:
109       return PROT_NONE;
110     case base::OS::MemoryPermission::kRead:
111       return PROT_READ;
112     case base::OS::MemoryPermission::kReadWrite:
113       return PROT_READ | PROT_WRITE;
114     case base::OS::MemoryPermission::kReadWriteExecute:
115       return PROT_READ | PROT_WRITE | PROT_EXEC;
116     case base::OS::MemoryPermission::kReadExecute:
117       return PROT_READ | PROT_EXEC;
118   }
119   UNREACHABLE();
120 }
121 #endif
122 
123 DISABLE_CFI_ICALL
SetPermissionsAndMemoryProtectionKey(PageAllocator * page_allocator,base::AddressRegion region,PageAllocator::Permission page_permissions,int key)124 bool SetPermissionsAndMemoryProtectionKey(
125     PageAllocator* page_allocator, base::AddressRegion region,
126     PageAllocator::Permission page_permissions, int key) {
127   DCHECK_NOT_NULL(page_allocator);
128 
129   void* address = reinterpret_cast<void*>(region.begin());
130   size_t size = region.size();
131 
132 #if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
133   typedef int (*pkey_mprotect_t)(void*, size_t, int, int);
134   static auto* pkey_mprotect =
135       bit_cast<pkey_mprotect_t>(dlsym(RTLD_DEFAULT, "pkey_mprotect"));
136 
137   if (pkey_mprotect == nullptr) {
138     // If there is no runtime support for {pkey_mprotect()}, no key should have
139     // been allocated in the first place.
140     DCHECK_EQ(kNoMemoryProtectionKey, key);
141 
142     // Without PKU support, fallback to regular {mprotect()}.
143     return page_allocator->SetPermissions(address, size, page_permissions);
144   }
145 
146   // Copied with slight modifications from base/platform/platform-posix.cc
147   // {OS::SetPermissions()}.
148   // TODO(dlehmann): Move this block into its own function at the right
149   // abstraction boundary (likely some static method in platform.h {OS})
150   // once the whole PKU code is moved into base/platform/.
151   DCHECK_EQ(0, region.begin() % page_allocator->CommitPageSize());
152   DCHECK_EQ(0, size % page_allocator->CommitPageSize());
153 
154   int protection = GetProtectionFromMemoryPermission(
155       static_cast<base::OS::MemoryPermission>(page_permissions));
156 
157   int ret = pkey_mprotect(address, size, protection, key);
158 
159   return ret == /* success */ 0;
160 #else
161   // Without PKU support, fallback to regular {mprotect()}.
162   return page_allocator->SetPermissions(address, size, page_permissions);
163 #endif
164 }
165 
166 DISABLE_CFI_ICALL
SetPermissionsForMemoryProtectionKey(int key,MemoryProtectionKeyPermission permissions)167 void SetPermissionsForMemoryProtectionKey(
168     int key, MemoryProtectionKeyPermission permissions) {
169   DCHECK_NE(kNoMemoryProtectionKey, key);
170 
171 #if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
172   typedef int (*pkey_set_t)(int, unsigned int);
173   static auto* pkey_set = bit_cast<pkey_set_t>(dlsym(RTLD_DEFAULT, "pkey_set"));
174   // If a valid key was allocated, {pkey_set()} must also be available.
175   DCHECK_NOT_NULL(pkey_set);
176 
177   int ret = pkey_set(key, permissions);
178   CHECK_EQ(0 /* success */, ret);
179 #else
180   // On platforms without PKU support, this method cannot be called because
181   // no protection key can have been allocated.
182   UNREACHABLE();
183 #endif
184 }
185 
186 DISABLE_CFI_ICALL
MemoryProtectionKeyWritable(int key)187 bool MemoryProtectionKeyWritable(int key) {
188   DCHECK_NE(kNoMemoryProtectionKey, key);
189 
190 #if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
191   typedef int (*pkey_get_t)(int);
192   static auto* pkey_get = bit_cast<pkey_get_t>(dlsym(RTLD_DEFAULT, "pkey_get"));
193   // If a valid key was allocated, {pkey_get()} must also be available.
194   DCHECK_NOT_NULL(pkey_get);
195 
196   int permissions = pkey_get(key);
197   return permissions == kNoRestrictions;
198 #else
199   // On platforms without PKU support, this method cannot be called because
200   // no protection key can have been allocated.
201   UNREACHABLE();
202 #endif
203 }
204 
205 }  // namespace wasm
206 }  // namespace internal
207 }  // namespace v8
208