]> git.ipfire.org Git - thirdparty/kernel/stable.git/commitdiff
selftests/perf_events: Add a mmap() correctness test
authorLorenzo Stoakes <lorenzo.stoakes@oracle.com>
Sat, 2 Aug 2025 20:55:35 +0000 (22:55 +0200)
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Fri, 15 Aug 2025 10:05:11 +0000 (12:05 +0200)
commit 084d2ac4030c5919e85bba1f4af26e33491469cb upstream.

Exercise various mmap(), munmap() and mremap() invocations, which might
cause a perf buffer mapping to be split or truncated.

To avoid hard coding the perf event and having dependencies on
architectures and configuration options, scan through event types in sysfs
and try to open them. On success, try to mmap() and if that succeeds try to
mmap() the AUX buffer.

In case that no AUX buffer supporting event is found, only test the base
buffer mapping. If no mappable event is found or permissions are not
sufficient, skip the tests.

Reserve a PROT_NONE region for both rb and aux tests to allow testing the
case where mremap unmaps beyond the end of a mapped VMA to prevent it from
unmapping unrelated mappings.

Signed-off-by: Lorenzo Stoakes <lorenzo.stoakes@oracle.com>
Co-developed-by: Thomas Gleixner <tglx@linutronix.de>
Signed-off-by: Thomas Gleixner <tglx@linutronix.de>
Reviewed-by: Lorenzo Stoakes <lorenzo.stoakes@oracle.com>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
tools/testing/selftests/perf_events/.gitignore
tools/testing/selftests/perf_events/Makefile
tools/testing/selftests/perf_events/mmap.c [new file with mode: 0644]

index 790c47001e77e3d1e66494681a09922f84d4c754..4858977dd55b5ff3cc1b032c353ede1cfcaa1072 100644 (file)
@@ -1,3 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 sigtrap_threads
 remove_on_exec
+mmap
index db93c4ff081a45d52ae50023a9be18be6b4ca298..913854914ae499a7006ba777594d03fe490d9cdb 100644 (file)
@@ -2,5 +2,5 @@
 CFLAGS += -Wl,-no-as-needed -Wall $(KHDR_INCLUDES)
 LDFLAGS += -lpthread
 
-TEST_GEN_PROGS := sigtrap_threads remove_on_exec
+TEST_GEN_PROGS := sigtrap_threads remove_on_exec mmap
 include ../lib.mk
