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:
parent
61da50b76b
commit
915b7f6f9a
@ -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
|
||||
|
@ -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
|
||||
|
284
tools/testing/selftests/powerpc/tm/tm-signal-pagefault.c
Normal file
284
tools/testing/selftests/powerpc/tm/tm-signal-pagefault.c
Normal 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");
|
||||
}
|
Loading…
Reference in New Issue
Block a user