From: Fredrik Markstrom <fredrik.markstrom@est.tech>
To: Catalin Marinas <catalin.marinas@arm.com>,
Will Deacon <will@kernel.org>, Shuah Khan <shuah@kernel.org>,
Peter Zijlstra <peterz@infradead.org>,
Ingo Molnar <mingo@redhat.com>,
Arnaldo Carvalho de Melo <acme@kernel.org>,
Namhyung Kim <namhyung@kernel.org>,
Mark Rutland <mark.rutland@arm.com>,
Alexander Shishkin <alexander.shishkin@linux.intel.com>,
Jiri Olsa <jolsa@kernel.org>, Ian Rogers <irogers@google.com>,
Adrian Hunter <adrian.hunter@intel.com>,
James Clark <james.clark@linaro.org>,
Santosh Shilimkar <santosh.shilimkar@ti.com>,
Olof Johansson <olof@lixom.net>,
Tony Lindgren <tony@atomide.com>
Cc: linux-arm-kernel@lists.infradead.org,
linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org,
linux-perf-users@vger.kernel.org,
Nicolas Pitre <nico@fluxnic.net>,
Fredrik Markstrom <fredrik.markstrom@est.tech>,
Ivar Holmqvist <ivar.holmqvist@est.tech>,
Malin Jonsson <malin.jonsson@est.tech>
Subject: [PATCH 3/3] DO NOT MERGE: selftests: perf_events: Add device memory callchain unwinding test
Date: Tue, 28 Apr 2026 22:49:00 +0200 [thread overview]
Message-ID: <20260428-master-with-pfix-v3-v1-3-c384d3e53092@est.tech> (raw)
In-Reply-To: <20260428-master-with-pfix-v3-v1-0-c384d3e53092@est.tech>
The device memory callchain guard introduced earlier in this series
needs a regression test to ensure future refactoring does not
silently reintroduce the vulnerability where perf follows frame
pointers into device memory.
Add a kselftest that exercises the exact attack vector: a process
mmaps /dev/mem (creating a device memory mapping), points its frame
pointer into it, and is sampled by perf with frame-pointer
callchains. The test passes if both the process and kernel survive.
The default MMIO address is 0xc0000000; override via the MMIO_ADDR
environment variable if that is unsuitable. The address must be a
physical address that is not backed by any responding device, so
that an access produces a synchronous external abort rather than
returning data. On QEMU's virt machine and many modern arm64
platforms, 0xc0000000 falls in an unused region of the MMIO
address space and works for this purpose.
Since the test only sets the frame pointer to the address (never
reads it directly), the only reads come from the perf unwinder —
which the guard blocks.
The /dev/mem mmap must happen in the child process after fork.
fork() does not copy PTEs for VM_PFNMAP regions, so mapping before
fork leaves the child with empty page tables — the unwinder gets a
translation fault (caught by extable) instead of a synchronous
external abort.
arm64-only; skipped on other architectures.
Assisted-by: Kiro:claude-opus-4.6 [kiro-cli]
Signed-off-by: Fredrik Markstrom <fredrik.markstrom@est.tech>
Reviewed-by: Ivar Holmqvist <ivar.holmqvist@est.tech>
Reviewed-by: Malin Jonsson <malin.jonsson@est.tech>
---
MAINTAINERS | 1 +
tools/testing/selftests/perf_events/Makefile | 2 +-
.../testing/selftests/perf_events/test_perf_vmio.c | 114 +++++++++++++++++++++
3 files changed, 116 insertions(+), 1 deletion(-)
diff --git a/MAINTAINERS b/MAINTAINERS
index 2fb1c75afd16388f590a77c04e08d2d6d002f5cc..5416f80c4aac28a5f1d780c76bb23110283dcdc3 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -20881,6 +20881,7 @@ F: include/uapi/linux/perf_event.h
F: kernel/events/*
F: tools/lib/perf/
F: tools/perf/
+F: tools/testing/selftests/perf_events/
PERFORMANCE EVENTS TOOLING ARM64
R: John Garry <john.g.garry@oracle.com>
diff --git a/tools/testing/selftests/perf_events/Makefile b/tools/testing/selftests/perf_events/Makefile
index 2e5d85770dfeadd909196dbf980fd334b9580477..a432e24d9e493f77951092571989249703d22351 100644
--- a/tools/testing/selftests/perf_events/Makefile
+++ b/tools/testing/selftests/perf_events/Makefile
@@ -2,5 +2,5 @@
CFLAGS += -Wl,-no-as-needed -Wall $(KHDR_INCLUDES)
LDFLAGS += -lpthread
-TEST_GEN_PROGS := sigtrap_threads remove_on_exec watermark_signal mmap
+TEST_GEN_PROGS := sigtrap_threads remove_on_exec watermark_signal mmap test_perf_vmio
include ../lib.mk
diff --git a/tools/testing/selftests/perf_events/test_perf_vmio.c b/tools/testing/selftests/perf_events/test_perf_vmio.c
new file mode 100644
index 0000000000000000000000000000000000000000..780c5800dd6bd3b7a9d3813b490d4621da876da3
--- /dev/null
+++ b/tools/testing/selftests/perf_events/test_perf_vmio.c
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Device memory perf callchain unwinding test (arm64 only).
+ *
+ * Maps a physical address via /dev/mem (creating a device memory mapping),
+ * launches perf record to sample this process with frame-pointer
+ * callchains, then points FP (x29) into the mapping and spins.
+ * The test passes if the kernel survives without crashing.
+ *
+ * The default MMIO address is 0xc0000000; override via environment:
+ * MMIO_ADDR=0x10000000 ./test_perf_vmio
+ */
+#include <fcntl.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/mman.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+
+#define DEFAULT_MMIO_ADDR 0xc0000000UL
+
+TEST(device_memory_callchain)
+{
+#ifndef __aarch64__
+ SKIP(return, "arm64 only");
+#else
+ unsigned long pa = DEFAULT_MMIO_ADDR;
+ unsigned long page, off;
+ pid_t spin_pid, perf_pid;
+ char pid_str[16];
+ int fd, pst;
+ void *m, *fp;
+ char *env;
+
+ if (getuid() != 0)
+ SKIP(return, "need root");
+
+ env = getenv("MMIO_ADDR");
+ if (env)
+ pa = strtoul(env, NULL, 16);
+
+ page = pa & ~0xFFFUL;
+ off = pa - page;
+
+ fd = open("/dev/mem", O_RDWR | O_SYNC);
+ if (fd < 0)
+ SKIP(return, "cannot open /dev/mem");
+
+ /* Fork a spinner child with FP pointing into device memory */
+ spin_pid = fork();
+ ASSERT_GE(spin_pid, 0);
+ if (spin_pid == 0) {
+ /*
+ * mmap /dev/mem in the child so remap_pfn_range populates
+ * PTEs directly. fork() does not copy PTEs for VM_PFNMAP
+ * regions, so mapping before fork leaves the child with
+ * empty page tables — the unwinder would get a translation
+ * fault instead of a synchronous external abort.
+ */
+ m = mmap(NULL, off + 4096, PROT_READ | PROT_WRITE,
+ MAP_SHARED, fd, page);
+ if (m == MAP_FAILED)
+ _exit(1);
+ fp = (char *)m + off;
+ __asm__ volatile(
+ "mov x29, %0\n"
+ "1: b 1b\n"
+ : : "r"(fp) : "x29", "memory");
+ _exit(0);
+ }
+
+ /* Launch perf to sample the spinner */
+ snprintf(pid_str, sizeof(pid_str), "%d", spin_pid);
+
+ perf_pid = fork();
+ if (perf_pid < 0) {
+ kill(spin_pid, SIGKILL);
+ waitpid(spin_pid, NULL, 0);
+ close(fd);
+ ASSERT_GE(perf_pid, 0);
+ }
+ if (perf_pid == 0) {
+ char *const perf_argv[] = {
+ "perf", "record", "-g", "--call-graph", "fp",
+ "-p", pid_str, "--", "sleep", "3", NULL
+ };
+
+ if (chdir("/tmp"))
+ _exit(1);
+ execvp(perf_argv[0], perf_argv);
+ _exit(1);
+ }
+
+ waitpid(perf_pid, &pst, 0);
+
+ kill(spin_pid, SIGKILL);
+ waitpid(spin_pid, NULL, 0);
+ close(fd);
+
+ if (WIFEXITED(pst) && WEXITSTATUS(pst) == 1)
+ SKIP(return, "perf not available");
+
+ /*
+ * The real test is that the kernel survived. If we got here
+ * without a synchronous external abort, the guard worked.
+ */
+ TH_LOG("kernel survived perf sampling with FP in device memory");
+#endif /* __aarch64__ */
+}
+
+TEST_HARNESS_MAIN
--
2.51.0
prev parent reply other threads:[~2026-04-28 20:49 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-28 20:48 [PATCH 0/3] arm64: perf: Skip device memory during user callchain unwinding Fredrik Markstrom
2026-04-28 20:48 ` [PATCH 1/3] " Fredrik Markstrom
2026-04-28 20:48 ` [PATCH 2/3] DO NOT MERGE: arm64: perf: Add skip_vmio parameter to control device memory callchain guard Fredrik Markstrom
2026-04-28 20:49 ` Fredrik Markstrom [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260428-master-with-pfix-v3-v1-3-c384d3e53092@est.tech \
--to=fredrik.markstrom@est.tech \
--cc=acme@kernel.org \
--cc=adrian.hunter@intel.com \
--cc=alexander.shishkin@linux.intel.com \
--cc=catalin.marinas@arm.com \
--cc=irogers@google.com \
--cc=ivar.holmqvist@est.tech \
--cc=james.clark@linaro.org \
--cc=jolsa@kernel.org \
--cc=linux-arm-kernel@lists.infradead.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=linux-perf-users@vger.kernel.org \
--cc=malin.jonsson@est.tech \
--cc=mark.rutland@arm.com \
--cc=mingo@redhat.com \
--cc=namhyung@kernel.org \
--cc=nico@fluxnic.net \
--cc=olof@lixom.net \
--cc=peterz@infradead.org \
--cc=santosh.shilimkar@ti.com \
--cc=shuah@kernel.org \
--cc=tony@atomide.com \
--cc=will@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox