On RISC-V, PMU overflow interrupts can be delivered through the SBI
Supervisor Software Events (SSE) mechanism. A perf event that samples
the raw user stack (PERF_SAMPLE_STACK_USER, as perf record
--call-graph dwarf does) then copies a chunk of the interrupted task's
user stack from an NMI-like context. If that copy is allowed to take a
nested page fault it can corrupt the interrupted task's kernel state and
hang or crash the machine under load; this is what
riscv_perf_out_copy_user() and the nofault page-fault change fix.

The existing SSE selftest module exercises the framework (register,
enable, inject, complete, priorities, stress) but never drives the perf
user-stack copy that motivated the no-fault path. Add a userspace test
that closes that gap:

  - Open a sampling hardware PMU event with PERF_SAMPLE_STACK_USER over a
    deep, partially non-resident user stack, drain the ring buffer, and
    verify every SAMPLE record is well formed and never reports more
    dumped bytes than were requested. This checks that a non-resident
    page truncates the dump cleanly instead of faulting or overrunning.
  - Drive a multi-CPU unix-socket + deep-recursion workload under
    high-frequency DWARF sampling; the pass criterion is simply that the
    machine survives, since the original bug took it down.

The test reports SKIP when hardware PMU sampling is unavailable or
perf_event_paranoid forbids it, so it is safe to run unprivileged or in
constrained environments. It is placed under the RISC-V SSE selftests
because SSE delivery is the RISC-V-specific condition it protects, and
is wired into the sse subtarget Makefile alongside the module runner.

Signed-off-by: Zhanpeng Zhang <[email protected]>
---
 tools/testing/selftests/riscv/sse/Makefile    |   5 +
 .../selftests/riscv/sse/sse_perf_ustack.c     | 564 ++++++++++++++++++
 2 files changed, 569 insertions(+)
 create mode 100644 tools/testing/selftests/riscv/sse/sse_perf_ustack.c

diff --git a/tools/testing/selftests/riscv/sse/Makefile 
b/tools/testing/selftests/riscv/sse/Makefile
index 7e2677fdce09..646b418de9ff 100644
--- a/tools/testing/selftests/riscv/sse/Makefile
+++ b/tools/testing/selftests/riscv/sse/Makefile
@@ -1,5 +1,10 @@
+CFLAGS += -I$(top_srcdir)/tools/testing/selftests
+LDLIBS += -lpthread
+
 TEST_GEN_MODS_DIR := module
 
+TEST_GEN_PROGS := sse_perf_ustack
+
 TEST_PROGS := run_sse_test.sh
 
 include ../../lib.mk
