1 | /* Tests for memory protection keys. |
2 | Copyright (C) 2017-2024 Free Software Foundation, Inc. |
3 | This file is part of the GNU C Library. |
4 | |
5 | The GNU C Library is free software; you can redistribute it and/or |
6 | modify it under the terms of the GNU Lesser General Public |
7 | License as published by the Free Software Foundation; either |
8 | version 2.1 of the License, or (at your option) any later version. |
9 | |
10 | The GNU C Library is distributed in the hope that it will be useful, |
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
13 | Lesser General Public License for more details. |
14 | |
15 | You should have received a copy of the GNU Lesser General Public |
16 | License along with the GNU C Library; if not, see |
17 | <https://www.gnu.org/licenses/>. */ |
18 | |
19 | #include <errno.h> |
20 | #include <inttypes.h> |
21 | #include <setjmp.h> |
22 | #include <stdbool.h> |
23 | #include <stdio.h> |
24 | #include <stdlib.h> |
25 | #include <string.h> |
26 | #include <support/check.h> |
27 | #include <support/support.h> |
28 | #include <support/test-driver.h> |
29 | #include <support/xsignal.h> |
30 | #include <support/xthread.h> |
31 | #include <support/xunistd.h> |
32 | #include <sys/mman.h> |
33 | |
34 | /* Used to force threads to wait until the main thread has set up the |
35 | keys as intended. */ |
36 | static pthread_barrier_t barrier; |
37 | |
38 | /* The keys used for testing. These have been allocated with access |
39 | rights set based on their array index. */ |
40 | enum { key_count = 3 }; |
41 | static int keys[key_count]; |
42 | static volatile int *pages[key_count]; |
43 | |
44 | /* Used to report results from the signal handler. */ |
45 | static volatile void *sigsegv_addr; |
46 | static volatile int sigsegv_code; |
47 | static volatile int sigsegv_pkey; |
48 | static sigjmp_buf sigsegv_jmp; |
49 | |
50 | /* Used to handle expected read or write faults. */ |
51 | static void |
52 | sigsegv_handler (int signum, siginfo_t *info, void *context) |
53 | { |
54 | sigsegv_addr = info->si_addr; |
55 | sigsegv_code = info->si_code; |
56 | sigsegv_pkey = info->si_pkey; |
57 | siglongjmp (sigsegv_jmp, 2); |
58 | } |
59 | |
60 | static const struct sigaction sigsegv_sigaction = |
61 | { |
62 | .sa_flags = SA_RESETHAND | SA_SIGINFO, |
63 | .sa_sigaction = &sigsegv_handler, |
64 | }; |
65 | |
66 | /* Check if PAGE is readable (if !WRITE) or writable (if WRITE). */ |
67 | static bool |
68 | check_page_access (int page, bool write) |
69 | { |
70 | /* This is needed to work around bug 22396: On x86-64, siglongjmp |
71 | does not restore the protection key access rights for the current |
72 | thread. We restore only the access rights for the keys under |
73 | test. (This is not a general solution to this problem, but it |
74 | allows testing to proceed after a fault.) */ |
75 | unsigned saved_rights[key_count]; |
76 | for (int i = 0; i < key_count; ++i) |
77 | saved_rights[i] = pkey_get (key: keys[i]); |
78 | |
79 | volatile int *addr = pages[page]; |
80 | if (test_verbose > 0) |
81 | { |
82 | printf (format: "info: checking access at %p (page %d) for %s\n" , |
83 | addr, page, write ? "writing" : "reading" ); |
84 | } |
85 | int result = sigsetjmp (sigsegv_jmp, 1); |
86 | if (result == 0) |
87 | { |
88 | xsigaction (SIGSEGV, newact: &sigsegv_sigaction, NULL); |
89 | if (write) |
90 | *addr = 3; |
91 | else |
92 | (void) *addr; |
93 | xsignal (SIGSEGV, SIG_DFL); |
94 | if (test_verbose > 0) |
95 | puts (s: " --> access allowed" ); |
96 | return true; |
97 | } |
98 | else |
99 | { |
100 | xsignal (SIGSEGV, SIG_DFL); |
101 | if (test_verbose > 0) |
102 | puts (s: " --> access denied" ); |
103 | TEST_COMPARE (result, 2); |
104 | TEST_COMPARE ((uintptr_t) sigsegv_addr, (uintptr_t) addr); |
105 | TEST_COMPARE (sigsegv_code, SEGV_PKUERR); |
106 | TEST_COMPARE (sigsegv_pkey, keys[page]); |
107 | for (int i = 0; i < key_count; ++i) |
108 | TEST_COMPARE (pkey_set (keys[i], saved_rights[i]), 0); |
109 | return false; |
110 | } |
111 | } |
112 | |
113 | static volatile sig_atomic_t sigusr1_handler_ran; |
114 | /* Used to check the behavior in signal handlers. In x86 all access are |
115 | revoked during signal handling. In PowerPC the key permissions are |
116 | inherited by the interrupted thread. This test accept both approaches. */ |
117 | static void |
118 | sigusr1_handler (int signum) |
119 | { |
120 | TEST_COMPARE (signum, SIGUSR1); |
121 | for (int i = 0; i < key_count; ++i) |
122 | TEST_VERIFY (pkey_get (keys[i]) == PKEY_DISABLE_ACCESS |
123 | || pkey_get (keys[i]) == i); |
124 | sigusr1_handler_ran = 1; |
125 | } |
126 | |
127 | /* Used to report results from other threads. */ |
128 | struct thread_result |
129 | { |
130 | int access_rights[key_count]; |
131 | pthread_t next_thread; |
132 | }; |
133 | |
134 | /* Return the thread's access rights for the keys under test. */ |
135 | static void * |
136 | get_thread_func (void *closure) |
137 | { |
138 | struct thread_result *result = xmalloc (n: sizeof (*result)); |
139 | for (int i = 0; i < key_count; ++i) |
140 | result->access_rights[i] = pkey_get (key: keys[i]); |
141 | memset (&result->next_thread, 0, sizeof (result->next_thread)); |
142 | return result; |
143 | } |
144 | |
145 | /* Wait for initialization and then check that the current thread does |
146 | not have access through the keys under test. */ |
147 | static void * |
148 | delayed_thread_func (void *closure) |
149 | { |
150 | bool check_access = *(bool *) closure; |
151 | pthread_barrier_wait (barrier: &barrier); |
152 | struct thread_result *result = get_thread_func (NULL); |
153 | |
154 | if (check_access) |
155 | { |
156 | /* Also check directly. This code should not run with other |
157 | threads in parallel because of the SIGSEGV handler which is |
158 | installed by check_page_access. */ |
159 | for (int i = 0; i < key_count; ++i) |
160 | { |
161 | TEST_VERIFY (!check_page_access (i, false)); |
162 | TEST_VERIFY (!check_page_access (i, true)); |
163 | } |
164 | } |
165 | |
166 | result->next_thread = xpthread_create (NULL, thread_func: get_thread_func, NULL); |
167 | return result; |
168 | } |
169 | |
170 | static int |
171 | do_test (void) |
172 | { |
173 | long pagesize = xsysconf (_SC_PAGESIZE); |
174 | |
175 | /* pkey_mprotect with key -1 should work even when there is no |
176 | protection key support. */ |
177 | { |
178 | int *page = xmmap (NULL, length: pagesize, PROT_NONE, |
179 | MAP_ANONYMOUS | MAP_PRIVATE, fd: -1); |
180 | TEST_COMPARE (pkey_mprotect (page, pagesize, PROT_READ | PROT_WRITE, -1), |
181 | 0); |
182 | volatile int *vpage = page; |
183 | *vpage = 5; |
184 | TEST_COMPARE (*vpage, 5); |
185 | xmunmap (addr: page, length: pagesize); |
186 | } |
187 | |
188 | xpthread_barrier_init (barrier: &barrier, NULL, count: 2); |
189 | bool delayed_thread_check_access = true; |
190 | pthread_t delayed_thread = xpthread_create |
191 | (NULL, thread_func: &delayed_thread_func, closure: &delayed_thread_check_access); |
192 | |
193 | keys[0] = pkey_alloc (flags: 0, access_rights: 0); |
194 | if (keys[0] < 0) |
195 | { |
196 | if (errno == ENOSYS) |
197 | FAIL_UNSUPPORTED |
198 | ("kernel does not support memory protection keys" ); |
199 | if (errno == EINVAL) |
200 | FAIL_UNSUPPORTED |
201 | ("CPU does not support memory protection keys: %m" ); |
202 | if (errno == ENOSPC) |
203 | FAIL_UNSUPPORTED |
204 | ("no keys available or kernel does not support memory" |
205 | " protection keys" ); |
206 | FAIL_EXIT1 ("pkey_alloc: %m" ); |
207 | } |
208 | TEST_COMPARE (pkey_get (keys[0]), 0); |
209 | for (int i = 1; i < key_count; ++i) |
210 | { |
211 | keys[i] = pkey_alloc (flags: 0, access_rights: i); |
212 | if (keys[i] < 0) |
213 | FAIL_EXIT1 ("pkey_alloc (0, %d): %m" , i); |
214 | /* pkey_alloc is supposed to change the current thread's access |
215 | rights for the new key. */ |
216 | TEST_COMPARE (pkey_get (keys[i]), i); |
217 | } |
218 | /* Check that all the keys have the expected access rights for the |
219 | current thread. */ |
220 | for (int i = 0; i < key_count; ++i) |
221 | TEST_COMPARE (pkey_get (keys[i]), i); |
222 | |
223 | /* Allocate a test page for each key. */ |
224 | for (int i = 0; i < key_count; ++i) |
225 | { |
226 | pages[i] = xmmap (NULL, length: pagesize, PROT_READ | PROT_WRITE, |
227 | MAP_ANONYMOUS | MAP_PRIVATE, fd: -1); |
228 | TEST_COMPARE (pkey_mprotect ((void *) pages[i], pagesize, |
229 | PROT_READ | PROT_WRITE, keys[i]), 0); |
230 | } |
231 | |
232 | /* Check that the initial thread does not have access to the new |
233 | keys. */ |
234 | { |
235 | pthread_barrier_wait (barrier: &barrier); |
236 | struct thread_result *result = xpthread_join (thr: delayed_thread); |
237 | for (int i = 0; i < key_count; ++i) |
238 | TEST_COMPARE (result->access_rights[i], |
239 | PKEY_DISABLE_ACCESS); |
240 | struct thread_result *result2 = xpthread_join (thr: result->next_thread); |
241 | for (int i = 0; i < key_count; ++i) |
242 | TEST_COMPARE (result->access_rights[i], |
243 | PKEY_DISABLE_ACCESS); |
244 | free (ptr: result); |
245 | free (ptr: result2); |
246 | } |
247 | |
248 | /* Check that the current thread access rights are inherited by new |
249 | threads. */ |
250 | { |
251 | pthread_t get_thread = xpthread_create (NULL, thread_func: get_thread_func, NULL); |
252 | struct thread_result *result = xpthread_join (thr: get_thread); |
253 | for (int i = 0; i < key_count; ++i) |
254 | TEST_COMPARE (result->access_rights[i], i); |
255 | free (ptr: result); |
256 | } |
257 | |
258 | for (int i = 0; i < key_count; ++i) |
259 | TEST_COMPARE (pkey_get (keys[i]), i); |
260 | |
261 | /* Check that in a signal handler, there is no access. */ |
262 | xsignal (SIGUSR1, handler: &sigusr1_handler); |
263 | xraise (SIGUSR1); |
264 | xsignal (SIGUSR1, SIG_DFL); |
265 | TEST_COMPARE (sigusr1_handler_ran, 1); |
266 | |
267 | /* The first key results in a writable page. */ |
268 | TEST_VERIFY (check_page_access (0, false)); |
269 | TEST_VERIFY (check_page_access (0, true)); |
270 | |
271 | /* The other keys do not. */ |
272 | for (int i = 1; i < key_count; ++i) |
273 | { |
274 | if (test_verbose) |
275 | printf (format: "info: checking access for key %d, bits 0x%x\n" , |
276 | i, pkey_get (key: keys[i])); |
277 | for (int j = 0; j < key_count; ++j) |
278 | TEST_COMPARE (pkey_get (keys[j]), j); |
279 | if (i & PKEY_DISABLE_ACCESS) |
280 | { |
281 | TEST_VERIFY (!check_page_access (i, false)); |
282 | TEST_VERIFY (!check_page_access (i, true)); |
283 | } |
284 | else |
285 | { |
286 | TEST_VERIFY (i & PKEY_DISABLE_WRITE); |
287 | TEST_VERIFY (check_page_access (i, false)); |
288 | TEST_VERIFY (!check_page_access (i, true)); |
289 | } |
290 | } |
291 | |
292 | /* But if we set the current thread's access rights, we gain |
293 | access. */ |
294 | for (int do_write = 0; do_write < 2; ++do_write) |
295 | for (int allowed_key = 0; allowed_key < key_count; ++allowed_key) |
296 | { |
297 | for (int i = 0; i < key_count; ++i) |
298 | if (i == allowed_key) |
299 | { |
300 | if (do_write) |
301 | TEST_COMPARE (pkey_set (keys[i], 0), 0); |
302 | else |
303 | TEST_COMPARE (pkey_set (keys[i], PKEY_DISABLE_WRITE), 0); |
304 | } |
305 | else |
306 | TEST_COMPARE (pkey_set (keys[i], PKEY_DISABLE_ACCESS), 0); |
307 | |
308 | if (test_verbose) |
309 | printf (format: "info: key %d is allowed access for %s\n" , |
310 | allowed_key, do_write ? "writing" : "reading" ); |
311 | for (int i = 0; i < key_count; ++i) |
312 | if (i == allowed_key) |
313 | { |
314 | TEST_VERIFY (check_page_access (i, false)); |
315 | TEST_VERIFY (check_page_access (i, true) == do_write); |
316 | } |
317 | else |
318 | { |
319 | TEST_VERIFY (!check_page_access (i, false)); |
320 | TEST_VERIFY (!check_page_access (i, true)); |
321 | } |
322 | } |
323 | |
324 | /* Restore access to all keys, and launch a thread which should |
325 | inherit that access. */ |
326 | for (int i = 0; i < key_count; ++i) |
327 | { |
328 | TEST_COMPARE (pkey_set (keys[i], 0), 0); |
329 | TEST_VERIFY (check_page_access (i, false)); |
330 | TEST_VERIFY (check_page_access (i, true)); |
331 | } |
332 | delayed_thread_check_access = false; |
333 | delayed_thread = xpthread_create |
334 | (NULL, thread_func: delayed_thread_func, closure: &delayed_thread_check_access); |
335 | |
336 | TEST_COMPARE (pkey_free (keys[0]), 0); |
337 | /* Second pkey_free will fail because the key has already been |
338 | freed. */ |
339 | TEST_COMPARE (pkey_free (keys[0]),-1); |
340 | TEST_COMPARE (errno, EINVAL); |
341 | for (int i = 1; i < key_count; ++i) |
342 | TEST_COMPARE (pkey_free (keys[i]), 0); |
343 | |
344 | /* Check what happens to running threads which have access to |
345 | previously allocated protection keys. The implemented behavior |
346 | is somewhat dubious: Ideally, pkey_free should revoke access to |
347 | that key and pkey_alloc of the same (numeric) key should not |
348 | implicitly confer access to already-running threads, but this is |
349 | not what happens in practice. */ |
350 | { |
351 | /* The limit is in place to avoid running indefinitely in case |
352 | there many keys available. */ |
353 | int *keys_array = xcalloc (n: 100000, s: sizeof (*keys_array)); |
354 | int keys_allocated = 0; |
355 | while (keys_allocated < 100000) |
356 | { |
357 | int new_key = pkey_alloc (flags: 0, PKEY_DISABLE_WRITE); |
358 | if (new_key < 0) |
359 | { |
360 | /* No key reuse observed before running out of keys. */ |
361 | TEST_COMPARE (errno, ENOSPC); |
362 | break; |
363 | } |
364 | for (int i = 0; i < key_count; ++i) |
365 | if (new_key == keys[i]) |
366 | { |
367 | /* We allocated the key with disabled write access. |
368 | This should affect the protection state of the |
369 | existing page. */ |
370 | TEST_VERIFY (check_page_access (i, false)); |
371 | TEST_VERIFY (!check_page_access (i, true)); |
372 | |
373 | xpthread_barrier_wait (barrier: &barrier); |
374 | struct thread_result *result = xpthread_join (thr: delayed_thread); |
375 | /* The thread which was launched before should still have |
376 | access to the key. */ |
377 | TEST_COMPARE (result->access_rights[i], 0); |
378 | struct thread_result *result2 |
379 | = xpthread_join (thr: result->next_thread); |
380 | /* Same for a thread which is launched afterwards from |
381 | the old thread. */ |
382 | TEST_COMPARE (result2->access_rights[i], 0); |
383 | free (ptr: result); |
384 | free (ptr: result2); |
385 | keys_array[keys_allocated++] = new_key; |
386 | goto after_key_search; |
387 | } |
388 | /* Save key for later deallocation. */ |
389 | keys_array[keys_allocated++] = new_key; |
390 | } |
391 | after_key_search: |
392 | /* Deallocate the keys allocated for testing purposes. */ |
393 | for (int j = 0; j < keys_allocated; ++j) |
394 | TEST_COMPARE (pkey_free (keys_array[j]), 0); |
395 | free (ptr: keys_array); |
396 | } |
397 | |
398 | for (int i = 0; i < key_count; ++i) |
399 | xmunmap (addr: (void *) pages[i], length: pagesize); |
400 | |
401 | xpthread_barrier_destroy (barrier: &barrier); |
402 | return 0; |
403 | } |
404 | |
405 | #include <support/test-driver.c> |
406 | |