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