diff --git a/tools/testing/selftests/riscv/sse/sse_perf_ustack.c 
b/tools/testing/selftests/riscv/sse/sse_perf_ustack.c
new file mode 100644
index 000000000000..9535b6d7ba4c
--- /dev/null
+++ b/tools/testing/selftests/riscv/sse/sse_perf_ustack.c
@@ -0,0 +1,564 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Regression test for the RISC-V perf user-stack copy taken in SSE
+ * (NMI-like) context.
+ *
+ * On RISC-V, PMU overflow interrupts can be delivered through the SBI
+ * Supervisor Software Events (SSE) mechanism. A perf event that samples the
+ * raw user stack (PERF_SAMPLE_STACK_USER, as perf record --call-graph dwarf
+ * does) then copies a large chunk of the interrupted task's user stack from
+ * that context. If that copy is allowed to take a nested page fault it can
+ * corrupt the interrupted task's kernel state and hang or crash the machine
+ * under load.
+ *
+ * This test exercises that exact path:
+ *   - It opens a sampling hardware PMU event with PERF_SAMPLE_STACK_USER.
+ *   - It samples a child running on a controlled user stack followed by an
+ *     inaccessible page, so the copy must truncate at that page boundary.
+ *   - It checks that every user-stack sample record is well formed and that
+ *     the dumped size never exceeds the requested size (i.e. the copy stops
+ *     cleanly rather than faulting on).
+ *   - It then drives a multi-threaded unix-socket + deep-recursion workload
+ *     under high-frequency per-CPU sampling and requires every active sampler
+ *     to make progress without taking the machine down.
+ *
+ * The test is architecture independent in what it drives; it is placed under
+ * the RISC-V SSE selftests because SSE delivery is the RISC-V-specific
+ * condition it is meant to protect.
+ */
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <poll.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <linux/perf_event.h>
+#include <pthread.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/socket.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+
+#include "../../kselftest.h"
+
+#ifndef noinline
+#define noinline __attribute__((noinline))
+#endif
+
+#define STACK_DUMP_SIZE                8192            /* 8 KiB, 8-byte 
aligned */
+#define RB_DATA_PAGES          64              /* power of two */
+#define SELF_SAMPLE_FREQ       4000
+#define STRESS_SAMPLE_FREQ     5000
+#define STRESS_SECONDS         5
+#define RECURSE_DEPTH          512
+#define TRUNCATION_RUN_MS      250
+
+static long page_size;
+
+static int perf_event_open(struct perf_event_attr *attr, pid_t pid, int cpu,
+                          int group_fd, unsigned long flags)
+{
+       return syscall(__NR_perf_event_open, attr, pid, cpu, group_fd, flags);
+}
+
+/* Prevent the compiler from optimizing away a stack buffer. */
+static void keep_alive(void *p)
+{
+       __asm__ __volatile__("" : : "r"(p) : "memory");
+}
+
+/*
+ * Consume a deep user stack and keep it live, so a raw user-stack sample has
+ * many pages to copy. Returns a value derived from the stack so the compiler
+ * cannot elide the frames.
+ */
+static noinline unsigned long burn_stack(int depth, unsigned long *sink)
+{
+       unsigned long frame[32];
+       unsigned int i;
+
+       for (i = 0; i < ARRAY_SIZE(frame); i++)
+               frame[i] = (unsigned long)depth * i + *sink;
+
+       if (depth > 0)
+               frame[depth & 31] += burn_stack(depth - 1, sink);
+
+       for (i = 0; i < ARRAY_SIZE(frame); i++)
+               *sink += frame[i];
+
+       keep_alive(frame);
+       return *sink;
+}
+
+static struct perf_event_attr sampling_attr(unsigned long freq)
+{
+       struct perf_event_attr attr = {
+               .type           = PERF_TYPE_HARDWARE,
+               .size           = sizeof(attr),
+               .config         = PERF_COUNT_HW_INSTRUCTIONS,
+               .sample_type    = PERF_SAMPLE_STACK_USER,
+               .sample_stack_user = STACK_DUMP_SIZE,
+               .freq           = 1,
+               .sample_freq    = freq,
+               .disabled       = 1,
+               .exclude_kernel = 1,
+               .exclude_hv     = 1,
+       };
+
+       return attr;
+}
+
+static bool open_skip_reason(int err, const char **why)
+{
+       switch (err) {
+       case EACCES:
+       case EPERM:
+               *why = "insufficient privilege for PMU sampling 
(perf_event_paranoid)";
+               return true;
+       case ENOENT:
+       case ENODEV:
+       case EOPNOTSUPP:
+               *why = "hardware PMU sampling event not available";
+               return true;
+       default:
+               return false;
+       }
+}
+
+static bool pmu_sse_route_testable(const char **why)
+{
+       char *line = NULL;
+       size_t line_size = 0;
+       FILE *interrupts;
+       bool testable = true;
+
+       interrupts = fopen("/proc/interrupts", "re");
+       if (!interrupts) {
+               *why = "cannot inspect the active PMU delivery route";
+               return false;
+       }
+
+       /* The SBI PMU driver registers this name only for ordinary IRQ 
delivery. */
+       while (getline(&line, &line_size, interrupts) >= 0) {
+               if (strstr(line, "riscv-pmu")) {
+                       *why = "ordinary RISC-V PMU IRQ delivery is active";
+                       testable = false;
+                       break;
+               }
+       }
+
+       free(line);
+       fclose(interrupts);
+       return testable;
+}
+
+static bool ring_copy_from(void *dst, const void *rb, size_t rb_bytes,
+                          uint64_t pos, size_t size)
+{
+       size_t offset = pos % rb_bytes;
+       size_t first;
+
+       if (size > rb_bytes)
+               return false;
+
+       first = size < rb_bytes - offset ? size : rb_bytes - offset;
+       memcpy(dst, (const char *)rb + offset, first);
+       if (first != size)
+               memcpy((char *)dst + first, rb, size - first);
+
+       return true;
+}
+
+static int truncation_child(void *arg)
+{
+       int ready_fd = (intptr_t)arg;
+       char ready = 1;
+
+       if (write(ready_fd, &ready, sizeof(ready)) != 1)
+               return 1;
+
+       for (;;)
+               __asm__ __volatile__("" : : : "memory");
+}
+
+static pid_t start_truncation_child(void **stack_mapping)
+{
+       struct pollfd pfd = { .events = POLLIN };
+       size_t mapping_size = 2 * page_size;
+       char ready;
+       void *stack;
+       pid_t pid;
+       int pipefd[2];
+       int saved_errno;
+
+       stack = mmap(NULL, mapping_size, PROT_NONE,
+                    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+       if (stack == MAP_FAILED)
+               return -1;
+       if (mprotect(stack, page_size, PROT_READ | PROT_WRITE))
+               goto err_unmap;
+       if (pipe(pipefd))
+               goto err_unmap;
+
+       /* clone() starts the child below the inaccessible second page. */
+       pid = clone(truncation_child, (char *)stack + page_size, SIGCHLD,
+                   (void *)(intptr_t)pipefd[1]);
+       if (pid < 0)
+               goto err_pipe;
+
+       close(pipefd[1]);
+       pfd.fd = pipefd[0];
+       if (poll(&pfd, 1, 1000) != 1 ||
+           read(pipefd[0], &ready, sizeof(ready)) != sizeof(ready)) {
+               saved_errno = ETIMEDOUT;
+               kill(pid, SIGKILL);
+               waitpid(pid, NULL, 0);
+               close(pipefd[0]);
+               errno = saved_errno;
+               goto err_unmap;
+       }
+       close(pipefd[0]);
+
+       *stack_mapping = stack;
+       return pid;
+
+err_pipe:
+       saved_errno = errno;
+       close(pipefd[0]);
+       close(pipefd[1]);
+       errno = saved_errno;
+err_unmap:
+       saved_errno = errno;
+       munmap(stack, mapping_size);
+       errno = saved_errno;
+       return -1;
+}
+
+static void stop_truncation_child(pid_t pid, void *stack_mapping)
+{
+       kill(pid, SIGKILL);
+       while (waitpid(pid, NULL, 0) < 0 && errno == EINTR)
+               ;
+       munmap(stack_mapping, 2 * page_size);
+}
+
+/*
+ * Subtest 1: sample a child whose stack is followed by an inaccessible page.
+ * Every record must be well formed and at least one stack copy must truncate
+ * at the controlled page boundary rather than fault or overrun.
+ */
+static void test_ustack_records_wellformed(void)
+{
+       struct perf_event_attr attr = sampling_attr(SELF_SAMPLE_FREQ);
+       size_t rb_bytes = (size_t)RB_DATA_PAGES * page_size;
+       struct perf_event_mmap_page *meta;
+       unsigned long samples = 0, truncated = 0;
+       void *child_stack;
+       const char *why;
+       void *rb;
+       pid_t child;
+       int fd;
+
+       child = start_truncation_child(&child_stack);
+       if (child < 0) {
+               ksft_test_result_fail("ustack records: create guarded stack 
child: %s\n",
+                                     strerror(errno));
+               return;
+       }
+
+       fd = perf_event_open(&attr, child, -1, -1, PERF_FLAG_FD_CLOEXEC);
+       if (fd < 0) {
+               if (open_skip_reason(errno, &why))
+                       ksft_test_result_skip("ustack records: %s\n", why);
+               else
+                       ksft_test_result_fail("ustack records: perf_event_open: 
%s\n",
+                                             strerror(errno));
+               goto out_child;
+       }
+
+       meta = mmap(NULL, page_size + rb_bytes, PROT_READ | PROT_WRITE,
+                   MAP_SHARED, fd, 0);
+       if (meta == MAP_FAILED) {
+               ksft_test_result_fail("ustack records: mmap ring buffer: %s\n",
+                                     strerror(errno));
+               close(fd);
+               goto out_child;
+       }
+       rb = (char *)meta + page_size;
+
+       ioctl(fd, PERF_EVENT_IOC_RESET, 0);
+       ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);
+       usleep(TRUNCATION_RUN_MS * 1000);
+       ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
+
+       /* Drain the ring buffer and validate every SAMPLE record. */
+       {
+               uint64_t head = __atomic_load_n(&meta->data_head, 
__ATOMIC_ACQUIRE);
+               uint64_t tail = meta->data_tail;
+               bool ok = true;
+
+               if (head < tail || head - tail > rb_bytes)
+                       ok = false;
+
+               while (ok && tail < head) {
+                       struct perf_event_header hdr;
+                       uint64_t available = head - tail;
+
+                       if (available < sizeof(hdr) ||
+                           !ring_copy_from(&hdr, rb, rb_bytes, tail, 
sizeof(hdr)) ||
+                           hdr.size < sizeof(hdr) || hdr.size > available ||
+                           hdr.size > rb_bytes) {
+                               ok = false;
+                               break;
+                       }
+
+                       if (hdr.type == PERF_RECORD_SAMPLE) {
+                               uint64_t dump_size, dyn_size;
+                               size_t cursor = sizeof(hdr);
+
+                               if (sizeof(dump_size) > hdr.size - cursor ||
+                                   !ring_copy_from(&dump_size, rb, rb_bytes,
+                                                   tail + cursor, 
sizeof(dump_size)) ||
+                                   dump_size > STACK_DUMP_SIZE) {
+                                       ok = false;
+                                       break;
+                               }
+                               cursor += sizeof(dump_size);
+                               samples++;
+                               if (dump_size) {
+                                       /* data blob then trailing dynamic size 
*/
+                                       if (dump_size > hdr.size - cursor) {
+                                               ok = false;
+                                               break;
+                                       }
+                                       cursor += dump_size;
+                                       if (sizeof(dyn_size) > hdr.size - 
cursor ||
+                                           !ring_copy_from(&dyn_size, rb, 
rb_bytes,
+                                                           tail + cursor,
+                                                           sizeof(dyn_size))) {
+                                               ok = false;
+                                               break;
+                                       }
+                                       if (dyn_size > dump_size) {
+                                               ok = false;
+                                               break;
+                                       }
+                                       if (dyn_size < dump_size)
+                                               truncated++;
+                               }
+                       }
+                       tail += hdr.size;
+               }
+               __atomic_store_n(&meta->data_tail, head, __ATOMIC_RELEASE);
+
+               if (!ok)
+                       ksft_test_result_fail("ustack records: malformed sample 
record\n");
+               else if (samples == 0)
+                       ksft_test_result_skip("ustack records: no samples 
collected\n");
+               else if (truncated == 0)
+                       ksft_test_result_fail("ustack records: no guarded-stack 
truncation\n");
+               else
+                       ksft_test_result_pass("ustack records: %lu samples, %lu 
truncated\n",
+                                             samples, truncated);
+       }
+
+       munmap(meta, page_size + rb_bytes);
+       close(fd);
+out_child:
+       stop_truncation_child(child, child_stack);
+}
+
+/* ---- Subtest 2: multi-threaded per-CPU sampling stress ---- */
+
+struct stress_thread {
+       pthread_t tid;
+       int cpu;
+       int *stop;
+       int fd;
+       void *rb;
+       size_t rb_bytes;
+};
+
+static void *stress_worker(void *arg)
+{
+       struct stress_thread *st = arg;
+       unsigned long sink = 1;
+       int sv[2];
+       char buf[64];
+
+       if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0) {
+               while (!__atomic_load_n(st->stop, __ATOMIC_RELAXED)) {
+                       /* unix-socket ping-pong: takes the socket locks the
+                        * original bug corrupted, while sampling nests.
+                        */
+                       if (write(sv[0], buf, sizeof(buf)) > 0)
+                               (void)read(sv[1], buf, sizeof(buf));
+                       burn_stack(RECURSE_DEPTH, &sink);
+                       /* Periodically consume the ring buffer so sampling
+                        * keeps delivering rather than filling up and stopping.
+                        */
+                       if (st->rb) {
+                               struct perf_event_mmap_page *m = st->rb;
+                               uint64_t h = __atomic_load_n(&m->data_head,
+                                                            __ATOMIC_ACQUIRE);
+                               __atomic_store_n(&m->data_tail, h,
+                                                __ATOMIC_RELEASE);
+                       }
+               }
+               close(sv[0]);
+               close(sv[1]);
+       }
+
+       return (void *)sink;
+}
+
+static void test_sse_stress_no_crash(void)
+{
+       struct perf_event_attr attr = sampling_attr(STRESS_SAMPLE_FREQ);
+       size_t rb_bytes = (size_t)RB_DATA_PAGES * page_size;
+       struct stress_thread *threads;
+       cpu_set_t available;
+       long progressed = 0;
+       int stop = 0;
+       const char *why = NULL;
+       long started = 0;
+       long nproc;
+       long slot;
+       int cpu;
+
+       if (sched_getaffinity(0, sizeof(available), &available)) {
+               ksft_test_result_fail("sse stress: sched_getaffinity: %s\n",
+                                     strerror(errno));
+               return;
+       }
+       nproc = CPU_COUNT(&available);
+       if (nproc < 1) {
+               ksft_test_result_skip("sse stress: no available CPUs\n");
+               return;
+       }
+
+       threads = calloc(nproc, sizeof(*threads));
+       if (!threads) {
+               ksft_test_result_fail("sse stress: out of memory\n");
+               return;
+       }
+
+       slot = 0;
+       for (cpu = 0; cpu < CPU_SETSIZE; cpu++) {
+               struct stress_thread *st;
+               pthread_attr_t thread_attr;
+               cpu_set_t set;
+               void *map;
+               int ret;
+
+               if (!CPU_ISSET(cpu, &available))
+                       continue;
+               st = &threads[slot++];
+
+               st->fd = -1;
+               st->cpu = cpu;
+               st->stop = &stop;
+               st->rb_bytes = rb_bytes;
+               st->fd = perf_event_open(&attr, -1, cpu, -1,
+                                        PERF_FLAG_FD_CLOEXEC);
+               if (st->fd < 0) {
+                       if (!started && open_skip_reason(errno, &why))
+                               break;
+                       continue;
+               }
+
+               map = mmap(NULL, page_size + rb_bytes, PROT_READ | PROT_WRITE,
+                          MAP_SHARED, st->fd, 0);
+               if (map == MAP_FAILED) {
+                       close(st->fd);
+                       st->fd = -1;
+                       continue;
+               }
+               st->rb = map;
+
+               CPU_ZERO(&set);
+               CPU_SET(cpu, &set);
+               pthread_attr_init(&thread_attr);
+               ret = pthread_attr_setaffinity_np(&thread_attr, sizeof(set), 
&set);
+               if (!ret)
+                       ret = pthread_create(&st->tid, &thread_attr,
+                                            stress_worker, st);
+               pthread_attr_destroy(&thread_attr);
+
+               if (ret) {
+                       munmap(st->rb, page_size + rb_bytes);
+                       close(st->fd);
+                       st->rb = NULL;
+                       st->fd = -1;
+                       continue;
+               }
+
+               ioctl(st->fd, PERF_EVENT_IOC_RESET, 0);
+               ioctl(st->fd, PERF_EVENT_IOC_ENABLE, 0);
+               started++;
+       }
+
+       if (started == 0) {
+               free(threads);
+               if (why)
+                       ksft_test_result_skip("sse stress: %s\n", why);
+               else
+                       ksft_test_result_skip("sse stress: could not start any 
sampler\n");
+               return;
+       }
+
+       sleep(STRESS_SECONDS);
+       __atomic_store_n(&stop, 1, __ATOMIC_RELAXED);
+
+       for (slot = 0; slot < nproc; slot++) {
+               struct stress_thread *st = &threads[slot];
+               struct perf_event_mmap_page *meta;
+
+               if (st->fd < 0)
+                       continue;
+               pthread_join(st->tid, NULL);
+               ioctl(st->fd, PERF_EVENT_IOC_DISABLE, 0);
+               meta = st->rb;
+               if (__atomic_load_n(&meta->data_head, __ATOMIC_ACQUIRE))
+                       progressed++;
+               munmap(st->rb, page_size + rb_bytes);
+               close(st->fd);
+       }
+
+       free(threads);
+       if (progressed != started)
+               ksft_test_result_fail("sse stress: %ld/%ld samplers made 
progress\n",
+                                     progressed, started);
+       else
+               ksft_test_result_pass("sse stress: %ld samplers x %ds made 
progress\n",
+                                     started, STRESS_SECONDS);
+}
+
+int main(void)
+{
+       const char *why;
+
+       page_size = sysconf(_SC_PAGESIZE);
+
+       ksft_print_header();
+       ksft_set_plan(2);
+       if (!pmu_sse_route_testable(&why)) {
+               ksft_test_result_skip("ustack records: %s\n", why);
+               ksft_test_result_skip("sse stress: %s\n", why);
+               ksft_finished();
+       }
+
+       test_ustack_records_wellformed();
+       test_sse_stress_no_crash();
+
+       ksft_finished();
+}
-- 
2.50.1 (Apple Git-155)


Reply via email to