diff --git a/tools/testing/selftests/perf_events/mmap.c b/tools/testing/selftests/perf_events/mmap.c
new file mode 100644 (file)
index 0000000..ea0427a
--- /dev/null
@@ -0,0 +1,236 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#define _GNU_SOURCE
+
+#include <dirent.h>
+#include <sched.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+
+#include <linux/perf_event.h>
+
+#include "../kselftest_harness.h"
+
+#define RB_SIZE                0x3000
+#define AUX_SIZE       0x10000
+#define AUX_OFFS       0x4000
+
+#define HOLE_SIZE      0x1000
+
+/* Reserve space for rb, aux with space for shrink-beyond-vma testing. */
+#define REGION_SIZE    (2 * RB_SIZE + 2 * AUX_SIZE)
+#define REGION_AUX_OFFS (2 * RB_SIZE)
+
+#define MAP_BASE       1
+#define MAP_AUX                2
+
+#define EVENT_SRC_DIR  "/sys/bus/event_source/devices"
+
+FIXTURE(perf_mmap)
+{
+       int             fd;
+       void            *ptr;
+       void            *region;
+};
+
+FIXTURE_VARIANT(perf_mmap)
+{
+       bool            aux;
+       unsigned long   ptr_size;
+};
+
+FIXTURE_VARIANT_ADD(perf_mmap, rb)
+{
+       .aux = false,
+       .ptr_size = RB_SIZE,
+};
+
+FIXTURE_VARIANT_ADD(perf_mmap, aux)
+{
+       .aux = true,
+       .ptr_size = AUX_SIZE,
+};
+
+static bool read_event_type(struct dirent *dent, __u32 *type)
+{
+       char typefn[512];
+       FILE *fp;
+       int res;
+
+       snprintf(typefn, sizeof(typefn), "%s/%s/type", EVENT_SRC_DIR, dent->d_name);
+       fp = fopen(typefn, "r");
+       if (!fp)
+               return false;
+
+       res = fscanf(fp, "%u", type);
+       fclose(fp);
+       return res > 0;
+}
+
+FIXTURE_SETUP(perf_mmap)
+{
+       struct perf_event_attr attr = {
+               .size           = sizeof(attr),
+               .disabled       = 1,
+               .exclude_kernel = 1,
+               .exclude_hv     = 1,
+       };
+       struct perf_event_attr attr_ok = {};
+       unsigned int eacces = 0, map = 0;
+       struct perf_event_mmap_page *rb;
+       struct dirent *dent;
+       void *aux, *region;
+       DIR *dir;
+
+       self->ptr = NULL;
+
+       dir = opendir(EVENT_SRC_DIR);
+       if (!dir)
+               SKIP(return, "perf not available.");
+
+       region = mmap(NULL, REGION_SIZE, PROT_NONE, MAP_ANON | MAP_PRIVATE, -1, 0);
+       ASSERT_NE(region, MAP_FAILED);
+       self->region = region;
+
+       // Try to find a suitable event on this system
+       while ((dent = readdir(dir))) {
+               int fd;
+
+               if (!read_event_type(dent, &attr.type))
+                       continue;
+
+               fd = syscall(SYS_perf_event_open, &attr, 0, -1, -1, 0);
+               if (fd < 0) {
+                       if (errno == EACCES)
+                               eacces++;
+                       continue;
+               }
+
+               // Check whether the event supports mmap()
+               rb = mmap(region, RB_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, fd, 0);
+               if (rb == MAP_FAILED) {
+                       close(fd);
+                       continue;
+               }
+
+               if (!map) {
+                       // Save the event in case that no AUX capable event is found
+                       attr_ok = attr;
+                       map = MAP_BASE;
+               }
+
+               if (!variant->aux)
+                       continue;
+
+               rb->aux_offset = AUX_OFFS;
+               rb->aux_size = AUX_SIZE;
+
+               // Check whether it supports a AUX buffer
+               aux = mmap(region + REGION_AUX_OFFS, AUX_SIZE, PROT_READ | PROT_WRITE,
+                          MAP_SHARED | MAP_FIXED, fd, AUX_OFFS);
+               if (aux == MAP_FAILED) {
+                       munmap(rb, RB_SIZE);
+                       close(fd);
+                       continue;
+               }
+
+               attr_ok = attr;
+               map = MAP_AUX;
+               munmap(aux, AUX_SIZE);
+               munmap(rb, RB_SIZE);
+               close(fd);
+               break;
+       }
+       closedir(dir);
+
+       if (!map) {
+               if (!eacces)
+                       SKIP(return, "No mappable perf event found.");
+               else
+                       SKIP(return, "No permissions for perf_event_open()");
+       }
+
+       self->fd = syscall(SYS_perf_event_open, &attr_ok, 0, -1, -1, 0);
+       ASSERT_NE(self->fd, -1);
+
+       rb = mmap(region, RB_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, self->fd, 0);
+       ASSERT_NE(rb, MAP_FAILED);
+
+       if (!variant->aux) {
+               self->ptr = rb;
+               return;
+       }
+
+       if (map != MAP_AUX)
+               SKIP(return, "No AUX event found.");
+
+       rb->aux_offset = AUX_OFFS;
+       rb->aux_size = AUX_SIZE;
+       aux = mmap(region + REGION_AUX_OFFS, AUX_SIZE, PROT_READ | PROT_WRITE,
+                  MAP_SHARED | MAP_FIXED, self->fd, AUX_OFFS);
+       ASSERT_NE(aux, MAP_FAILED);
+       self->ptr = aux;
+}
+
+FIXTURE_TEARDOWN(perf_mmap)
+{
+       ASSERT_EQ(munmap(self->region, REGION_SIZE), 0);
+       if (self->fd != -1)
+               ASSERT_EQ(close(self->fd), 0);
+}
+
+TEST_F(perf_mmap, remap)
+{
+       void *tmp, *ptr = self->ptr;
+       unsigned long size = variant->ptr_size;
+
+       // Test the invalid remaps
+       ASSERT_EQ(mremap(ptr, size, HOLE_SIZE, MREMAP_MAYMOVE), MAP_FAILED);
+       ASSERT_EQ(mremap(ptr + HOLE_SIZE, size, HOLE_SIZE, MREMAP_MAYMOVE), MAP_FAILED);
+       ASSERT_EQ(mremap(ptr + size - HOLE_SIZE, HOLE_SIZE, size, MREMAP_MAYMOVE), MAP_FAILED);
+       // Shrink the end of the mapping such that we only unmap past end of the VMA,
+       // which should succeed and poke a hole into the PROT_NONE region
+       ASSERT_NE(mremap(ptr + size - HOLE_SIZE, size, HOLE_SIZE, MREMAP_MAYMOVE), MAP_FAILED);
+
+       // Remap the whole buffer to a new address
+       tmp = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
+       ASSERT_NE(tmp, MAP_FAILED);
+
+       // Try splitting offset 1 hole size into VMA, this should fail
+       ASSERT_EQ(mremap(ptr + HOLE_SIZE, size - HOLE_SIZE, size - HOLE_SIZE,
+                        MREMAP_MAYMOVE | MREMAP_FIXED, tmp), MAP_FAILED);
+       // Remapping the whole thing should succeed fine
+       ptr = mremap(ptr, size, size, MREMAP_MAYMOVE | MREMAP_FIXED, tmp);
+       ASSERT_EQ(ptr, tmp);
+       ASSERT_EQ(munmap(tmp, size), 0);
+}
+
+TEST_F(perf_mmap, unmap)
+{
+       unsigned long size = variant->ptr_size;
+
+       // Try to poke holes into the mappings
+       ASSERT_NE(munmap(self->ptr, HOLE_SIZE), 0);
+       ASSERT_NE(munmap(self->ptr + HOLE_SIZE, HOLE_SIZE), 0);
+       ASSERT_NE(munmap(self->ptr + size - HOLE_SIZE, HOLE_SIZE), 0);
+}
+
+TEST_F(perf_mmap, map)
+{
+       unsigned long size = variant->ptr_size;
+
+       // Try to poke holes into the mappings by mapping anonymous memory over it
+       ASSERT_EQ(mmap(self->ptr, HOLE_SIZE, PROT_READ | PROT_WRITE,
+                      MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0), MAP_FAILED);
+       ASSERT_EQ(mmap(self->ptr + HOLE_SIZE, HOLE_SIZE, PROT_READ | PROT_WRITE,
+                      MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0), MAP_FAILED);
+       ASSERT_EQ(mmap(self->ptr + size - HOLE_SIZE, HOLE_SIZE, PROT_READ | PROT_WRITE,
+                      MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0), MAP_FAILED);
+}
+
+TEST_HARNESS_MAIN