Blob Blame History Raw
/* 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>