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)