[PATCH RFC v3 3/8] selftests/kcov_dataflow: add ioctl interface selftest

0 views
Skip to first unread message

Yunseong Kim

unread,
Sep 2, 2026, 12:30:06 PM (9 days ago) Sep 2
to linux-...@vger.kernel.org, kasa...@googlegroups.com, linu...@kvack.org, linux-...@vger.kernel.org, rust-fo...@vger.kernel.org, ll...@lists.linux.dev, work...@vger.kernel.org, linu...@vger.kernel.org, linux-k...@vger.kernel.org, Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett, Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko, Kees Cook, Nathan Chancellor, Nicolas Schier, Josh Poimboeuf, Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich, Daniel Almeida, Tamir Duberstein, Alexandre Courbot, Onur Özkan, Nick Desaulniers, Bill Wendling, Marco Elver, Nikita Popov, Matt Arsenault, Justin Stitt, Jonathan Corbet, Shuah Khan, Randy Dunlap, Shuah Khan, Yeoreum Yun, Yunseong Kim, Yunseong Kim
Add a kselftest tests for the kcov_dataflow ioctl interface in
user_ioctl/. Nine cases cover the fd lifecycle and error paths without a
custom compiler, so it runs on any kernel that has the device:

- KCOV_DF_INIT_TRACK tests 3 cases:
1. accepted
2. too-small size -> -EINVAL
3. second init -> -EBUSY
- mmap before init -> fails; enable works with or without a prior mmap.
- KCOV_DF_DISABLE without an enabled session -> -EINVAL.
- A second fd trying to enable while this task already
has a session -> -EBUSY.
- After enabling and running a syscall, any records present parse:
the walk starts with a known type (ENTRY/RET, or CMP when
CONFIG_KCOV_ENABLE_COMPARISONS interleaves them), each record has at
least one value word, and the walk ends exactly at area[0] inside the
buffer.

The test SKIPs cleanly when /sys/kernel/debug/kcov_dataflow is absent
(CONFIG_KCOV_DATAFLOW_ARGS not built).

Assisted-by: Claude:claude-opus-4-6 [kiro-chat]
Signed-off-by: Yunseong Kim <yunseo...@est.tech>
---
.../selftests/kcov_dataflow/user_ioctl/Makefile | 5 +
.../selftests/kcov_dataflow/user_ioctl/README.rst | 11 ++
.../kcov_dataflow/user_ioctl/user_ioctl.c | 168 +++++++++++++++++++++
3 files changed, 184 insertions(+)

