summaryrefslogtreecommitdiff
path: root/sysdeps/unix/sysv/linux/tst-pkey.c
diff options
context:
space:
mode:
Diffstat (limited to 'sysdeps/unix/sysv/linux/tst-pkey.c')
-rw-r--r--sysdeps/unix/sysv/linux/tst-pkey.c399
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..5f721d4444
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/tst-pkey.c
@@ -0,0 +1,399 @@
+/* Tests for memory protection keys.
+ Copyright (C) 2017-2018 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>