selftests/powerpc: Add tm-signal-pagefault test

This test triggers a TM Bad Thing by raising a signal in transactional state
and forcing a pagefault to happen in kernelspace when the kernel signal
handling code first touches the user signal stack.

This is inspired by the test tm-signal-context-force-tm but uses userfaultfd to
make the test deterministic. While this test always triggers the bug in one
run, I had to execute tm-signal-context-force-tm several times (the test runs
5000 times each execution) to trigger the same bug.

tm-signal-context-force-tm is kept instead of replaced because, while this test
is more reliable and triggers the same bug, tm-signal-context-force-tm has a
better coverage, in the sense that by running the test several times it might
trigger the pagefault and/or be preempted at different places.

v3: skip test if userfaultfd is unavailable.

Signed-off-by: Gustavo Luiz Duarte <gustavold@linux.ibm.com>
Signed-off-by: Michael Ellerman <mpe@ellerman.id.au>
Link: https://lore.kernel.org/r/20200211033831.11165-2-gustavold@linux.ibm.com
This commit is contained in:
Gustavo Luiz Duarte 2020-02-11 00:38:30 -03:00 committed by Michael Ellerman
parent 61da50b76b
commit 915b7f6f9a
3 changed files with 287 additions and 1 deletions

View File

@ -13,6 +13,7 @@ tm-signal-context-chk-vmx
tm-signal-context-chk-vsx
tm-signal-context-force-tm
tm-signal-sigreturn-nt
tm-signal-pagefault
tm-vmx-unavail
tm-unavailable
tm-trap

View File

@ -5,7 +5,7 @@ SIGNAL_CONTEXT_CHK_TESTS := tm-signal-context-chk-gpr tm-signal-context-chk-fpu
TEST_GEN_PROGS := tm-resched-dscr tm-syscall tm-signal-msr-resv tm-signal-stack \
tm-vmxcopy tm-fork tm-tar tm-tmspr tm-vmx-unavail tm-unavailable tm-trap \
$(SIGNAL_CONTEXT_CHK_TESTS) tm-sigreturn tm-signal-sigreturn-nt \
tm-signal-context-force-tm tm-poison
tm-signal-context-force-tm tm-poison tm-signal-pagefault
top_srcdir = ../../../../..
include ../../lib.mk
@ -22,6 +22,7 @@ $(OUTPUT)/tm-resched-dscr: ../pmu/lib.c
$(OUTPUT)/tm-unavailable: CFLAGS += -O0 -pthread -m64 -Wno-error=uninitialized -mvsx
$(OUTPUT)/tm-trap: CFLAGS += -O0 -pthread -m64
$(OUTPUT)/tm-signal-context-force-tm: CFLAGS += -pthread -m64
$(OUTPUT)/tm-signal-pagefault: CFLAGS += -pthread -m64
SIGNAL_CONTEXT_CHK_TESTS := $(patsubst %,$(OUTPUT)/%,$(SIGNAL_CONTEXT_CHK_TESTS))
$(SIGNAL_CONTEXT_CHK_TESTS): tm-signal.S

View File

@ -0,0 +1,284 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright 2020, Gustavo Luiz Duarte, IBM Corp.
*
* This test starts a transaction and triggers a signal, forcing a pagefault to
* happen when the kernel signal handling code touches the user signal stack.
*
* In order to avoid pre-faulting the signal stack memory and to force the
* pagefault to happen precisely in the kernel signal handling code, the
* pagefault handling is done in userspace using the userfaultfd facility.
*
* Further pagefaults are triggered by crafting the signal handler's ucontext
* to point to additional memory regions managed by the userfaultfd, so using
* the same mechanism used to avoid pre-faulting the signal stack memory.
*
* On failure (bug is present) kernel crashes or never returns control back to
* userspace. If bug is not present, tests completes almost immediately.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <pthread.h>
#include <signal.h>
#include <errno.h>
#include "tm.h"
#define UF_MEM_SIZE 655360 /* 10 x 64k pages */
/* Memory handled by userfaultfd */
static char *uf_mem;
static size_t uf_mem_offset = 0;
/*
* Data that will be copied into the faulting pages (instead of zero-filled
* pages). This is used to make the test more reliable and avoid segfaulting
* when we return from the signal handler. Since we are making the signal
* handler's ucontext point to newly allocated memory, when that memory is
* paged-in it will contain the expected content.
*/
static char backing_mem[UF_MEM_SIZE];
static size_t pagesize;
/*
* Return a chunk of at least 'size' bytes of memory that will be handled by
* userfaultfd. If 'backing_data' is not NULL, its content will be save to
* 'backing_mem' and then copied into the faulting pages when the page fault
* is handled.
*/
void *get_uf_mem(size_t size, void *backing_data)
{
void *ret;
if (uf_mem_offset + size > UF_MEM_SIZE) {
fprintf(stderr, "Requesting more uf_mem than expected!\n");
exit(EXIT_FAILURE);
}
ret = &uf_mem[uf_mem_offset];
/* Save the data that will be copied into the faulting page */
if (backing_data != NULL)
memcpy(&backing_mem[uf_mem_offset], backing_data, size);
/* Reserve the requested amount of uf_mem */
uf_mem_offset += size;
/* Keep uf_mem_offset aligned to the page size (round up) */
uf_mem_offset = (uf_mem_offset + pagesize - 1) & ~(pagesize - 1);
return ret;
}
void *fault_handler_thread(void *arg)
{
struct uffd_msg msg; /* Data read from userfaultfd */
long uffd; /* userfaultfd file descriptor */
struct uffdio_copy uffdio_copy;
struct pollfd pollfd;
ssize_t nread, offset;
uffd = (long) arg;
for (;;) {
pollfd.fd = uffd;
pollfd.events = POLLIN;
if (poll(&pollfd, 1, -1) == -1) {
perror("poll() failed");
exit(EXIT_FAILURE);
}
nread = read(uffd, &msg, sizeof(msg));
if (nread == 0) {
fprintf(stderr, "read(): EOF on userfaultfd\n");
exit(EXIT_FAILURE);
}
if (nread == -1) {
perror("read() failed");
exit(EXIT_FAILURE);
}
/* We expect only one kind of event */
if (msg.event != UFFD_EVENT_PAGEFAULT) {
fprintf(stderr, "Unexpected event on userfaultfd\n");
exit(EXIT_FAILURE);
}
/*
* We need to handle page faults in units of pages(!).
* So, round faulting address down to page boundary.
*/
uffdio_copy.dst = msg.arg.pagefault.address & ~(pagesize-1);
offset = (char *) uffdio_copy.dst - uf_mem;
uffdio_copy.src = (unsigned long) &backing_mem[offset];
uffdio_copy.len = pagesize;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1) {
perror("ioctl-UFFDIO_COPY failed");
exit(EXIT_FAILURE);
}
}
}
void setup_uf_mem(void)
{
long uffd; /* userfaultfd file descriptor */
pthread_t thr;
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
int ret;
pagesize = sysconf(_SC_PAGE_SIZE);
/* Create and enable userfaultfd object */
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1) {
perror("userfaultfd() failed");
exit(EXIT_FAILURE);
}
uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) {
perror("ioctl-UFFDIO_API failed");
exit(EXIT_FAILURE);
}
/*
* Create a private anonymous mapping. The memory will be demand-zero
* paged, that is, not yet allocated. When we actually touch the memory
* the related page will be allocated via the userfaultfd mechanism.
*/
uf_mem = mmap(NULL, UF_MEM_SIZE, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (uf_mem == MAP_FAILED) {
perror("mmap() failed");
exit(EXIT_FAILURE);
}
/*
* Register the memory range of the mapping we've just mapped to be
* handled by the userfaultfd object. In 'mode' we request to track
* missing pages (i.e. pages that have not yet been faulted-in).
*/
uffdio_register.range.start = (unsigned long) uf_mem;
uffdio_register.range.len = UF_MEM_SIZE;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) {
perror("ioctl-UFFDIO_REGISTER");
exit(EXIT_FAILURE);
}
/* Create a thread that will process the userfaultfd events */
ret = pthread_create(&thr, NULL, fault_handler_thread, (void *) uffd);
if (ret != 0) {
fprintf(stderr, "pthread_create(): Error. Returned %d\n", ret);
exit(EXIT_FAILURE);
}
}
/*
* Assumption: the signal was delivered while userspace was in transactional or
* suspended state, i.e. uc->uc_link != NULL.
*/
void signal_handler(int signo, siginfo_t *si, void *uc)
{
ucontext_t *ucp = uc;
/* Skip 'trap' after returning, otherwise we get a SIGTRAP again */
ucp->uc_link->uc_mcontext.regs->nip += 4;
ucp->uc_mcontext.v_regs =
get_uf_mem(sizeof(elf_vrreg_t), ucp->uc_mcontext.v_regs);
ucp->uc_link->uc_mcontext.v_regs =
get_uf_mem(sizeof(elf_vrreg_t), ucp->uc_link->uc_mcontext.v_regs);
ucp->uc_link = get_uf_mem(sizeof(ucontext_t), ucp->uc_link);
}
bool have_userfaultfd(void)
{
long rc;
errno = 0;
rc = syscall(__NR_userfaultfd, -1);
return rc == 0 || errno != ENOSYS;
}
int tm_signal_pagefault(void)
{
struct sigaction sa;
stack_t ss;
SKIP_IF(!have_htm());
SKIP_IF(!have_userfaultfd());
setup_uf_mem();
/*
* Set an alternative stack that will generate a page fault when the
* signal is raised. The page fault will be treated via userfaultfd,
* i.e. via fault_handler_thread.
*/
ss.ss_sp = get_uf_mem(SIGSTKSZ, NULL);
ss.ss_size = SIGSTKSZ;
ss.ss_flags = 0;
if (sigaltstack(&ss, NULL) == -1) {
perror("sigaltstack() failed");
exit(EXIT_FAILURE);
}
sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
sa.sa_sigaction = signal_handler;
if (sigaction(SIGTRAP, &sa, NULL) == -1) {
perror("sigaction() failed");
exit(EXIT_FAILURE);
}
/* Trigger a SIGTRAP in transactional state */
asm __volatile__(
"tbegin.;"
"beq 1f;"
"trap;"
"1: ;"
: : : "memory");
/* Trigger a SIGTRAP in suspended state */
asm __volatile__(
"tbegin.;"
"beq 1f;"
"tsuspend.;"
"trap;"
"tresume.;"
"1: ;"
: : : "memory");
return EXIT_SUCCESS;
}
int main(int argc, char **argv)
{
/*
* Depending on kernel config, the TM Bad Thing might not result in a
* crash, instead the kernel never returns control back to userspace, so
* set a tight timeout. If the test passes it completes almost
* immediately.
*/
test_harness_set_timeout(2);
return test_harness(tm_signal_pagefault, "tm_signal_pagefault");
}