* [PATCH 3/5] perf/core: add KUnit tests for AUX kernel-consumer API
2026-10-04 17:34 [PATCH 0/5] perf/core: add AUX buffer kernel-consumer API Kunwu Chan
2026-10-04 17:34 ` [PATCH 1/5] perf/core: add AUX buffer ownership for kernel events Kunwu Chan
2026-10-04 17:34 ` [PATCH 2/5] perf/core: add AUX ring accessors for kernel consumers Kunwu Chan
@ 2026-10-04 17:34 ` Kunwu Chan
2026-10-04 17:34 ` [PATCH 4/5] selftests/perf_events: add userspace AUX regression test Kunwu Chan
2026-10-04 17:34 ` [PATCH 5/5] selftests/perf_events: add AUX kernel API selftest script Kunwu Chan
4 siblings, 0 replies; 6+ messages in thread
From: Kunwu Chan @ 2026-10-04 17:34 UTC (permalink / raw)
To: peterz, mingo, acme, namhyung
Cc: sj, corbet, skhan, rdunlap, mark.rutland, alexander.shishkin,
jolsa, irogers, adrian.hunter, james.clark, akpm, lianux.mm,
kunwu.chan, linux-doc, linux-kernel, linux-perf-users,
linux-kselftest
From: Lian Wang <lianux.mm@gmail.com>
Add a KUnit test suite (perf_aux_kernel) for the perf_event_setup_aux(),
perf_event_release_aux(), perf_event_aux_head(), perf_event_aux_tail_set()
and perf_event_aux_copy() functions.
A self-contained dummy AUX PMU is registered in suite_init so that
every test is deterministic and needs no hardware. The dummy
setup_aux pre-fills all pages with a recognisable pattern (0xAB);
aux_test_produce() advances the published head via
perf_aux_output_begin/end, and copy tests verify the pattern.
39 test cases cover setup/release lifecycle, writer admission and
blocking, head/tail/copy accessor contract validation (including
wrap-around, boundary cursors, and data correctness), buffer-full
behaviour, negative watermark rejection, independence of two
events on the same PMU, user/kernel coexistence simulation, and
concurrent release on the same event.
Every test event is released through a kunit_add_action() cleanup, so
a test aborted by a KUNIT_ASSERT_*() (which skips the remaining
manual cleanup) still releases its events. The concurrent-release
test waits on a struct completion instead of a lockless
wait_event() condition, and does not kthread_stop() workers that exit
on their own. The buffer-full test asserts that perf_aux_output_begin()
refuses a full non-overwrite buffer.
Gated by CONFIG_PERF_AUX_KERNEL_KUNIT_TEST.
Co-developed-by: Kunwu Chan <kunwu.chan@gmail.com>
Signed-off-by: Kunwu Chan <kunwu.chan@gmail.com>
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
kernel/events/Makefile | 1 +
kernel/events/aux_kernel_test.c | 1152 +++++++++++++++++++++++++++++++
lib/Kconfig.debug | 14 +
3 files changed, 1167 insertions(+)
create mode 100644 kernel/events/aux_kernel_test.c
diff --git a/kernel/events/Makefile b/kernel/events/Makefile
index 91a62f566743..592604db2c76 100644
--- a/kernel/events/Makefile
+++ b/kernel/events/Makefile
@@ -3,4 +3,5 @@ obj-y := core.o ring_buffer.o callchain.o
obj-$(CONFIG_HAVE_HW_BREAKPOINT) += hw_breakpoint.o
obj-$(CONFIG_HW_BREAKPOINT_KUNIT_TEST) += hw_breakpoint_test.o
+obj-$(CONFIG_PERF_AUX_KERNEL_KUNIT_TEST) += aux_kernel_test.o
obj-$(CONFIG_UPROBES) += uprobes.o
diff --git a/kernel/events/aux_kernel_test.c b/kernel/events/aux_kernel_test.c
new file mode 100644
index 000000000000..fa481b8015db
--- /dev/null
+++ b/kernel/events/aux_kernel_test.c
@@ -0,0 +1,1152 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for the perf AUX kernel-consumer API.
+ *
+ * Tests the exported functions:
+ * - perf_event_setup_aux() / perf_event_release_aux()
+ * - perf_event_aux_head() / perf_event_aux_tail_set() / perf_event_aux_copy()
+ *
+ * A self-contained dummy AUX PMU is registered in suite_init so that
+ * every test is deterministic and requires no hardware. The dummy
+ * setup_aux pre-fills all pages with a recognisable pattern (0xAB);
+ * aux_test_produce() advances the published head via
+ * perf_aux_output_begin/end, and copy tests verify the pattern.
+ *
+ * Every event created by aux_test_create_event() is released through a
+ * kunit_add_action() cleanup, so a test aborted by a KUNIT_ASSERT_*()
+ * (which skips the remaining manual cleanup) still releases its events.
+ *
+ * Run via debugfs:
+ * echo run > /sys/kernel/debug/kunit/perf_aux_kernel/run
+ * cat /sys/kernel/debug/kunit/perf_aux_kernel/results
+ */
+
+#include <kunit/test.h>
+#include <linux/completion.h>
+#include <linux/kthread.h>
+#include <linux/perf_event.h>
+#include <linux/slab.h>
+#include <linux/smp.h>
+#include <linux/string.h>
+#include "internal.h"
+
+/* ---- Dummy AUX PMU ---- */
+
+static struct pmu dummy_aux_pmu;
+static int dummy_aux_pmu_type = -1;
+
+static int dummy_aux_event_init(struct perf_event *event)
+{
+ return 0;
+}
+
+static void dummy_aux_event_read(struct perf_event *event) { }
+
+static int dummy_aux_event_add(struct perf_event *event, int mode)
+{
+ return 0;
+}
+
+static void dummy_aux_event_del(struct perf_event *event, int mode) { }
+
+static void dummy_aux_event_start(struct perf_event *event, int mode) { }
+
+static void dummy_aux_event_stop(struct perf_event *event, int mode) { }
+
+static void *dummy_aux_setup_aux(struct perf_event *event, void **pages,
+ int nr_pages, bool overwrite)
+{
+ int i;
+
+ for (i = 0; i < nr_pages; i++)
+ memset(pages[i], 0xAB, PAGE_SIZE);
+
+ return pages;
+}
+
+static void dummy_aux_free_aux(void *priv)
+{
+ /* aux_priv is the pages array; __rb_free_aux frees it via kfree. */
+}
+
+static int dummy_aux_pmu_register(void)
+{
+ int ret;
+
+ if (dummy_aux_pmu_type >= 0)
+ return 0;
+
+ memset(&dummy_aux_pmu, 0, sizeof(dummy_aux_pmu));
+ dummy_aux_pmu.event_init = dummy_aux_event_init;
+ dummy_aux_pmu.add = dummy_aux_event_add;
+ dummy_aux_pmu.del = dummy_aux_event_del;
+ dummy_aux_pmu.start = dummy_aux_event_start;
+ dummy_aux_pmu.stop = dummy_aux_event_stop;
+ dummy_aux_pmu.read = dummy_aux_event_read;
+ dummy_aux_pmu.setup_aux = dummy_aux_setup_aux;
+ dummy_aux_pmu.free_aux = dummy_aux_free_aux;
+ dummy_aux_pmu.capabilities = PERF_PMU_CAP_ITRACE;
+ dummy_aux_pmu.task_ctx_nr = perf_sw_context;
+
+ ret = perf_pmu_register(&dummy_aux_pmu, "dummy_aux", -1);
+ if (ret)
+ return ret;
+
+ dummy_aux_pmu_type = dummy_aux_pmu.type;
+ return 0;
+}
+
+static void dummy_aux_pmu_unregister(void)
+{
+ if (dummy_aux_pmu_type >= 0)
+ perf_pmu_unregister(&dummy_aux_pmu);
+ dummy_aux_pmu_type = -1;
+}
+
+static int suite_init(struct kunit_suite *suite)
+{
+ return dummy_aux_pmu_register();
+}
+
+static void suite_exit(struct kunit_suite *suite)
+{
+ dummy_aux_pmu_unregister();
+}
+
+/* ---- Helpers ---- */
+
+/*
+ * Deferred cleanup for the kernel events created by the tests.
+ *
+ * Registering this via kunit_add_action() makes every test abort-safe:
+ * KUNIT_ASSERT_* macros (and kunit_skip()) terminate the test by
+ * throwing, which skips the remaining manual cleanup, but KUnit still
+ * runs registered actions on the aborted test's exit. A test that
+ * already called perf_event_release_aux() itself is not affected: a
+ * second release is a no-op, and the LIFO ordering of actions handles
+ * tests that create more than one event.
+ */
+static void aux_test_event_release(void *p)
+{
+ struct perf_event *event = p;
+
+ perf_event_release_aux(event);
+ perf_event_release_kernel(event);
+}
+
+static struct perf_event *aux_test_create_event(struct kunit *test)
+{
+ struct perf_event_attr attr = {};
+ struct perf_event *event;
+
+ attr.type = dummy_aux_pmu_type;
+ attr.size = sizeof(attr);
+ attr.disabled = 1;
+
+ event = perf_event_create_kernel_counter(&attr, raw_smp_processor_id(),
+ NULL, NULL, NULL);
+ if (IS_ERR(event))
+ return event;
+
+ if (kunit_add_action_or_reset(test, aux_test_event_release, event)) {
+ perf_event_release_kernel(event);
+ return ERR_PTR(-ENOMEM);
+ }
+
+ return event;
+}
+
+/*
+ * Advance the AUX head by @bytes via perf_aux_output_begin/end.
+ * The pages are pre-filled with 0xAB in dummy_aux_setup_aux, so
+ * the data is already present; this function only publishes it.
+ */
+static unsigned long aux_test_produce(struct perf_event *event, unsigned long bytes)
+{
+ struct perf_output_handle handle;
+ unsigned long head, written = 0;
+
+ while (written < bytes) {
+ unsigned long chunk = min_t(unsigned long, bytes - written, 1024);
+
+ if (!perf_aux_output_begin(&handle, event))
+ break;
+ chunk = min_t(unsigned long, chunk, handle.size);
+ perf_aux_output_end(&handle, chunk);
+ written += chunk;
+ }
+
+ head = perf_event_aux_head(event);
+ return head;
+}
+
+/* ---- Patch-1: Setup / release tests ---- */
+
+static void test_setup_rejects_non_power_of_two(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 3, 0);
+ KUNIT_EXPECT_EQ(test, -EINVAL, ret);
+
+ ret = perf_event_setup_aux(event, 0, 0);
+ KUNIT_EXPECT_EQ(test, -EINVAL, ret);
+
+ ret = perf_event_setup_aux(event, 6, 0);
+ KUNIT_EXPECT_EQ(test, -EINVAL, ret);
+}
+
+static void test_setup_accepts_power_of_two(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ int i, ret;
+ int pages[] = { 1, 2, 4, 8, 16, 32, 64 };
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ for (i = 0; i < ARRAY_SIZE(pages); i++) {
+ ret = perf_event_setup_aux(event, pages[i], 0);
+ KUNIT_EXPECT_EQ_MSG(test, 0, ret,
+ "setup_aux(%d pages) expected 0, got %d",
+ pages[i], ret);
+ if (ret == 0)
+ perf_event_release_aux(event);
+ }
+}
+
+static void test_setup_rejects_double(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, -EBUSY, ret);
+
+ perf_event_release_aux(event);
+}
+
+static void test_setup_parent_event_rejected(struct kunit *test)
+{
+ struct perf_event *parent = aux_test_create_event(test);
+ struct perf_event *child = aux_test_create_event(test);
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, parent);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, child);
+
+ child->parent = parent;
+
+ ret = perf_event_setup_aux(child, 1, 0);
+ KUNIT_EXPECT_EQ(test, -EINVAL, ret);
+
+ /* Restore before the deferred cleanup releases the events. */
+ child->parent = NULL;
+}
+
+static void test_writer_admitted_after_setup(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ unsigned long head;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ head = aux_test_produce(event, 512);
+ KUNIT_EXPECT_GT(test, head, 0);
+
+ perf_event_release_aux(event);
+}
+
+static void test_writer_blocked_after_release(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ struct perf_output_handle handle;
+ void *addr;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ perf_event_release_aux(event);
+
+ addr = perf_aux_output_begin(&handle, event);
+ KUNIT_EXPECT_NULL(test, addr);
+}
+
+static void test_release_noop_without_rb(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ perf_event_release_aux(event);
+ KUNIT_EXPECT_TRUE(test, true);
+}
+
+static void test_setup_release_roundtrip(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ perf_event_release_aux(event);
+ KUNIT_EXPECT_TRUE(test, true);
+}
+
+static void test_multi_setup_release_cycle(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ int i, ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ for (i = 0; i < 3; i++) {
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_ASSERT_EQ_MSG(test, 0, ret,
+ "cycle %d: setup_aux failed", i);
+ perf_event_release_aux(event);
+ }
+}
+
+static void test_double_release_clean(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ perf_event_release_aux(event);
+ perf_event_release_aux(event);
+ KUNIT_EXPECT_TRUE(test, true);
+}
+
+/* ---- Patch-2: Accessor tests ---- */
+
+static void test_head_initial_zero(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ unsigned long head;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ head = perf_event_aux_head(event);
+ KUNIT_EXPECT_EQ(test, 0, head);
+
+ perf_event_release_aux(event);
+}
+
+static void test_head_advances_after_produce(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ unsigned long head;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ head = aux_test_produce(event, 512);
+ KUNIT_EXPECT_GT(test, head, 0);
+
+ perf_event_release_aux(event);
+}
+
+static void test_copy_full_window(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ const unsigned long buf_size = PAGE_SIZE;
+ void *buf;
+ unsigned long head;
+ long copied;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ head = aux_test_produce(event, buf_size / 2);
+ KUNIT_EXPECT_GT(test, head, 0);
+
+ buf = kunit_kmalloc(test, head, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buf);
+
+ copied = perf_event_aux_copy(event, 0, head, buf);
+ KUNIT_EXPECT_EQ_MSG(test, (long)head, copied,
+ "copy(0, %lu) returned %ld, expected %lu",
+ head, copied, head);
+
+ perf_event_release_aux(event);
+}
+
+static void test_copy_wrap(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ const unsigned long buf_size = 2 * PAGE_SIZE;
+ void *buf;
+ unsigned long head, tail;
+ long copied;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 2, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ /* Fill the buffer to near-full */
+ head = aux_test_produce(event, buf_size);
+ KUNIT_EXPECT_GT(test, head, 0);
+
+ /* Consume half to free space */
+ tail = head / 2;
+ ret = perf_event_aux_tail_set(event, tail);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ /* Produce more — head now wraps past buf_size */
+ head = aux_test_produce(event, buf_size);
+ KUNIT_EXPECT_GT(test, head, buf_size);
+
+ /* Copy a window that spans the ring wrap point */
+ buf = kunit_kmalloc(test, buf_size, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buf);
+
+ copied = perf_event_aux_copy(event, tail, head, buf);
+ KUNIT_EXPECT_GT(test, copied, 0);
+ KUNIT_EXPECT_LE(test, copied, (long)buf_size);
+
+ perf_event_release_aux(event);
+}
+
+static void test_copy_zero_len(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ void *buf;
+ long copied;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ buf = kunit_kmalloc(test, 1, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buf);
+
+ copied = perf_event_aux_copy(event, 0, 0, buf);
+ KUNIT_EXPECT_EQ(test, 0, copied);
+
+ perf_event_release_aux(event);
+}
+
+static void test_copy_rejects_rewound(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ const unsigned long buf_size = PAGE_SIZE;
+ void *buf;
+ long copied;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ aux_test_produce(event, buf_size / 2);
+
+ buf = kunit_kmalloc(test, 64, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buf);
+
+ /* Reversed cursor range. */
+ copied = perf_event_aux_copy(event, 100, 50, buf);
+ KUNIT_EXPECT_EQ(test, -EINVAL, copied);
+
+ perf_event_release_aux(event);
+}
+
+static void test_copy_rejects_future(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ unsigned long head;
+ void *buf;
+ long copied;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ head = aux_test_produce(event, 512);
+ KUNIT_EXPECT_GT(test, head, 0);
+
+ buf = kunit_kmalloc(test, 64, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buf);
+
+ /* to > head: future cursor */
+ copied = perf_event_aux_copy(event, head, head + 64, buf);
+ KUNIT_EXPECT_EQ(test, -EINVAL, copied);
+
+ perf_event_release_aux(event);
+}
+
+static void test_copy_rejects_oversized(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ const unsigned long buf_size = PAGE_SIZE;
+ void *buf;
+ long copied;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ aux_test_produce(event, 512);
+
+ buf = kunit_kmalloc(test, PAGE_SIZE, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buf);
+
+ /* Window extends past the published producer cursor. */
+ copied = perf_event_aux_copy(event, 0, 2 * buf_size, buf);
+ KUNIT_EXPECT_EQ(test, -EINVAL, copied);
+
+ perf_event_release_aux(event);
+}
+
+static void test_copy_rejects_null_buf(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ long copied;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ copied = perf_event_aux_copy(event, 0, 0, NULL);
+ KUNIT_EXPECT_EQ(test, -EINVAL, copied);
+
+ perf_event_release_aux(event);
+}
+
+static void test_copy_no_rb_enoent(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ void *buf;
+ long ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ buf = kunit_kmalloc(test, 64, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buf);
+
+ ret = perf_event_aux_copy(event, 0, 64, buf);
+ KUNIT_EXPECT_EQ(test, -ENOENT, ret);
+}
+
+static void test_tail_set_valid_frees_space(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ unsigned long head;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ head = aux_test_produce(event, 512);
+ KUNIT_EXPECT_GT(test, head, 0);
+
+ ret = perf_event_aux_tail_set(event, head / 2);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ perf_event_release_aux(event);
+}
+
+static void test_tail_set_rejects_future(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ /* tail=1 when head=0: future */
+ ret = perf_event_aux_tail_set(event, 1);
+ KUNIT_EXPECT_EQ(test, -EINVAL, ret);
+
+ perf_event_release_aux(event);
+}
+
+static void test_tail_set_accepts_within_ring_window(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ const unsigned long buf_size = PAGE_SIZE;
+ unsigned long head, tail;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ /* Produce enough data so head - tail <= aux_size for a large tail */
+ head = aux_test_produce(event, buf_size / 2);
+ KUNIT_EXPECT_GT(test, head, 0);
+
+ /*
+ * Set tail within the current [old_tail, head] window.
+ */
+ tail = head / 4;
+ ret = perf_event_aux_tail_set(event, tail);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ perf_event_release_aux(event);
+}
+
+static void test_tail_set_rejects_behind_ring(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ const unsigned long buf_size = PAGE_SIZE;
+ unsigned long head;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ head = aux_test_produce(event, buf_size / 2);
+ KUNIT_EXPECT_GT(test, head, 0);
+
+ /*
+ * Cursor outside the current producer window. The requested
+ * tail is not between head - aux_size and head, so the
+ * modular-distance check must reject it.
+ */
+ ret = perf_event_aux_tail_set(event, head + buf_size + 1);
+ KUNIT_EXPECT_EQ(test, -EINVAL, ret);
+
+ perf_event_release_aux(event);
+}
+
+static void test_tail_set_no_rb_enoent(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_aux_tail_set(event, 0);
+ KUNIT_EXPECT_EQ(test, -ENOENT, ret);
+}
+
+static void test_head_no_rb_zero(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ unsigned long head;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ head = perf_event_aux_head(event);
+ KUNIT_EXPECT_EQ(test, 0, head);
+}
+
+/* ---- Additional boundary and correctness tests ---- */
+
+static void test_setup_rejects_negative_watermark(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, -1);
+ KUNIT_EXPECT_EQ(test, -EINVAL, ret);
+
+ ret = perf_event_setup_aux(event, 1, -4096);
+ KUNIT_EXPECT_EQ(test, -EINVAL, ret);
+}
+
+static void test_tail_set_noop(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ ret = perf_event_aux_tail_set(event, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ perf_event_release_aux(event);
+}
+
+static void test_tail_set_consume_all(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ unsigned long head;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ head = aux_test_produce(event, 512);
+ KUNIT_EXPECT_GT(test, head, 0);
+
+ ret = perf_event_aux_tail_set(event, head);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ perf_event_release_aux(event);
+}
+
+static void test_copy_data_correctness(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ void *buf;
+ unsigned long head;
+ long copied;
+ int ret, i;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ head = aux_test_produce(event, 512);
+ KUNIT_EXPECT_GT(test, head, 0);
+
+ buf = kunit_kmalloc(test, head, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buf);
+
+ copied = perf_event_aux_copy(event, 0, head, buf);
+ KUNIT_EXPECT_EQ(test, (long)head, copied);
+
+ for (i = 0; i < copied; i++) {
+ KUNIT_EXPECT_EQ_MSG(test, 0xAB, ((u8 *)buf)[i],
+ "data mismatch at offset %d", i);
+ if (((u8 *)buf)[i] != 0xAB)
+ break;
+ }
+
+ perf_event_release_aux(event);
+}
+
+static void test_multi_produce_copy_cycle(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ void *buf;
+ unsigned long head, tail = 0;
+ long copied;
+ int ret, cycle;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ buf = kunit_kmalloc(test, PAGE_SIZE, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buf);
+
+ for (cycle = 0; cycle < 3; cycle++) {
+ head = aux_test_produce(event, 256);
+ KUNIT_EXPECT_GT(test, head, tail);
+
+ copied = perf_event_aux_copy(event, tail, head, buf);
+ KUNIT_EXPECT_EQ(test, (long)(head - tail), copied);
+
+ ret = perf_event_aux_tail_set(event, head);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+ tail = head;
+ }
+
+ perf_event_release_aux(event);
+}
+
+static void test_copy_rejects_already_consumed(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ void *buf;
+ unsigned long head, tail;
+ long copied;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ head = aux_test_produce(event, 512);
+ KUNIT_EXPECT_GT(test, head, 0);
+
+ tail = head / 2;
+ ret = perf_event_aux_tail_set(event, tail);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ buf = kunit_kmalloc(test, 64, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buf);
+
+ copied = perf_event_aux_copy(event, tail - 1, tail, buf);
+ KUNIT_EXPECT_EQ(test, -EINVAL, copied);
+
+ perf_event_release_aux(event);
+}
+
+static void test_output_end_zero_size(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ struct perf_output_handle handle;
+ void *addr;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ addr = perf_aux_output_begin(&handle, event);
+ KUNIT_ASSERT_NOT_NULL(test, addr);
+
+ perf_aux_output_end(&handle, 0);
+ KUNIT_EXPECT_EQ(test, 0, perf_event_aux_head(event));
+
+ perf_event_release_aux(event);
+}
+
+static void test_buffer_full_stops_producer(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ struct perf_output_handle handle;
+ const unsigned long buf_size = PAGE_SIZE;
+ unsigned long head;
+ void *addr;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ head = aux_test_produce(event, buf_size);
+ KUNIT_EXPECT_GT(test, head, 0);
+
+ addr = perf_aux_output_begin(&handle, event);
+ /*
+ * The buffer is full and no consumer has freed space, so a new
+ * non-overwrite transaction must be refused outright, not handed a
+ * zero-size window.
+ */
+ KUNIT_EXPECT_NULL_MSG(test, addr,
+ "begin() must refuse a full non-overwrite buffer");
+ if (addr) {
+ /* Unexpected: keep the AUX refcount balanced. */
+ KUNIT_EXPECT_EQ(test, 0, handle.size);
+ perf_aux_output_end(&handle, 0);
+ }
+
+ perf_event_release_aux(event);
+}
+
+static void test_setup_with_explicit_watermark(struct kunit *test)
+{
+ struct perf_event *event = aux_test_create_event(test);
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ ret = perf_event_setup_aux(event, 2, 4096);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+ perf_event_release_aux(event);
+
+ ret = perf_event_setup_aux(event, 4, 8192);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+ perf_event_release_aux(event);
+}
+
+static void test_two_events_independent(struct kunit *test)
+{
+ struct perf_event *ev_a = aux_test_create_event(test);
+ struct perf_event *ev_b = aux_test_create_event(test);
+ unsigned long head_a, head_b;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ev_a);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ev_b);
+
+ ret = perf_event_setup_aux(ev_a, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ ret = perf_event_setup_aux(ev_b, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ head_a = aux_test_produce(ev_a, 512);
+ head_b = perf_event_aux_head(ev_b);
+
+ KUNIT_EXPECT_GT(test, head_a, 0);
+ KUNIT_EXPECT_EQ(test, 0, head_b);
+
+ head_b = aux_test_produce(ev_b, 256);
+ KUNIT_EXPECT_GT(test, head_b, 0);
+ KUNIT_EXPECT_EQ(test, head_a, perf_event_aux_head(ev_a));
+
+ perf_event_release_aux(ev_a);
+ perf_event_release_aux(ev_b);
+}
+
+static void test_release_one_does_not_affect_other(struct kunit *test)
+{
+ struct perf_event *ev_a = aux_test_create_event(test);
+ struct perf_event *ev_b = aux_test_create_event(test);
+ unsigned long head_b;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ev_a);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ev_b);
+
+ ret = perf_event_setup_aux(ev_a, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ ret = perf_event_setup_aux(ev_b, 1, 0);
+ KUNIT_EXPECT_EQ(test, 0, ret);
+
+ head_b = aux_test_produce(ev_b, 512);
+ KUNIT_EXPECT_GT(test, head_b, 0);
+
+ perf_event_release_aux(ev_a);
+
+ KUNIT_EXPECT_EQ(test, head_b, perf_event_aux_head(ev_b));
+ KUNIT_EXPECT_GT(test, aux_test_produce(ev_b, 256), head_b);
+
+ perf_event_release_aux(ev_b);
+}
+
+/*
+ * Test: kernel consumer and simulated userspace consumer coexist on
+ * the same PMU without interfering.
+ *
+ * Event A uses aux_kernel_count (via perf_event_setup_aux).
+ * Event B swaps its refcounts to aux_mmap_count=1 to simulate a
+ * userspace mmap'd consumer. Both rings on the same dummy PMU.
+ *
+ * - produce on A → B's head is unaffected
+ * - produce on B → A's head is unaffected
+ * - release A (kernel) → B still produces
+ */
+static void test_user_kernel_coexistence(struct kunit *test)
+{
+ struct perf_event *ev_a = aux_test_create_event(test);
+ struct perf_event *ev_b = aux_test_create_event(test);
+ struct perf_output_handle handle_a, handle_b;
+ unsigned long a_head_before, b_head_before;
+ void *addr;
+ int ret;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ev_a);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ev_b);
+
+ /* Event A: kernel consumer */
+ ret = perf_event_setup_aux(ev_a, 1, 0);
+ KUNIT_ASSERT_EQ(test, 0, ret);
+
+ /* Event B: start with kernel setup, then simulate userspace.
+ * ASSERT: the refcount_set() below dereferences ev_b->rb.
+ */
+ ret = perf_event_setup_aux(ev_b, 1, 0);
+ KUNIT_ASSERT_EQ(test, 0, ret);
+
+ /* Simulate userspace ownership: swap refcounts on B's rb.
+ * perf_mmap() would set aux_mmap_count=1 for a userspace event.
+ */
+ refcount_set(&ev_b->rb->aux_mmap_count, 1);
+ refcount_set(&ev_b->rb->aux_kernel_count, 0);
+
+ /* Produce on A — B's head stays 0 */
+ addr = perf_aux_output_begin(&handle_a, ev_a);
+ KUNIT_ASSERT_NOT_NULL(test, addr);
+ perf_aux_output_end(&handle_a, 512);
+ KUNIT_EXPECT_GT(test, ev_a->rb->aux_head, 0);
+ KUNIT_EXPECT_EQ(test, 0, ev_b->rb->aux_head);
+
+ /* Produce on B — A's head unchanged */
+ a_head_before = ev_a->rb->aux_head;
+ addr = perf_aux_output_begin(&handle_b, ev_b);
+ KUNIT_ASSERT_NOT_NULL(test, addr);
+ perf_aux_output_end(&handle_b, 256);
+ KUNIT_EXPECT_GT(test, ev_b->rb->aux_head, 0);
+ KUNIT_EXPECT_EQ(test, a_head_before, ev_a->rb->aux_head);
+
+ /* Release A (kernel) — B still produces */
+ b_head_before = ev_b->rb->aux_head;
+ perf_event_release_aux(ev_a);
+ addr = perf_aux_output_begin(&handle_b, ev_b);
+ KUNIT_ASSERT_NOT_NULL(test, addr);
+ perf_aux_output_end(&handle_b, 128);
+ KUNIT_EXPECT_GT(test, ev_b->rb->aux_head, b_head_before);
+
+ /* Cleanup B: restore kernel ownership so the deferred release
+ * path (and the explicit one below) tears the buffer down.
+ */
+ refcount_set(&ev_b->rb->aux_kernel_count, 1);
+ refcount_set(&ev_b->rb->aux_mmap_count, 0);
+ perf_event_release_aux(ev_b);
+}
+
+/*
+ * Test: two kthreads concurrently call perf_event_release_aux() on the
+ * same event. perf_event_release_aux() serialises the whole teardown on
+ * event->mmap_mutex, so exactly one caller drops aux_kernel_count and
+ * performs the teardown while the other observes event->rb == NULL and
+ * returns as a no-op. The test verifies the race is handled without a
+ * refcount underflow warning, crash, or leak.
+ */
+struct concurrent_release_ctx {
+ struct perf_event *event;
+ struct completion done;
+};
+
+static int concurrent_release_thread(void *data)
+{
+ struct concurrent_release_ctx *ctx = data;
+
+ perf_event_release_aux(ctx->event);
+ complete(&ctx->done);
+
+ return 0;
+}
+
+static void test_concurrent_release(struct kunit *test)
+{
+ struct concurrent_release_ctx ctx;
+ struct task_struct *threads[2];
+ struct perf_event *event;
+ int created = 0, i;
+
+ event = aux_test_create_event(test);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, event);
+
+ KUNIT_ASSERT_EQ(test, 0, perf_event_setup_aux(event, 1, 0));
+
+ ctx.event = event;
+ init_completion(&ctx.done);
+
+ for (i = 0; i < ARRAY_SIZE(threads); i++) {
+ threads[i] = kthread_run(concurrent_release_thread, &ctx,
+ "aux_crel_%d", i);
+ if (IS_ERR(threads[i])) {
+ threads[i] = NULL;
+ break;
+ }
+ created++;
+ }
+
+ /*
+ * Wait for every thread that was actually created before returning:
+ * ctx lives on this stack, and a struct completion is the right
+ * wait/notify primitive for an on-stack waiter (unlike a lockless
+ * wait_event() condition, which the worker could satisfy via
+ * atomic_dec_and_test() before it gets to wake_up()).
+ *
+ * No kthread_stop() here: the workers do not check
+ * kthread_should_stop() and exit on their own after complete(), and
+ * stopping an already-exited kthread would touch freed memory.
+ */
+ for (i = 0; i < created; i++)
+ wait_for_completion(&ctx.done);
+
+ if (created != ARRAY_SIZE(threads))
+ kunit_warn(test, "only %d/%zu release threads created\n",
+ created, ARRAY_SIZE(threads));
+
+ /*
+ * If no worker could run, tear the buffer down here; if one did,
+ * this is the documented no-op second release. The deferred
+ * cleanup action releases the event itself.
+ */
+ perf_event_release_aux(event);
+}
+
+static struct kunit_case aux_kernel_test_cases[] = {
+ KUNIT_CASE(test_setup_rejects_non_power_of_two),
+ KUNIT_CASE(test_setup_accepts_power_of_two),
+ KUNIT_CASE(test_setup_rejects_double),
+ KUNIT_CASE(test_setup_parent_event_rejected),
+ KUNIT_CASE(test_setup_rejects_negative_watermark),
+ KUNIT_CASE(test_setup_with_explicit_watermark),
+ KUNIT_CASE(test_writer_admitted_after_setup),
+ KUNIT_CASE(test_writer_blocked_after_release),
+ KUNIT_CASE(test_release_noop_without_rb),
+ KUNIT_CASE(test_setup_release_roundtrip),
+ KUNIT_CASE(test_multi_setup_release_cycle),
+ KUNIT_CASE(test_double_release_clean),
+ KUNIT_CASE(test_two_events_independent),
+ KUNIT_CASE(test_release_one_does_not_affect_other),
+ KUNIT_CASE(test_user_kernel_coexistence),
+ KUNIT_CASE(test_concurrent_release),
+ KUNIT_CASE(test_head_initial_zero),
+ KUNIT_CASE(test_head_advances_after_produce),
+ KUNIT_CASE(test_head_no_rb_zero),
+ KUNIT_CASE(test_copy_full_window),
+ KUNIT_CASE(test_copy_wrap),
+ KUNIT_CASE(test_copy_zero_len),
+ KUNIT_CASE(test_copy_data_correctness),
+ KUNIT_CASE(test_copy_rejects_rewound),
+ KUNIT_CASE(test_copy_rejects_future),
+ KUNIT_CASE(test_copy_rejects_oversized),
+ KUNIT_CASE(test_copy_rejects_null_buf),
+ KUNIT_CASE(test_copy_rejects_already_consumed),
+ KUNIT_CASE(test_copy_no_rb_enoent),
+ KUNIT_CASE(test_tail_set_valid_frees_space),
+ KUNIT_CASE(test_tail_set_rejects_future),
+ KUNIT_CASE(test_tail_set_accepts_within_ring_window),
+ KUNIT_CASE(test_tail_set_rejects_behind_ring),
+ KUNIT_CASE(test_tail_set_noop),
+ KUNIT_CASE(test_tail_set_consume_all),
+ KUNIT_CASE(test_tail_set_no_rb_enoent),
+ KUNIT_CASE(test_multi_produce_copy_cycle),
+ KUNIT_CASE(test_output_end_zero_size),
+ KUNIT_CASE(test_buffer_full_stops_producer),
+ {},
+};
+
+static struct kunit_suite aux_kernel_test_suite = {
+ .name = "perf_aux_kernel",
+ .suite_init = suite_init,
+ .suite_exit = suite_exit,
+ .test_cases = aux_kernel_test_cases,
+};
+
+kunit_test_suite(aux_kernel_test_suite);
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 134b15a44625..7db7eb07c2f2 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -3073,6 +3073,20 @@ config HW_BREAKPOINT_KUNIT_TEST
If unsure, say N.
+config PERF_AUX_KERNEL_KUNIT_TEST
+ bool "Test perf AUX kernel-consumer API" if !KUNIT_ALL_TESTS
+ depends on PERF_EVENTS=y
+ depends on KUNIT=y
+ default KUNIT_ALL_TESTS
+ help
+ Tests for the perf AUX kernel-consumer API functions:
+ perf_event_setup_aux(), perf_event_release_aux(),
+ perf_event_aux_head(), perf_event_aux_tail_set(), and
+ perf_event_aux_copy(). Uses a dummy AUX PMU and does
+ not require AUX-capable hardware.
+
+ If unsure, say N.
+
source "lib/crypto/tests/Kconfig"
config SIPHASH_KUNIT_TEST
--
2.43.0
^ permalink raw reply [flat|nested] 6+ messages in thread* [PATCH 4/5] selftests/perf_events: add userspace AUX regression test
2026-10-04 17:34 [PATCH 0/5] perf/core: add AUX buffer kernel-consumer API Kunwu Chan
` (2 preceding siblings ...)
2026-10-04 17:34 ` [PATCH 3/5] perf/core: add KUnit tests for AUX kernel-consumer API Kunwu Chan
@ 2026-10-04 17:34 ` Kunwu Chan
2026-10-04 17:34 ` [PATCH 5/5] selftests/perf_events: add AUX kernel API selftest script Kunwu Chan
4 siblings, 0 replies; 6+ messages in thread
From: Kunwu Chan @ 2026-10-04 17:34 UTC (permalink / raw)
To: peterz, mingo, acme, namhyung
Cc: sj, corbet, skhan, rdunlap, mark.rutland, alexander.shishkin,
jolsa, irogers, adrian.hunter, james.clark, akpm, lianux.mm,
kunwu.chan, linux-doc, linux-kernel, linux-perf-users,
linux-kselftest
From: Lian Wang <lianux.mm@gmail.com>
Add a standalone C program that opens a perf event, mmaps the metadata
and data ring, sets up the AUX area via a second mmap, enables tracing,
reads aux_head from the mmap page, and tears down the mapping. This
verifies the kernel-side aux_kernel_count changes do not break the
existing userspace AUX mmap protocol.
Environment-dependent outcomes are reported as SKIP rather than PASS:
an AUX area mmap failure (PMU does not support userspace AUX), a PMU
that allows only one open event (e.g. ARM SPE), or a 100 ms window in
which no AUX data was generated are SKIP rather than PASS. The
two-event independence test follows the full dual-mmap AUX
protocol for both
events so both aux_mmap_count holders really exist, and the
mmap-page-fields test checks the kernel-managed fields
(data_offset/data_size) and the read-back of the user-published
aux_offset/aux_size rather than expecting the kernel to set them.
Co-developed-by: Kunwu Chan <kunwu.chan@gmail.com>
Signed-off-by: Kunwu Chan <kunwu.chan@gmail.com>
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
tools/testing/selftests/perf_events/Makefile | 1 +
.../perf_events/aux_kernel_usermode.c | 627 ++++++++++++++++++
tools/testing/selftests/perf_events/config | 2 +
3 files changed, 630 insertions(+)
create mode 100644 tools/testing/selftests/perf_events/aux_kernel_usermode.c
diff --git a/tools/testing/selftests/perf_events/Makefile b/tools/testing/selftests/perf_events/Makefile
index 2e5d85770dfe..fbafbd43f097 100644
--- a/tools/testing/selftests/perf_events/Makefile
+++ b/tools/testing/selftests/perf_events/Makefile
@@ -3,4 +3,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 += aux_kernel_usermode
include ../lib.mk
diff --git a/tools/testing/selftests/perf_events/aux_kernel_usermode.c b/tools/testing/selftests/perf_events/aux_kernel_usermode.c
new file mode 100644
index 000000000000..5c2da4bac074
--- /dev/null
+++ b/tools/testing/selftests/perf_events/aux_kernel_usermode.c
@@ -0,0 +1,627 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Userspace AUX regression test for the perf AUX kernel-consumer API.
+ *
+ * Verifies that the kernel-side AUX changes (aux_kernel_count,
+ * perf_event_setup_aux, etc.) do NOT break the existing userspace
+ * AUX mmap protocol:
+ * - perf_event_open() with AUX still works
+ * - mmap AUX buffer still works
+ * - aux_head / aux_tail from the mmap page are still correct
+ * - aux_mmap_count is independent of aux_kernel_count
+ * - Userspace and kernel AUX owners can coexist
+ *
+ * Build: $(CC) -o aux_kernel_usermode aux_kernel_usermode.c
+ * Usage: sudo ./aux_kernel_usermode [pmu_name]
+ *
+ * Environment-dependent outcomes are reported as SKIP, not PASS: an
+ * unsupported PMU, a PMU that only allows one open event at a time, or
+ * a system configuration that rejects the event means the protocol was
+ * not exercised, so it must not inflate the pass count.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/perf_event.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#define PASS(fmt, ...) printf(" [PASS] " fmt "\n", ##__VA_ARGS__)
+#define FAIL(fmt, ...) do { \
+ printf(" [FAIL] " fmt "\n", ##__VA_ARGS__); \
+ failed = 1; \
+} while (0)
+#define SKIP(fmt, ...) do { \
+ printf(" [SKIP] " fmt "\n", ##__VA_ARGS__); \
+ skipped = 1; \
+} while (0)
+
+static int failed;
+static int skipped;
+
+static long 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);
+}
+
+/*
+ * Find an AUX PMU type by scanning sysfs PMU directories.
+ */
+static int find_aux_pmu_type(void)
+{
+ char path[256], type_path[261];
+ FILE *f;
+ struct stat st;
+ int type;
+
+ /* Try common AUX PMU names first */
+ static const char * const known[] = {
+ "arm_spe_0", "arm_spe_1", "arm_spe_2",
+ "intel_pt", "intel_pt0",
+ NULL
+ };
+
+ for (int i = 0; known[i]; i++) {
+ snprintf(path, sizeof(path),
+ "/sys/bus/event_source/devices/%s", known[i]);
+ if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
+ snprintf(type_path, sizeof(type_path),
+ "%s/type", path);
+ f = fopen(type_path, "r");
+ if (f) {
+ if (fscanf(f, "%d", &type) == 1) {
+ fclose(f);
+ printf(" Found AUX PMU: %s (type=%d)\n",
+ known[i], type);
+ return type;
+ }
+ fclose(f);
+ }
+ }
+ }
+
+ return -1;
+}
+
+/*
+ * Test 1: Basic perf_event_open() with software event.
+ * This verifies the perf core is not broken at all.
+ */
+static int test_basic_sw_event(void)
+{
+ struct perf_event_attr attr = {};
+ int fd;
+
+ attr.type = PERF_TYPE_SOFTWARE;
+ attr.config = PERF_COUNT_SW_CPU_CLOCK;
+ attr.size = sizeof(attr);
+ attr.disabled = 1;
+
+ fd = perf_event_open(&attr, 0, -1, -1, 0);
+ if (fd < 0) {
+ FAIL("basic SW event: perf_event_open failed (%s)",
+ strerror(errno));
+ return 1;
+ }
+
+ if (ioctl(fd, PERF_EVENT_IOC_ENABLE, 0) < 0) {
+ FAIL("basic SW event: enable failed (%s)", strerror(errno));
+ close(fd);
+ return 1;
+ }
+ usleep(10000);
+ if (ioctl(fd, PERF_EVENT_IOC_DISABLE, 0) < 0) {
+ FAIL("basic SW event: disable failed (%s)", strerror(errno));
+ close(fd);
+ return 1;
+ }
+
+ close(fd);
+ PASS("basic SW event: open/enable/disable/close");
+ return 0;
+}
+
+/*
+ * Test 2: Userspace AUX mmap full lifecycle.
+ * Opens an AUX event, mmaps the metadata/data ring, sets up the AUX
+ * area via a second mmap, enables, reads aux_head, disables, unmaps,
+ * closes. This is the critical regression test.
+ *
+ * If the kernel rejects the AUX area mmap, the test is SKIP (not all
+ * PMUs support userspace AUX mmap).
+ */
+static int test_aux_mmap(int pmu_type)
+{
+ struct perf_event_attr attr = {};
+ struct perf_event_mmap_page *mp;
+ void *aux_base;
+ unsigned long aux_size, aux_offset, mmap_size;
+ int fd, ret;
+
+ attr.type = pmu_type;
+ attr.size = sizeof(attr);
+ attr.disabled = 1;
+ attr.sample_period = 256;
+
+ /* ARM SPE requires period mode (freq=0) */
+ attr.freq = 0;
+
+ fd = perf_event_open(&attr, 0, -1, -1, 0);
+ if (fd < 0) {
+ FAIL("AUX mmap: perf_event_open failed (%s)", strerror(errno));
+ return 1;
+ }
+
+ /*
+ * Step 1: mmap the metadata page and data ring at offset 0.
+ * The kernel creates the ring buffer and returns the metadata page.
+ */
+ aux_size = 1UL << 20; /* 1 MiB */
+ mmap_size = aux_size + getpagesize();
+ mp = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE,
+ MAP_SHARED, fd, 0);
+ if (mp == MAP_FAILED) {
+ FAIL("AUX mmap: metadata mmap failed (%s)", strerror(errno));
+ close(fd);
+ return 1;
+ }
+
+ /*
+ * Step 2: set aux_offset and aux_size in the metadata page.
+ * The AUX area starts after the metadata and data pages.
+ * The kernel reads these values when the AUX area is mmap'd.
+ */
+ aux_offset = mmap_size;
+ mp->aux_offset = aux_offset;
+ mp->aux_size = aux_size;
+
+ /*
+ * Step 3: mmap the AUX area at aux_offset.
+ * The kernel reads aux_offset from the metadata page, verifies
+ * the mmap offset matches, and allocates the AUX buffer.
+ */
+ aux_base = mmap(NULL, aux_size, PROT_READ, MAP_SHARED, fd,
+ aux_offset);
+ if (aux_base == MAP_FAILED) {
+ printf(" [SKIP] AUX mmap: AUX area mmap failed (%s)\n",
+ strerror(errno));
+ munmap(mp, mmap_size);
+ close(fd);
+ return 0;
+ }
+ PASS("AUX mmap: metadata mmap OK, AUX area mmap OK");
+
+ /* Enable the event */
+ ret = ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);
+ if (ret < 0) {
+ FAIL("AUX mmap: enable failed (%s)", strerror(errno));
+ munmap(aux_base, aux_size);
+ munmap(mp, mmap_size);
+ close(fd);
+ return 1;
+ }
+
+ /* Run a small workload to generate some AUX data */
+ usleep(100000); /* 100ms */
+
+ /* Disable the event */
+ ret = ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
+ if (ret < 0) {
+ FAIL("AUX mmap: disable failed (%s)", strerror(errno));
+ munmap(aux_base, aux_size);
+ munmap(mp, mmap_size);
+ close(fd);
+ return 1;
+ }
+
+ /* Unmap AUX area */
+ ret = munmap(aux_base, aux_size);
+ if (ret < 0) {
+ FAIL("AUX mmap: AUX munmap failed (%s)", strerror(errno));
+ munmap(mp, mmap_size);
+ close(fd);
+ return 1;
+ }
+
+ /* Unmap metadata */
+ ret = munmap(mp, mmap_size);
+ if (ret < 0) {
+ FAIL("AUX mmap: metadata munmap failed (%s)", strerror(errno));
+ close(fd);
+ return 1;
+ }
+ PASS("AUX mmap: full lifecycle PASS");
+ close(fd);
+ return 0;
+}
+
+/*
+ * Map the data ring (offset 0) of @fd and then, following the AUX
+ * protocol, publish aux_offset/aux_size in the metadata page and map
+ * the AUX area itself. Returns 0 on success, 1 on failure, and -1
+ * when the kernel refused the AUX mmap (caller should SKIP, since not
+ * every PMU supports userspace AUX).
+ */
+static int mmap_event_aux(int fd, unsigned long aux_size,
+ struct perf_event_mmap_page **mpp, void **auxp,
+ unsigned long *mmap_sizep)
+{
+ struct perf_event_mmap_page *mp;
+ void *aux_base;
+ unsigned long mmap_size, aux_offset;
+
+ mmap_size = aux_size + getpagesize();
+ mp = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE,
+ MAP_SHARED, fd, 0);
+ if (mp == MAP_FAILED) {
+ FAIL("mmap of metadata/data ring failed (%s)", strerror(errno));
+ return 1;
+ }
+
+ /* The AUX area follows the metadata and data pages. */
+ aux_offset = mmap_size;
+ mp->aux_offset = aux_offset;
+ mp->aux_size = aux_size;
+
+ aux_base = mmap(NULL, aux_size, PROT_READ, MAP_SHARED, fd, aux_offset);
+ if (aux_base == MAP_FAILED) {
+ munmap(mp, mmap_size);
+ return -1;
+ }
+
+ *mpp = mp;
+ *auxp = aux_base;
+ *mmap_sizep = mmap_size;
+ return 0;
+}
+
+/*
+ * Test 3: Verify that aux_mmap_count is independent of aux_kernel_count.
+ * Open two events for the same AUX PMU and give each its own AUX area
+ * through the full dual-mmap protocol, so both aux_mmap_count holders
+ * exist at the same time. Verifies both events can hold AUX buffers
+ * simultaneously.
+ *
+ * If the PMU allows only one open event (e.g. ARM SPE) the second open
+ * is a SKIP, not a PASS: the coexistence was not exercised.
+ */
+static int test_aux_mmap_independence(int pmu_type)
+{
+ struct perf_event_attr attr = {};
+ struct perf_event_mmap_page *mp1, *mp2;
+ unsigned long aux_size, mmap_size1, mmap_size2;
+ void *aux1, *aux2;
+ int fd1, fd2, ret;
+
+ aux_size = 1UL << 16; /* 64 KiB */
+
+ attr.type = pmu_type;
+ attr.size = sizeof(attr);
+ attr.disabled = 1;
+ attr.sample_period = 256;
+ attr.freq = 0;
+
+ fd1 = perf_event_open(&attr, 0, -1, -1, 0);
+ if (fd1 < 0) {
+ FAIL("AUX independence: first event open failed (%s)",
+ strerror(errno));
+ return 1;
+ }
+
+ ret = mmap_event_aux(fd1, aux_size, &mp1, &aux1, &mmap_size1);
+ if (ret < 0) {
+ SKIP("AUX independence: first AUX area mmap failed (%s)",
+ strerror(errno));
+ close(fd1);
+ return 0;
+ }
+ if (ret > 0) {
+ close(fd1);
+ return 1;
+ }
+ PASS("AUX independence: first event has an AUX buffer");
+
+ /* Open a second event for the same PMU */
+ fd2 = perf_event_open(&attr, 0, -1, -1, 0);
+ if (fd2 < 0) {
+ /* Some PMUs (e.g. ARM SPE) only allow one event at a time */
+ SKIP("AUX independence: second event open (%s)",
+ strerror(errno));
+ munmap(aux1, aux_size);
+ munmap(mp1, mmap_size1);
+ close(fd1);
+ return 0;
+ }
+
+ ret = mmap_event_aux(fd2, aux_size, &mp2, &aux2, &mmap_size2);
+ if (ret < 0) {
+ SKIP("AUX independence: second AUX area mmap failed (%s)",
+ strerror(errno));
+ munmap(aux1, aux_size);
+ munmap(mp1, mmap_size1);
+ close(fd1);
+ close(fd2);
+ return 0;
+ }
+ if (ret > 0) {
+ munmap(aux1, aux_size);
+ munmap(mp1, mmap_size1);
+ close(fd1);
+ close(fd2);
+ return 1;
+ }
+ PASS("AUX independence: second event has an AUX buffer");
+
+ /* Enable both */
+ if (ioctl(fd1, PERF_EVENT_IOC_ENABLE, 0) < 0 ||
+ ioctl(fd2, PERF_EVENT_IOC_ENABLE, 0) < 0) {
+ FAIL("AUX independence: enable failed (%s)", strerror(errno));
+ munmap(aux2, aux_size); munmap(mp2, mmap_size2);
+ munmap(aux1, aux_size); munmap(mp1, mmap_size1);
+ close(fd1); close(fd2);
+ return 1;
+ }
+ usleep(50000);
+ if (ioctl(fd1, PERF_EVENT_IOC_DISABLE, 0) < 0 ||
+ ioctl(fd2, PERF_EVENT_IOC_DISABLE, 0) < 0) {
+ FAIL("AUX independence: disable failed (%s)", strerror(errno));
+ munmap(aux2, aux_size); munmap(mp2, mmap_size2);
+ munmap(aux1, aux_size); munmap(mp1, mmap_size1);
+ close(fd1); close(fd2);
+ return 1;
+ }
+
+ PASS("AUX independence: two events ran simultaneously");
+
+ /* Both metadata pages must remain distinct and readable */
+ PASS("AUX independence: event1 head=%llu event2 head=%llu",
+ (unsigned long long)__atomic_load_n(&mp1->aux_head, __ATOMIC_RELAXED),
+ (unsigned long long)__atomic_load_n(&mp2->aux_head, __ATOMIC_RELAXED));
+
+ munmap(aux1, aux_size);
+ munmap(mp1, mmap_size1);
+ munmap(aux2, aux_size);
+ munmap(mp2, mmap_size2);
+ close(fd1);
+ close(fd2);
+ PASS("AUX independence: cleanup OK");
+ return 0;
+}
+
+/*
+ * Test 4: Verify that the mmap control page fields are correct.
+ *
+ * version, compat_version, data_offset and data_size are managed by
+ * the kernel; aux_offset and aux_size are published by userspace (they
+ * are the request for the AUX mapping), so the test checks the
+ * kernel-managed fields and the read-back of the user-published ones
+ * instead of expecting the kernel to fill aux_offset in.
+ */
+static int test_mmap_page_fields(int pmu_type)
+{
+ struct perf_event_attr attr = {};
+ struct perf_event_mmap_page *mp;
+ unsigned long total_size;
+ int fd;
+
+ total_size = 1UL << 18; /* 256 KiB */
+
+ attr.type = pmu_type;
+ attr.size = sizeof(attr);
+ attr.disabled = 1;
+ attr.sample_period = 256;
+ attr.freq = 0;
+
+ fd = perf_event_open(&attr, 0, -1, -1, 0);
+ if (fd < 0) {
+ FAIL("mmap fields: perf_event_open failed (%s)", strerror(errno));
+ return 1;
+ }
+
+ mp = mmap(NULL, total_size + getpagesize(), PROT_READ | PROT_WRITE,
+ MAP_SHARED, fd, 0);
+ if (mp == MAP_FAILED) {
+ FAIL("mmap fields: mmap failed (%s)", strerror(errno));
+ close(fd);
+ return 1;
+ }
+
+ /* Kernel-managed fields */
+ if (mp->version != 1 && mp->version != 0) {
+ FAIL("mmap fields: version=%u (expected 0 or 1)", mp->version);
+ munmap(mp, total_size + getpagesize());
+ close(fd);
+ return 1;
+ }
+ PASS("mmap fields: version=%u", mp->version);
+
+ if (mp->compat_version != 0) {
+ FAIL("mmap fields: compat_version=%u (expected 0)",
+ mp->compat_version);
+ munmap(mp, total_size + getpagesize());
+ close(fd);
+ return 1;
+ }
+ PASS("mmap fields: compat_version=%u", mp->compat_version);
+
+ if (mp->data_offset == 0 || mp->data_size == 0) {
+ FAIL("mmap fields: data_offset=%llu data_size=%llu (no data ring)",
+ (unsigned long long)mp->data_offset,
+ (unsigned long long)mp->data_size);
+ munmap(mp, total_size + getpagesize());
+ close(fd);
+ return 1;
+ }
+ PASS("mmap fields: data_offset=%llu data_size=%llu",
+ (unsigned long long)mp->data_offset,
+ (unsigned long long)mp->data_size);
+
+ /* User-published AUX request fields must round-trip */
+ mp->aux_offset = total_size + getpagesize();
+ mp->aux_size = 1UL << 16;
+ if (mp->aux_offset != total_size + getpagesize() || mp->aux_size != 1UL << 16) {
+ FAIL("mmap fields: aux_offset/aux_size write not visible");
+ munmap(mp, total_size + getpagesize());
+ close(fd);
+ return 1;
+ }
+ PASS("mmap fields: aux_offset/aux_size writable and readable");
+
+ munmap(mp, total_size + getpagesize());
+ close(fd);
+ PASS("mmap fields: all fields correct");
+ return 0;
+}
+
+/*
+ * Test 5: Verify that aux_head does not go backwards while the event
+ * runs. An unchanged head means the PMU produced no AUX data for this
+ * workload, which is a SKIP (the monotonicity was not exercised), not a
+ * PASS; a head that moved backwards is a FAIL, since a u64 cursor
+ * cannot realistically wrap in 100 ms.
+ */
+static int test_aux_head_monotonic(int pmu_type)
+{
+ struct perf_event_attr attr = {};
+ struct perf_event_mmap_page *mp;
+ unsigned long aux_size, mmap_size, head_before, head_after;
+ void *aux_base;
+ int fd, ret;
+
+ aux_size = 1UL << 16;
+ mmap_size = aux_size + getpagesize();
+
+ attr.type = pmu_type;
+ attr.size = sizeof(attr);
+ attr.disabled = 1;
+ attr.sample_period = 256;
+ attr.freq = 0;
+
+ fd = perf_event_open(&attr, 0, -1, -1, 0);
+ if (fd < 0) {
+ /* Some PMUs may reject the default config */
+ SKIP("AUX head monotonic: PMU rejected config (%s)",
+ strerror(errno));
+ return 0;
+ }
+
+ ret = mmap_event_aux(fd, aux_size, &mp, &aux_base, &mmap_size);
+ if (ret < 0) {
+ SKIP("AUX head monotonic: AUX area mmap failed (%s)",
+ strerror(errno));
+ close(fd);
+ return 0;
+ }
+ if (ret > 0) {
+ close(fd);
+ return 1;
+ }
+
+ ret = ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);
+ if (ret < 0) {
+ SKIP("AUX head monotonic: enable failed (%s)", strerror(errno));
+ munmap(aux_base, aux_size);
+ munmap(mp, mmap_size);
+ close(fd);
+ return 0;
+ }
+
+ /* Read initial head */
+ head_before = __atomic_load_n(&mp->aux_head, __ATOMIC_RELAXED);
+
+ usleep(100000); /* 100ms */
+
+ head_after = __atomic_load_n(&mp->aux_head, __ATOMIC_RELAXED);
+
+ ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
+
+ if (head_after > head_before) {
+ PASS("AUX head monotonic: before=%lu after=%lu (advanced)",
+ head_before, head_after);
+ } else if (head_after == head_before) {
+ SKIP("AUX head monotonic: no AUX data generated in 100ms");
+ } else {
+ FAIL("AUX head monotonic: backwards (before=%lu after=%lu)",
+ head_before, head_after);
+ }
+
+ munmap(aux_base, aux_size);
+ munmap(mp, mmap_size);
+ close(fd);
+ return 0;
+}
+
+int main(void)
+{
+ int pmu_type;
+
+ if (geteuid() != 0) {
+ fprintf(stderr, "Run as root\n");
+ return 4; /* skip */
+ }
+
+ printf("=== Perf AUX Kernel API: Userspace Regression Test ===\n\n");
+
+ /* Test 1: Basic SW event */
+ printf("--- 1. Basic Software Event ---\n");
+ test_basic_sw_event();
+
+ /* Find an AUX-capable PMU */
+ pmu_type = find_aux_pmu_type();
+ if (pmu_type < 0) {
+ printf("--- 2-5. AUX Tests ---\n");
+ printf(" [SKIP] No AUX-capable PMU found\n");
+ } else {
+ /* Test 2: AUX mmap lifecycle */
+ printf("\n--- 2. AUX Mmap Lifecycle ---\n");
+ test_aux_mmap(pmu_type);
+
+ /* Test 3: AUX mmap independence */
+ printf("\n--- 3. AUX Mmap Independence ---\n");
+ test_aux_mmap_independence(pmu_type);
+
+ /* Test 4: mmap page fields */
+ printf("\n--- 4. Mmap Page Fields ---\n");
+ test_mmap_page_fields(pmu_type);
+
+ /* Test 5: AUX head monotonic */
+ printf("\n--- 5. AUX Head Monotonic ---\n");
+ test_aux_head_monotonic(pmu_type);
+ }
+
+ /* Test 6: Verify /proc/sys/kernel/perf_event_paranoid */
+ printf("\n--- 6. Perf Event Paranoid ---\n");
+ {
+ FILE *f = fopen("/proc/sys/kernel/perf_event_paranoid", "r");
+ int paranoid;
+
+ if (f) {
+ if (fscanf(f, "%d", ¶noid) == 1) {
+ printf(" perf_event_paranoid=%d\n", paranoid);
+ if (paranoid > 2)
+ FAIL("perf_event_paranoid=%d may block AUX",
+ paranoid);
+ else
+ PASS("perf_event_paranoid=%d (OK)", paranoid);
+ }
+ fclose(f);
+ } else {
+ printf(" [SKIP] Cannot read perf_event_paranoid\n");
+ }
+ }
+
+ printf("\n=== Result: %s ===\n", failed ? "FAIL" : "PASS");
+ if (skipped)
+ printf("=== Note: some tests were SKIPped (environment)\n");
+ return failed ? 1 : 0;
+}
diff --git a/tools/testing/selftests/perf_events/config b/tools/testing/selftests/perf_events/config
index ba58ff2203e4..2508f331cfe0 100644
--- a/tools/testing/selftests/perf_events/config
+++ b/tools/testing/selftests/perf_events/config
@@ -1 +1,3 @@
CONFIG_PERF_EVENTS=y
+CONFIG_PERF_AUX_KERNEL_KUNIT_TEST=y
+CONFIG_KUNIT_DEBUGFS=y
--
2.43.0
^ permalink raw reply [flat|nested] 6+ messages in thread