diff --git a/tools/testing/selftests/kcov_dataflow/user_ioctl/Makefile b/tools/testing/selftests/kcov_dataflow/user_ioctl/Makefile
new file mode 100644
index 000000000000..1cb3d9b41c07
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/user_ioctl/Makefile
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0
+# Standalone build of the ioctl test: make -C tools/testing/selftests/kcov_dataflow/user_ioctl
+TEST_GEN_PROGS := user_ioctl
+CFLAGS += -Wall -O2 $(KHDR_INCLUDES)
+include ../../lib.mk
diff --git a/tools/testing/selftests/kcov_dataflow/user_ioctl/README.rst b/tools/testing/selftests/kcov_dataflow/user_ioctl/README.rst
new file mode 100644
index 000000000000..55072de189d3
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/user_ioctl/README.rst
@@ -0,0 +1,11 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+KCOV-Dataflow Selftests: user_ioctl
+===================================
+
+Automated ioctl interface test (kselftest harness, 9 TAP cases): INIT_TRACK
+argument checking, double init, mmap before init, ENABLE/DISABLE pairing,
+a second fd failing with -EBUSY, and record validity after a syscall::
+
+ make -C tools/testing/selftests TARGETS=kcov_dataflow
+ tools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl
diff --git a/tools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl.c b/tools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl.c
new file mode 100644
index 000000000000..d7b04c368ced
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl.c
@@ -0,0 +1,168 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * kcov_dataflow_test.c - Selftest for /sys/kernel/debug/kcov_dataflow
+ *
+ * Verifies the ioctl interface: open, INIT_TRACK, mmap, ENABLE, DISABLE.
+ * With INSTRUMENT_ALL, also verifies that records are produced for
+ * syscalls executed while recording is active.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <stdint.h>
+#include <string.h>
+#include <errno.h>
+#include <linux/kcov_dataflow.h>
+
+#include "../../kselftest_harness.h"
+
+
+#define BUF_SIZE 65536
+
+#define DF_TYPE_ENTRY KCOV_DF_TYPE_ENTRY
+#define DF_TYPE_RET KCOV_DF_TYPE_RET
+
+FIXTURE(kcov_dataflow) {
+ int fd;
+ uint64_t *buf;
+};
+
+FIXTURE_SETUP(kcov_dataflow)
+{
+ self->fd = open("/sys/kernel/debug/kcov_dataflow", O_RDWR);
+ if (self->fd < 0)
+ SKIP(return, "kcov_dataflow not available (need CONFIG_KCOV_DATAFLOW_ARGS)");
+ self->buf = MAP_FAILED;
+}
+
+FIXTURE_TEARDOWN(kcov_dataflow)
+{
+ if (self->buf != MAP_FAILED)
+ munmap(self->buf, BUF_SIZE * sizeof(uint64_t));
+ if (self->fd >= 0)
+ close(self->fd);
+}
+
+TEST_F(kcov_dataflow, init_track)
+{
+ int ret = ioctl(self->fd, KCOV_DF_INIT_TRACK, (unsigned long)BUF_SIZE);
+
+ ASSERT_EQ(0, ret);
+}
+
+TEST_F(kcov_dataflow, init_track_too_small)
+{
+ int ret = ioctl(self->fd, KCOV_DF_INIT_TRACK, 1UL);
+
+ ASSERT_EQ(-1, ret);
+ ASSERT_EQ(EINVAL, errno);
+}
+
+TEST_F(kcov_dataflow, init_track_double)
+{
+ ASSERT_EQ(0, ioctl(self->fd, KCOV_DF_INIT_TRACK, (unsigned long)BUF_SIZE));
+ ASSERT_EQ(-1, ioctl(self->fd, KCOV_DF_INIT_TRACK, (unsigned long)BUF_SIZE));
+ ASSERT_EQ(EBUSY, errno);
+}
+
+TEST_F(kcov_dataflow, mmap_before_init)
+{
+ self->buf = mmap(NULL, BUF_SIZE * sizeof(uint64_t),
+ PROT_READ | PROT_WRITE, MAP_SHARED, self->fd, 0);
+ ASSERT_EQ(MAP_FAILED, self->buf);
+}
+
+TEST_F(kcov_dataflow, enable_disable)
+{
+ ASSERT_EQ(0, ioctl(self->fd, KCOV_DF_INIT_TRACK, (unsigned long)BUF_SIZE));
+ self->buf = mmap(NULL, BUF_SIZE * sizeof(uint64_t),
+ PROT_READ | PROT_WRITE, MAP_SHARED, self->fd, 0);
+ ASSERT_NE(MAP_FAILED, self->buf);
+ ASSERT_EQ(0, ioctl(self->fd, KCOV_DF_ENABLE, 0));
+ ASSERT_EQ(0, ioctl(self->fd, KCOV_DF_DISABLE, 0));
+}
+
+TEST_F(kcov_dataflow, enable_without_mmap)
+{
+ ASSERT_EQ(0, ioctl(self->fd, KCOV_DF_INIT_TRACK, (unsigned long)BUF_SIZE));
+ /* enable works even without mmap (mmap is optional for setup) */
+ ASSERT_EQ(0, ioctl(self->fd, KCOV_DF_ENABLE, 0));
+ ASSERT_EQ(0, ioctl(self->fd, KCOV_DF_DISABLE, 0));
+}
+
+TEST_F(kcov_dataflow, disable_without_enable)
+{
+ ASSERT_EQ(0, ioctl(self->fd, KCOV_DF_INIT_TRACK, (unsigned long)BUF_SIZE));
+ ASSERT_EQ(-1, ioctl(self->fd, KCOV_DF_DISABLE, 0));
+ ASSERT_EQ(EINVAL, errno);
+}
+
+TEST_F(kcov_dataflow, double_enable)
+{
+ int fd2;
+
+ ASSERT_EQ(0, ioctl(self->fd, KCOV_DF_INIT_TRACK, (unsigned long)BUF_SIZE));
+ self->buf = mmap(NULL, BUF_SIZE * sizeof(uint64_t),
+ PROT_READ | PROT_WRITE, MAP_SHARED, self->fd, 0);
+ ASSERT_NE(MAP_FAILED, self->buf);
+ ASSERT_EQ(0, ioctl(self->fd, KCOV_DF_ENABLE, 0));
+
+ /* Second fd should fail to enable (task already active) */
+ fd2 = open("/sys/kernel/debug/kcov_dataflow", O_RDWR);
+ ASSERT_GE(fd2, 0);
+ ASSERT_EQ(0, ioctl(fd2, KCOV_DF_INIT_TRACK, (unsigned long)BUF_SIZE));
+ ASSERT_EQ(-1, ioctl(fd2, KCOV_DF_ENABLE, 0));
+ ASSERT_EQ(EBUSY, errno);
+ close(fd2);
+
+ ASSERT_EQ(0, ioctl(self->fd, KCOV_DF_DISABLE, 0));
+}
+
+TEST_F(kcov_dataflow, records_captured)
+{
+ uint64_t count;
+
+ ASSERT_EQ(0, ioctl(self->fd, KCOV_DF_INIT_TRACK, (unsigned long)BUF_SIZE));
+ self->buf = mmap(NULL, BUF_SIZE * sizeof(uint64_t),
+ PROT_READ | PROT_WRITE, MAP_SHARED, self->fd, 0);
+ ASSERT_NE(MAP_FAILED, self->buf);
+ ASSERT_EQ(0, ioctl(self->fd, KCOV_DF_ENABLE, 0));
+
+ /* Trigger some kernel code in this task */
+ getpid();
+
+ ASSERT_EQ(0, ioctl(self->fd, KCOV_DF_DISABLE, 0));
+
+ count = self->buf[0];
+ /*
+ * With INSTRUMENT_ALL, getpid() produces records; without it count may
+ * be 0. Whatever was written must parse: known types (CMP records are
+ * interleaved with CONFIG_KCOV_ENABLE_COMPARISONS=y), at least one value
+ * word each, and a walk that ends exactly at area[0] inside the buffer.
+ */
+ ASSERT_LE(count, (uint64_t)BUF_SIZE - 1);
+ if (count > 0) {
+ uint64_t pos = 1, end = 1 + count;
+ unsigned int nargs = 0;
+
+ while (pos + KCOV_DF_RECORD_HDR_WORDS <= end) {
+ uint64_t hdr = self->buf[pos];
+ unsigned int type = KCOV_DF_HDR_TYPE(hdr);
+ unsigned int nvals = KCOV_DF_HDR_NVALS(hdr);
+
+ ASSERT_TRUE(type == DF_TYPE_ENTRY || type == DF_TYPE_RET ||
+ type == KCOV_DF_TYPE_CMP);
+ ASSERT_GE(nvals, 1);
+ if (type != KCOV_DF_TYPE_CMP)
+ nargs++;
+ pos += KCOV_DF_RECORD_WORDS(nvals);
+ }
+ ASSERT_EQ(end, pos);
+ ASSERT_GT(nargs, 0);
+ }
+}
+
+TEST_HARNESS_MAIN

--
2.47.3

Reply all
Reply to author
Forward
0 new messages