diff options
Diffstat (limited to 'sysdeps/unix/sysv/linux/tst-pkey.c')
-rw-r--r-- | sysdeps/unix/sysv/linux/tst-pkey.c | 399 |
1 files changed, 399 insertions, 0 deletions
diff --git a/sysdeps/unix/sysv/linux/tst-pkey.c b/sysdeps/unix/sysv/linux/tst-pkey.c new file mode 100644 index 0000000000..e7205dba1f --- /dev/null +++ b/sysdeps/unix/sysv/linux/tst-pkey.c @@ -0,0 +1,399 @@ +/* Tests for memory protection keys. + Copyright (C) 2017 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + <http://www.gnu.org/licenses/>. */ + +#include <errno.h> +#include <inttypes.h> +#include <setjmp.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <support/check.h> +#include <support/support.h> +#include <support/test-driver.h> +#include <support/xsignal.h> +#include <support/xthread.h> +#include <support/xunistd.h> +#include <sys/mman.h> + +/* Used to force threads to wait until the main thread has set up the + keys as intended. */ +static pthread_barrier_t barrier; + +/* The keys used for testing. These have been allocated with access + rights set based on their array index. */ +enum { key_count = 4 }; +static int keys[key_count]; +static volatile int *pages[key_count]; + +/* Used to report results from the signal handler. */ +static volatile void *sigsegv_addr; +static volatile int sigsegv_code; +static volatile int sigsegv_pkey; +static sigjmp_buf sigsegv_jmp; + +/* Used to handle expected read or write faults. */ +static void +sigsegv_handler (int signum, siginfo_t *info, void *context) +{ + sigsegv_addr = info->si_addr; + sigsegv_code = info->si_code; + sigsegv_pkey = info->si_pkey; + siglongjmp (sigsegv_jmp, 2); +} + +static const struct sigaction sigsegv_sigaction = + { + .sa_flags = SA_RESETHAND | SA_SIGINFO, + .sa_sigaction = &sigsegv_handler, + }; + +/* Check if PAGE is readable (if !WRITE) or writable (if WRITE). */ +static bool +check_page_access (int page, bool write) +{ + /* This is needed to work around bug 22396: On x86-64, siglongjmp + does not restore the protection key access rights for the current + thread. We restore only the access rights for the keys under + test. (This is not a general solution to this problem, but it + allows testing to proceed after a fault.) */ + unsigned saved_rights[key_count]; + for (int i = 0; i < key_count; ++i) + saved_rights[i] = pkey_get (keys[i]); + + volatile int *addr = pages[page]; + if (test_verbose > 0) + { + printf ("info: checking access at %p (page %d) for %s\n", + addr, page, write ? "writing" : "reading"); + } + int result = sigsetjmp (sigsegv_jmp, 1); + if (result == 0) + { + xsigaction (SIGSEGV, &sigsegv_sigaction, NULL); + if (write) + *addr = 3; + else + (void) *addr; + xsignal (SIGSEGV, SIG_DFL); + if (test_verbose > 0) + puts (" --> access allowed"); + return true; + } + else + { + xsignal (SIGSEGV, SIG_DFL); + if (test_verbose > 0) + puts (" --> access denied"); + TEST_COMPARE (result, 2); + TEST_COMPARE ((uintptr_t) sigsegv_addr, (uintptr_t) addr); + TEST_COMPARE (sigsegv_code, SEGV_PKUERR); + TEST_COMPARE (sigsegv_pkey, keys[page]); + for (int i = 0; i < key_count; ++i) + TEST_COMPARE (pkey_set (keys[i], saved_rights[i]), 0); + return false; + } +} + +static volatile sig_atomic_t sigusr1_handler_ran; + +/* Used to check that access is revoked in signal handlers. */ +static void +sigusr1_handler (int signum) +{ + TEST_COMPARE (signum, SIGUSR1); + for (int i = 0; i < key_count; ++i) + TEST_COMPARE (pkey_get (keys[i]), PKEY_DISABLE_ACCESS); + sigusr1_handler_ran = 1; +} + +/* Used to report results from other threads. */ +struct thread_result +{ + int access_rights[key_count]; + pthread_t next_thread; +}; + +/* Return the thread's access rights for the keys under test. */ +static void * +get_thread_func (void *closure) +{ + struct thread_result *result = xmalloc (sizeof (*result)); + for (int i = 0; i < key_count; ++i) + result->access_rights[i] = pkey_get (keys[i]); + memset (&result->next_thread, 0, sizeof (result->next_thread)); + return result; +} + +/* Wait for initialization and then check that the current thread does + not have access through the keys under test. */ +static void * +delayed_thread_func (void *closure) +{ + bool check_access = *(bool *) closure; + pthread_barrier_wait (&barrier); + struct thread_result *result = get_thread_func (NULL); + + if (check_access) + { + /* Also check directly. This code should not run with other + threads in parallel because of the SIGSEGV handler which is + installed by check_page_access. */ + for (int i = 0; i < key_count; ++i) + { + TEST_VERIFY (!check_page_access (i, false)); + TEST_VERIFY (!check_page_access (i, true)); + } + } + + result->next_thread = xpthread_create (NULL, get_thread_func, NULL); + return result; +} + +static int +do_test (void) +{ + long pagesize = xsysconf (_SC_PAGESIZE); + + /* pkey_mprotect with key -1 should work even when there is no + protection key support. */ + { + int *page = xmmap (NULL, pagesize, PROT_NONE, + MAP_ANONYMOUS | MAP_PRIVATE, -1); + TEST_COMPARE (pkey_mprotect (page, pagesize, PROT_READ | PROT_WRITE, -1), + 0); + volatile int *vpage = page; + *vpage = 5; + TEST_COMPARE (*vpage, 5); + xmunmap (page, pagesize); + } + + xpthread_barrier_init (&barrier, NULL, 2); + bool delayed_thread_check_access = true; + pthread_t delayed_thread = xpthread_create + (NULL, &delayed_thread_func, &delayed_thread_check_access); + + keys[0] = pkey_alloc (0, 0); + if (keys[0] < 0) + { + if (errno == ENOSYS) + FAIL_UNSUPPORTED + ("kernel does not support memory protection keys"); + if (errno == EINVAL) + FAIL_UNSUPPORTED + ("CPU does not support memory protection keys: %m"); + FAIL_EXIT1 ("pkey_alloc: %m"); + } + TEST_COMPARE (pkey_get (keys[0]), 0); + for (int i = 1; i < key_count; ++i) + { + keys[i] = pkey_alloc (0, i); + if (keys[i] < 0) + FAIL_EXIT1 ("pkey_alloc (0, %d): %m", i); + /* pkey_alloc is supposed to change the current thread's access + rights for the new key. */ + TEST_COMPARE (pkey_get (keys[i]), i); + } + /* Check that all the keys have the expected access rights for the + current thread. */ + for (int i = 0; i < key_count; ++i) + TEST_COMPARE (pkey_get (keys[i]), i); + + /* Allocate a test page for each key. */ + for (int i = 0; i < key_count; ++i) + { + pages[i] = xmmap (NULL, pagesize, PROT_READ | PROT_WRITE, + MAP_ANONYMOUS | MAP_PRIVATE, -1); + TEST_COMPARE (pkey_mprotect ((void *) pages[i], pagesize, + PROT_READ | PROT_WRITE, keys[i]), 0); + } + + /* Check that the initial thread does not have access to the new + keys. */ + { + pthread_barrier_wait (&barrier); + struct thread_result *result = xpthread_join (delayed_thread); + for (int i = 0; i < key_count; ++i) + TEST_COMPARE (result->access_rights[i], + PKEY_DISABLE_ACCESS); + struct thread_result *result2 = xpthread_join (result->next_thread); + for (int i = 0; i < key_count; ++i) + TEST_COMPARE (result->access_rights[i], + PKEY_DISABLE_ACCESS); + free (result); + free (result2); + } + + /* Check that the current thread access rights are inherited by new + threads. */ + { + pthread_t get_thread = xpthread_create (NULL, get_thread_func, NULL); + struct thread_result *result = xpthread_join (get_thread); + for (int i = 0; i < key_count; ++i) + TEST_COMPARE (result->access_rights[i], i); + free (result); + } + + for (int i = 0; i < key_count; ++i) + TEST_COMPARE (pkey_get (keys[i]), i); + + /* Check that in a signal handler, there is no access. */ + xsignal (SIGUSR1, &sigusr1_handler); + xraise (SIGUSR1); + xsignal (SIGUSR1, SIG_DFL); + TEST_COMPARE (sigusr1_handler_ran, 1); + + /* The first key results in a writable page. */ + TEST_VERIFY (check_page_access (0, false)); + TEST_VERIFY (check_page_access (0, true)); + + /* The other keys do not. */ + for (int i = 1; i < key_count; ++i) + { + if (test_verbose) + printf ("info: checking access for key %d, bits 0x%x\n", + i, pkey_get (keys[i])); + for (int j = 0; j < key_count; ++j) + TEST_COMPARE (pkey_get (keys[j]), j); + if (i & PKEY_DISABLE_ACCESS) + { + TEST_VERIFY (!check_page_access (i, false)); + TEST_VERIFY (!check_page_access (i, true)); + } + else + { + TEST_VERIFY (i & PKEY_DISABLE_WRITE); + TEST_VERIFY (check_page_access (i, false)); + TEST_VERIFY (!check_page_access (i, true)); + } + } + + /* But if we set the current thread's access rights, we gain + access. */ + for (int do_write = 0; do_write < 2; ++do_write) + for (int allowed_key = 0; allowed_key < key_count; ++allowed_key) + { + for (int i = 0; i < key_count; ++i) + if (i == allowed_key) + { + if (do_write) + TEST_COMPARE (pkey_set (keys[i], 0), 0); + else + TEST_COMPARE (pkey_set (keys[i], PKEY_DISABLE_WRITE), 0); + } + else + TEST_COMPARE (pkey_set (keys[i], PKEY_DISABLE_ACCESS), 0); + + if (test_verbose) + printf ("info: key %d is allowed access for %s\n", + allowed_key, do_write ? "writing" : "reading"); + for (int i = 0; i < key_count; ++i) + if (i == allowed_key) + { + TEST_VERIFY (check_page_access (i, false)); + TEST_VERIFY (check_page_access (i, true) == do_write); + } + else + { + TEST_VERIFY (!check_page_access (i, false)); + TEST_VERIFY (!check_page_access (i, true)); + } + } + + /* Restore access to all keys, and launch a thread which should + inherit that access. */ + for (int i = 0; i < key_count; ++i) + { + TEST_COMPARE (pkey_set (keys[i], 0), 0); + TEST_VERIFY (check_page_access (i, false)); + TEST_VERIFY (check_page_access (i, true)); + } + delayed_thread_check_access = false; + delayed_thread = xpthread_create + (NULL, delayed_thread_func, &delayed_thread_check_access); + + TEST_COMPARE (pkey_free (keys[0]), 0); + /* Second pkey_free will fail because the key has already been + freed. */ + TEST_COMPARE (pkey_free (keys[0]),-1); + TEST_COMPARE (errno, EINVAL); + for (int i = 1; i < key_count; ++i) + TEST_COMPARE (pkey_free (keys[i]), 0); + + /* Check what happens to running threads which have access to + previously allocated protection keys. The implemented behavior + is somewhat dubious: Ideally, pkey_free should revoke access to + that key and pkey_alloc of the same (numeric) key should not + implicitly confer access to already-running threads, but this is + not what happens in practice. */ + { + /* The limit is in place to avoid running indefinitely in case + there many keys available. */ + int *keys_array = xcalloc (100000, sizeof (*keys_array)); + int keys_allocated = 0; + while (keys_allocated < 100000) + { + int new_key = pkey_alloc (0, PKEY_DISABLE_WRITE); + if (new_key < 0) + { + /* No key reuse observed before running out of keys. */ + TEST_COMPARE (errno, ENOSPC); + break; + } + for (int i = 0; i < key_count; ++i) + if (new_key == keys[i]) + { + /* We allocated the key with disabled write access. + This should affect the protection state of the + existing page. */ + TEST_VERIFY (check_page_access (i, false)); + TEST_VERIFY (!check_page_access (i, true)); + + xpthread_barrier_wait (&barrier); + struct thread_result *result = xpthread_join (delayed_thread); + /* The thread which was launched before should still have + access to the key. */ + TEST_COMPARE (result->access_rights[i], 0); + struct thread_result *result2 + = xpthread_join (result->next_thread); + /* Same for a thread which is launched afterwards from + the old thread. */ + TEST_COMPARE (result2->access_rights[i], 0); + free (result); + free (result2); + keys_array[keys_allocated++] = new_key; + goto after_key_search; + } + /* Save key for later deallocation. */ + keys_array[keys_allocated++] = new_key; + } + after_key_search: + /* Deallocate the keys allocated for testing purposes. */ + for (int j = 0; j < keys_allocated; ++j) + TEST_COMPARE (pkey_free (keys_array[j]), 0); + free (keys_array); + } + + for (int i = 0; i < key_count; ++i) + xmunmap ((void *) pages[i], pagesize); + + xpthread_barrier_destroy (&barrier); + return 0; +} + +#include <support/test-driver.c> |