| 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 | if (pkey_get (key: keys[0]) < 0) |
| 209 | { |
| 210 | if (errno == ENOSYS) |
| 211 | FAIL_UNSUPPORTED |
| 212 | ("glibc does not support memory protection keys" ); |
| 213 | FAIL_EXIT1 ("pkey_alloc: %m" ); |
| 214 | } |
| 215 | for (int i = 1; i < key_count; ++i) |
| 216 | { |
| 217 | keys[i] = pkey_alloc (flags: 0, access_rights: i); |
| 218 | if (keys[i] < 0) |
| 219 | FAIL_EXIT1 ("pkey_alloc (0, %d): %m" , i); |
| 220 | /* pkey_alloc is supposed to change the current thread's access |
| 221 | rights for the new key. */ |
| 222 | TEST_COMPARE (pkey_get (keys[i]), i); |
| 223 | } |
| 224 | /* Check that all the keys have the expected access rights for the |
| 225 | current thread. */ |
| 226 | for (int i = 0; i < key_count; ++i) |
| 227 | TEST_COMPARE (pkey_get (keys[i]), i); |
| 228 | |
| 229 | /* Allocate a test page for each key. */ |
| 230 | for (int i = 0; i < key_count; ++i) |
| 231 | { |
| 232 | pages[i] = xmmap (NULL, length: pagesize, PROT_READ | PROT_WRITE, |
| 233 | MAP_ANONYMOUS | MAP_PRIVATE, fd: -1); |
| 234 | TEST_COMPARE (pkey_mprotect ((void *) pages[i], pagesize, |
| 235 | PROT_READ | PROT_WRITE, keys[i]), 0); |
| 236 | } |
| 237 | |
| 238 | /* Check that the initial thread does not have access to the new |
| 239 | keys. */ |
| 240 | { |
| 241 | pthread_barrier_wait (barrier: &barrier); |
| 242 | struct thread_result *result = xpthread_join (thr: delayed_thread); |
| 243 | for (int i = 0; i < key_count; ++i) |
| 244 | TEST_COMPARE (result->access_rights[i], |
| 245 | PKEY_DISABLE_ACCESS); |
| 246 | struct thread_result *result2 = xpthread_join (thr: result->next_thread); |
| 247 | for (int i = 0; i < key_count; ++i) |
| 248 | TEST_COMPARE (result->access_rights[i], |
| 249 | PKEY_DISABLE_ACCESS); |
| 250 | free (ptr: result); |
| 251 | free (ptr: result2); |
| 252 | } |
| 253 | |
| 254 | /* Check that the current thread access rights are inherited by new |
| 255 | threads. */ |
| 256 | { |
| 257 | pthread_t get_thread = xpthread_create (NULL, thread_func: get_thread_func, NULL); |
| 258 | struct thread_result *result = xpthread_join (thr: get_thread); |
| 259 | for (int i = 0; i < key_count; ++i) |
| 260 | TEST_COMPARE (result->access_rights[i], i); |
| 261 | free (ptr: result); |
| 262 | } |
| 263 | |
| 264 | for (int i = 0; i < key_count; ++i) |
| 265 | TEST_COMPARE (pkey_get (keys[i]), i); |
| 266 | |
| 267 | /* Check that in a signal handler, there is no access. */ |
| 268 | xsignal (SIGUSR1, handler: &sigusr1_handler); |
| 269 | xraise (SIGUSR1); |
| 270 | xsignal (SIGUSR1, SIG_DFL); |
| 271 | TEST_COMPARE (sigusr1_handler_ran, 1); |
| 272 | |
| 273 | /* The first key results in a writable page. */ |
| 274 | TEST_VERIFY (check_page_access (0, false)); |
| 275 | TEST_VERIFY (check_page_access (0, true)); |
| 276 | |
| 277 | /* The other keys do not. */ |
| 278 | for (int i = 1; i < key_count; ++i) |
| 279 | { |
| 280 | if (test_verbose) |
| 281 | printf (format: "info: checking access for key %d, bits 0x%x\n" , |
| 282 | i, pkey_get (key: keys[i])); |
| 283 | for (int j = 0; j < key_count; ++j) |
| 284 | TEST_COMPARE (pkey_get (keys[j]), j); |
| 285 | if (i & PKEY_DISABLE_ACCESS) |
| 286 | { |
| 287 | TEST_VERIFY (!check_page_access (i, false)); |
| 288 | TEST_VERIFY (!check_page_access (i, true)); |
| 289 | } |
| 290 | else |
| 291 | { |
| 292 | TEST_VERIFY (i & PKEY_DISABLE_WRITE); |
| 293 | TEST_VERIFY (check_page_access (i, false)); |
| 294 | TEST_VERIFY (!check_page_access (i, true)); |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | /* But if we set the current thread's access rights, we gain |
| 299 | access. */ |
| 300 | for (int do_write = 0; do_write < 2; ++do_write) |
| 301 | for (int allowed_key = 0; allowed_key < key_count; ++allowed_key) |
| 302 | { |
| 303 | for (int i = 0; i < key_count; ++i) |
| 304 | if (i == allowed_key) |
| 305 | { |
| 306 | if (do_write) |
| 307 | TEST_COMPARE (pkey_set (keys[i], 0), 0); |
| 308 | else |
| 309 | TEST_COMPARE (pkey_set (keys[i], PKEY_DISABLE_WRITE), 0); |
| 310 | } |
| 311 | else |
| 312 | TEST_COMPARE (pkey_set (keys[i], PKEY_DISABLE_ACCESS), 0); |
| 313 | |
| 314 | if (test_verbose) |
| 315 | printf (format: "info: key %d is allowed access for %s\n" , |
| 316 | allowed_key, do_write ? "writing" : "reading" ); |
| 317 | for (int i = 0; i < key_count; ++i) |
| 318 | if (i == allowed_key) |
| 319 | { |
| 320 | TEST_VERIFY (check_page_access (i, false)); |
| 321 | TEST_VERIFY (check_page_access (i, true) == do_write); |
| 322 | } |
| 323 | else |
| 324 | { |
| 325 | TEST_VERIFY (!check_page_access (i, false)); |
| 326 | TEST_VERIFY (!check_page_access (i, true)); |
| 327 | } |
| 328 | } |
| 329 | |
| 330 | /* Restore access to all keys, and launch a thread which should |
| 331 | inherit that access. */ |
| 332 | for (int i = 0; i < key_count; ++i) |
| 333 | { |
| 334 | TEST_COMPARE (pkey_set (keys[i], 0), 0); |
| 335 | TEST_VERIFY (check_page_access (i, false)); |
| 336 | TEST_VERIFY (check_page_access (i, true)); |
| 337 | } |
| 338 | delayed_thread_check_access = false; |
| 339 | delayed_thread = xpthread_create |
| 340 | (NULL, thread_func: delayed_thread_func, closure: &delayed_thread_check_access); |
| 341 | |
| 342 | TEST_COMPARE (pkey_free (keys[0]), 0); |
| 343 | /* Second pkey_free will fail because the key has already been |
| 344 | freed. */ |
| 345 | TEST_COMPARE (pkey_free (keys[0]),-1); |
| 346 | TEST_COMPARE (errno, EINVAL); |
| 347 | for (int i = 1; i < key_count; ++i) |
| 348 | TEST_COMPARE (pkey_free (keys[i]), 0); |
| 349 | |
| 350 | /* Check what happens to running threads which have access to |
| 351 | previously allocated protection keys. The implemented behavior |
| 352 | is somewhat dubious: Ideally, pkey_free should revoke access to |
| 353 | that key and pkey_alloc of the same (numeric) key should not |
| 354 | implicitly confer access to already-running threads, but this is |
| 355 | not what happens in practice. */ |
| 356 | { |
| 357 | /* The limit is in place to avoid running indefinitely in case |
| 358 | there many keys available. */ |
| 359 | int *keys_array = xcalloc (n: 100000, s: sizeof (*keys_array)); |
| 360 | int keys_allocated = 0; |
| 361 | while (keys_allocated < 100000) |
| 362 | { |
| 363 | int new_key = pkey_alloc (flags: 0, PKEY_DISABLE_WRITE); |
| 364 | if (new_key < 0) |
| 365 | { |
| 366 | /* No key reuse observed before running out of keys. */ |
| 367 | TEST_COMPARE (errno, ENOSPC); |
| 368 | break; |
| 369 | } |
| 370 | for (int i = 0; i < key_count; ++i) |
| 371 | if (new_key == keys[i]) |
| 372 | { |
| 373 | /* We allocated the key with disabled write access. |
| 374 | This should affect the protection state of the |
| 375 | existing page. */ |
| 376 | TEST_VERIFY (check_page_access (i, false)); |
| 377 | TEST_VERIFY (!check_page_access (i, true)); |
| 378 | |
| 379 | xpthread_barrier_wait (barrier: &barrier); |
| 380 | struct thread_result *result = xpthread_join (thr: delayed_thread); |
| 381 | /* The thread which was launched before should still have |
| 382 | access to the key. */ |
| 383 | TEST_COMPARE (result->access_rights[i], 0); |
| 384 | struct thread_result *result2 |
| 385 | = xpthread_join (thr: result->next_thread); |
| 386 | /* Same for a thread which is launched afterwards from |
| 387 | the old thread. */ |
| 388 | TEST_COMPARE (result2->access_rights[i], 0); |
| 389 | free (ptr: result); |
| 390 | free (ptr: result2); |
| 391 | keys_array[keys_allocated++] = new_key; |
| 392 | goto after_key_search; |
| 393 | } |
| 394 | /* Save key for later deallocation. */ |
| 395 | keys_array[keys_allocated++] = new_key; |
| 396 | } |
| 397 | after_key_search: |
| 398 | /* Deallocate the keys allocated for testing purposes. */ |
| 399 | for (int j = 0; j < keys_allocated; ++j) |
| 400 | TEST_COMPARE (pkey_free (keys_array[j]), 0); |
| 401 | free (ptr: keys_array); |
| 402 | } |
| 403 | |
| 404 | for (int i = 0; i < key_count; ++i) |
| 405 | xmunmap (addr: (void *) pages[i], length: pagesize); |
| 406 | |
| 407 | xpthread_barrier_destroy (barrier: &barrier); |
| 408 | return 0; |
| 409 | } |
| 410 | |
| 411 | #include <support/test-driver.c> |
| 412 | |