[PATCH RFC v3 2/8] Documentation: add kcov-dataflow.rst

0 views
Skip to first unread message

Yunseong Kim

unread,
Sep 2, 2026, 12:30:04 PM (8 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
Document the kcov_dataflow subsystem under Documentation/dev-tools/:

- Prerequisites and the Kconfig options (KCOV_DATAFLOW_ARGS / _RET,
NO_INLINE, INSTRUMENT_ALL) and the compiler they require.
- Per-file (KCOV_DATAFLOW_<obj>.o := y) and whole-kernel instrumentation.
- A worked collection example: open /sys/kernel/debug/kcov_dataflow,
KCOV_DF_INIT_TRACK, mmap, KCOV_DF_ENABLE, run the workload, and walk the
buffer.
- The record layout: area[0] as the record-word count, the header word
fields (sequence, type, value count, size, argument index), the PC with
the KASLR offset removed, the traced pointer or comparison type, and the
value words.
- Safety properties and the ioctl interface reference.
- Coexistence with KCOV, Rust module support, and the fork/child
tracing pattern.

Add the ioctl 'd' numbers (KCOV_DF_INIT_TRACK and 100-103) to
Documentation/userspace-api/ioctl/ioctl-number.rst, link the new
file from the dev-tools index, and add the MAINTAINERS entries for
kernel/kcov_dataflow.c and include/uapi/linux/kcov_dataflow.h.

Signed-off-by: Yunseong Kim <yunseo...@est.tech>
---
Documentation/dev-tools/index.rst | 1 +
Documentation/dev-tools/kcov-dataflow.rst | 449 +++++++++++++++++++++
Documentation/userspace-api/ioctl/ioctl-number.rst | 2 +
MAINTAINERS | 2 +
4 files changed, 454 insertions(+)

diff --git a/Documentation/dev-tools/index.rst b/Documentation/dev-tools/index.rst
index 59cbb77b33ff4..541c58cc65ea5 100644
--- a/Documentation/dev-tools/index.rst
+++ b/Documentation/dev-tools/index.rst
@@ -24,6 +24,7 @@ Documentation/process/debugging/index.rst
context-analysis
sparse
kcov
+ kcov-dataflow
gcov
kasan
kmsan
diff --git a/Documentation/dev-tools/kcov-dataflow.rst b/Documentation/dev-tools/kcov-dataflow.rst
new file mode 100644
index 0000000000000..4c023032fea00
--- /dev/null
+++ b/Documentation/dev-tools/kcov-dataflow.rst
@@ -0,0 +1,449 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+KCOV-Dataflow: function argument and return value extraction
+=============================================================
+
+KCOV-Dataflow captures function arguments and return values, including
+automatic struct field decomposition, at instrumented kernel function
+boundaries. It provides per-task, lock-free ring buffers accessible via
+``mmap()``, enabling data-flow-aware fuzzing and post-mortem contract
+verification.
+
+Unlike KCOV's ``trace-pc`` which reports *which* code executed,
+KCOV-Dataflow reports *what values* were passed and returned. This is
+a completely separate device from ``/sys/kernel/debug/kcov``.
+
+Prerequisites
+-------------
+
+KCOV-Dataflow requires Clang/LLVM with the ``trace-args`` and
+``trace-ret`` SanitizerCoverage extensions. Standard (unpatched)
+compilers will not expose these Kconfig options.
+
+To enable KCOV-Dataflow, configure the kernel with::
+
+ CONFIG_KCOV=y
+ CONFIG_KCOV_DATAFLOW_ARGS=y
+ CONFIG_KCOV_DATAFLOW_RET=y
+
+Optional: instrument the entire kernel (significant overhead)::
+
+ CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL=y
+
+Coverage data becomes accessible once debugfs is mounted::
+
+ mount -t debugfs none /sys/kernel/debug
+
+Per-module instrumentation
+--------------------------
+
+To instrument a specific module, add to its Makefile::
+
+ KCOV_DATAFLOW_my_module.o := y
+
+For example, to instrument the Android binder driver::
+
+ # drivers/android/Makefile
+ KCOV_DATAFLOW_binder.o := y
+ KCOV_DATAFLOW_binder_alloc.o := y
+
+To instrument an entire directory, set the variable without a filename::
+
+ # fs/Makefile
+ KCOV_DATAFLOW := y
+
+The build system automatically adds the required compiler flags
+(``-fsanitize-coverage=trace-args,trace-ret``). Debug info is provided
+by ``CONFIG_DEBUG_INFO`` which is a Kconfig dependency.
+
+Data collection
+---------------
+
+The following program demonstrates how to collect function argument and
+return value data for a single syscall:
+
+.. code-block:: c
+
+ #include <stdio.h>
+ #include <stdint.h>
+ #include <stdlib.h>
+ #include <sys/types.h>
+ #include <sys/ioctl.h>
+ #include <sys/mman.h>
+ #include <unistd.h>
+ #include <fcntl.h>
+
+ #include <linux/kcov_dataflow.h> /* ioctls, record layout, helpers */
+ #define BUF_SIZE (1 << 20) /* 1M words = 8MB */
+
+ int main(void)
+ {
+ int fd;
+ uint64_t *buf, n, i;
+
+ fd = open("/sys/kernel/debug/kcov_dataflow", O_RDWR);
+ if (fd == -1)
+ perror("open"), exit(1);
+
+ /* Allocate buffer (size in u64 words). */
+ if (ioctl(fd, KCOV_DF_INIT_TRACK, BUF_SIZE))
+ perror("ioctl(INIT)"), exit(1);
+
+ /* Map the buffer into user space. */
+ buf = (uint64_t *)mmap(NULL, BUF_SIZE * sizeof(uint64_t),
+ PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ if (buf == MAP_FAILED)
+ perror("mmap"), exit(1);
+
+ /* Enable data-flow collection for this task. */
+ if (ioctl(fd, KCOV_DF_ENABLE, 0))
+ perror("ioctl(ENABLE)"), exit(1);
+
+ /* Reset counter. */
+ __atomic_store_n(&buf[0], 0, __ATOMIC_RELAXED);
+
+ /* === Trigger syscall(s) here === */
+ read(-1, NULL, 0);
+
+ /* Read how many words were written. */
+ n = __atomic_load_n(&buf[0], __ATOMIC_RELAXED);
+
+ /* Parse TLV records. */
+ i = 1;
+ while (i + KCOV_DF_RECORD_HDR_WORDS <= 1 + n) {
+ uint64_t hdr = buf[i];
+ uint64_t pc = buf[i + 1]; /* KASLR offset removed */
+ uint64_t ptr = buf[i + 2]; /* traced pointer (ENTRY/RET) */
+ uint32_t type = KCOV_DF_HDR_TYPE(hdr);
+ uint32_t num_vals = KCOV_DF_HDR_NVALS(hdr);
+ uint32_t seq = KCOV_DF_HDR_SEQ(hdr);
+ uint32_t arg_idx = KCOV_DF_HDR_ARGIDX(hdr);
+ uint32_t size = KCOV_DF_HDR_SIZE(hdr);
+
+ if (!num_vals || (type != KCOV_DF_TYPE_ENTRY &&
+ type != KCOV_DF_TYPE_RET &&
+ type != KCOV_DF_TYPE_CMP)) {
+ i++; /* garbage (e.g. reset mid-run): resync */
+ continue;
+ }
+ if (type != KCOV_DF_TYPE_CMP)
+ printf("[%s] seq=%u pc=0x%lx ptr=0x%lx arg_idx=%u size=%u val=0x%lx\n",
+ type == KCOV_DF_TYPE_ENTRY ? "ENTRY" : "RET",
+ seq, pc, ptr, arg_idx, size, buf[i + 3]);
+ i += KCOV_DF_RECORD_WORDS(num_vals);
+ }
+
+ if (ioctl(fd, KCOV_DF_DISABLE, 0))
+ perror("ioctl(DISABLE)"), exit(1);
+
+ munmap(buf, BUF_SIZE * sizeof(uint64_t));
+ close(fd);
+ return 0;
+ }
+
+Ring buffer format
+------------------
+
+The buffer is an array of ``u64`` words::
+
+ buf[0]: atomic counter -- total words written
+
+Each record occupies 3 + N words:
+
+.. list-table::
+ :header-rows: 1
+
+ * - Offset
+ - Field
+ - Description
+ * - 0
+ - header
+ - bits[63:56] = arg_idx (0 for return), bits[55:48] = size in bytes
+ (clamped to 255), bits[47:32] = num_vals (>= 1),
+ bits[31:28] = type: ``KCOV_DF_TYPE_ENTRY`` (0xE),
+ ``KCOV_DF_TYPE_RET`` (0xF) or ``KCOV_DF_TYPE_CMP`` (0xC),
+ bits[23:0] = sequence number
+ * - 1
+ - pc
+ - Instrumented function address with the KASLR offset removed (same
+ as the PCs mainline kcov records), so it can be symbolized against
+ vmlinux; add the runtime offset back for ``/proc/kallsyms``
+ * - 2
+ - ptr / cmp_type
+ - ENTRY/RET: the full 64-bit traced pointer (may be NULL/ERR_PTR, in
+ which case the values are ``0xBADADD85``). CMP: the comparison
+ type, ``KCOV_CMP_SIZE()``/``KCOV_CMP_CONST`` bits from linux/kcov.h
+ * - 3..3+num_vals
+ - values
+ - Struct field values, a single scalar, or the two CMP operands
+
+``area[0]`` never exceeds the buffer size minus one and every counted word
+has been written, so a consumer that walks ``area[0]`` words never leaves
+its mapping. All of the above is defined in ``include/uapi/linux/kcov_dataflow.h``
+(``KCOV_DF_HDR_*()``, ``KCOV_DF_RECORD_WORDS()``).
+
+Magic values:
+
+- ``0xBADADD85``: field read failed (pointer was invalid/freed/poisoned)
+
+Safety
+------
+
+- Callbacks are ``notrace``, ``__no_sanitize_coverage``, ``noinline``
+ to prevent recursion.
+- All pointer reads use ``copy_from_kernel_nofault()`` -- survives
+ freed, poisoned, or unmapped memory.
+- An ``in_task()`` guard rejects calls from hardirq/softirq/NMI context,
+ preventing reentrant buffer corruption.
+- No ``printk`` or allocation in the data path.
+- When not enabled for a task, overhead is a single boolean check.
+
+Ioctl interface
+---------------
+
+.. list-table::
+ :header-rows: 1
+
+ * - Command
+ - Value
+ - Description
+ * - KCOV_DF_INIT_TRACK
+ - ``_IOR('d', 1, unsigned long)``
+ - Allocate buffer (size in u64 words)
+ * - KCOV_DF_ENABLE
+ - ``_IO('d', 100)``
+ - Start collection for current task
+ * - KCOV_DF_DISABLE
+ - ``_IO('d', 101)``
+ - Stop collection
+ * - KCOV_DF_REMOTE_ENABLE
+ - ``_IOW('d', 102, __u64)`` -- argument is a pointer to the handle
+ - Publish buffer for kworker/kthread remote capture
+ * - KCOV_DF_REMOTE_DISABLE
+ - ``_IO('d', 103)``
+ - Unpublish buffer from remote capture
+
+Compatibility
+-------------
+
+KCOV-Dataflow is completely independent from legacy KCOV:
+
+- Separate device: ``/sys/kernel/debug/kcov_dataflow``
+- Separate ioctl namespace (``'d'`` vs ``'c'``)
+- Separate per-task buffer
+- Both can be used simultaneously without interference
+- syzkaller and other KCOV users are unaffected
+
+Rust module support
+-------------------
+
+Rust kernel modules are instrumented natively through the build system.
+The ``KCOV_DATAFLOW_<module>.o := y`` mechanism works identically for
+Rust and C modules. The build system passes
+``-Cllvm-args=-sanitizer-coverage-trace-args`` and
+``-Cllvm-args=-sanitizer-coverage-trace-ret`` to rustc via
+``RUSTFLAGS_KCOV_DATAFLOW``.
+
+Example Makefile for a Rust module::
+
+ obj-m := my_rust_module.o
+ KCOV_DATAFLOW_my_rust_module.o := y
+
+Requires a rustc built against LLVM with trace-args/trace-ret support
+and ``CONFIG_RUST=y`` in the kernel config.
+
+Selftests
+---------
+
+Automated tests and visualization tools are in
+``tools/testing/selftests/kcov_dataflow/``::
+
+ # Automated ioctl interface test (TAP output):
+ make -C tools/testing/selftests/kcov_dataflow
+ vng --user root --exec \
+ tools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl
+
+ # Load a test module and view captured records:
+ make LLVM=1 CC=clang M=tools/testing/selftests/kcov_dataflow/eight_struct_args_c modules
+ vng --user root --exec \
+ "python3 tools/testing/selftests/kcov_dataflow/trigger-view.py \
+ eight_struct_args_c --ko \
+ tools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.ko"
+
+ # Binderfs ioctl capture test (requires CONFIG_ANDROID_BINDER_IPC):
+ make -C tools/testing/selftests/kcov_dataflow/binderfs
+ vng --user root --exec \
+ tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test
+
+See ``tools/testing/selftests/kcov_dataflow/README.rst`` for details.
+
+Tracing child processes
+-----------------------
+
+KCOV-Dataflow is per-task: after ``fork()``, the child does not inherit
+the enabled state. To trace child processes, re-enable on the inherited
+file descriptor in the child before ``exec()``. The ``mmap``'d buffer is
+shared (``MAP_SHARED``), so both parent and child write to the same ring
+buffer atomically.
+
+.. code-block:: c
+
+ #include <stdio.h>
+ #include <stdint.h>
+ #include <stdlib.h>
+ #include <sys/ioctl.h>
+ #include <sys/mman.h>
+ #include <sys/wait.h>
+ #include <unistd.h>
+ #include <fcntl.h>
+
+ #include <linux/kcov_dataflow.h> /* ioctls, record layout, helpers */
+ #define BUF_SIZE (1 << 20)
+
+ int main(int argc, char **argv)
+ {
+ int fd = open("/sys/kernel/debug/kcov_dataflow", O_RDWR);
+ ioctl(fd, KCOV_DF_INIT_TRACK, BUF_SIZE);
+ uint64_t *buf = mmap(NULL, BUF_SIZE * 8,
+ PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+
+ /* Enable for parent task. */
+ ioctl(fd, KCOV_DF_ENABLE, 0);
+ __atomic_store_n(&buf[0], 0, __ATOMIC_RELAXED);
+
+ pid_t pid = fork();
+ if (pid == 0) {
+ /*
+ * Child: re-enable on inherited fd.
+ * The shared mmap buffer receives records from both tasks.
+ */
+ ioctl(fd, KCOV_DF_ENABLE, 0);
+ execvp(argv[1], &argv[1]);
+ _exit(1);
+ }
+
+ waitpid(pid, NULL, 0);
+ ioctl(fd, KCOV_DF_DISABLE, 0);
+
+ uint64_t n = __atomic_load_n(&buf[0], __ATOMIC_RELAXED);
+ printf("Captured %lu words from parent + child\n", n);
+
+ munmap(buf, BUF_SIZE * 8);
+ close(fd);
+ return 0;
+ }
+
+Note: the child's ``ioctl(fd, KCOV_DF_ENABLE)`` will fail if the parent
+has not yet called ``KCOV_DF_DISABLE``, because only one task can be
+associated with a descriptor at a time. For true multi-process tracing,
+open a separate ``kcov_dataflow`` fd per child, or disable in the parent
+before the child enables (as shown above -- the parent is blocked in
+``waitpid`` so it generates no records during that time anyway).
+
+Remote tracing (kworker/kthread)
+--------------------------------
+
+To capture data from kernel threads (kworkers, kthreads) that are not
+direct descendants of user space, use the remote API:
+
+1. User space allocates and publishes a buffer with ``KCOV_DF_REMOTE_ENABLE``
+2. The kernel module calls ``kcov_df_remote_start()`` at work entry
+3. The kernel module calls ``kcov_df_remote_stop()`` at work exit
+4. User space reads the buffer and unpublishes with ``KCOV_DF_REMOTE_DISABLE``
+
+User space setup:
+
+.. code-block:: c
+
+ #include <stdio.h>
+ #include <stdint.h>
+ #include <sys/ioctl.h>
+ #include <sys/mman.h>
+ #include <unistd.h>
+ #include <fcntl.h>
+
+ #include <linux/kcov.h> /* kcov_remote_handle() */
+ #include <linux/kcov_dataflow.h>
+ #define BUF_SIZE (1 << 20)
+
+ int main(void)
+ {
+ int fd = open("/sys/kernel/debug/kcov_dataflow", O_RDWR);
+ ioctl(fd, KCOV_DF_INIT_TRACK, BUF_SIZE);
+ uint64_t *buf = mmap(NULL, BUF_SIZE * 8,
+ PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ __atomic_store_n(&buf[0], 0, __ATOMIC_RELAXED);
+
+ /*
+ * Publish the buffer under a remote handle. The handle must be a
+ * valid kcov_remote_handle() encoding (KCOV_SUBSYSTEM_COMMON with a
+ * nonzero instance, or KCOV_SUBSYSTEM_USB) and is the value the
+ * kernel side passes to kcov_df_remote_start(); one handle per fd,
+ * and not while KCOV_DF_ENABLE is active on the same fd.
+ */
+ __u64 handle = kcov_remote_handle(KCOV_SUBSYSTEM_COMMON, 1);
+ if (ioctl(fd, KCOV_DF_REMOTE_ENABLE, &handle))
+ perror("ioctl(REMOTE_ENABLE)"), exit(1);
+
+ /* Trigger kworker activity (e.g., write to a file, ioctl). */
+ /* ... */
+ sleep(1);
+
+ /* Unpublish and read results. */
+ ioctl(fd, KCOV_DF_REMOTE_DISABLE, 0);
+
+ uint64_t n = __atomic_load_n(&buf[0], __ATOMIC_RELAXED);
+ printf("Captured %lu words from kworker\n", n);
+
+ munmap(buf, BUF_SIZE * 8);
+ close(fd);
+ return 0;
+ }
+
+Kernel module side (called from kworker context):
+
+.. code-block:: c
+
+ #include <linux/kcov.h>
+
+ void my_work_fn(struct work_struct *work)
+ {
+ kcov_df_remote_start();
+ /* ... instrumented code runs here ... */
+ kcov_df_remote_stop();
+ }
+
+Only one buffer can be published at a time. ``kcov_df_remote_start()``
+is a no-op if no buffer is published or if the current task already has
+dataflow enabled.
+
+Limitations
+-----------
+
+ABI argument mapping
+ The LLVM pass maps IR-level arguments to source-level parameters using
+ ``DILocalVariable`` debug records (``-g`` required). This correctly
+ handles hidden ``sret`` pointers, struct decomposition into multiple
+ registers, and C++ ``this`` pointers.
+
+ When debug info is absent or stripped, the pass falls back to positional
+ indexing which may misattribute arguments in functions with ABI-inserted
+ hidden parameters. The kernel is always built with ``-g``, so this
+ limitation does not apply to kernel use.
+
+Struct-by-value reassembly
+ When a small struct is passed by value and the ABI decomposes it into
+ multiple scalar registers (e.g., ``struct { int x; int y; }`` as two
+ ``i32`` values on x86_64), the pass reassembles the fragments into a
+ stack slot. The struct field offsets are preserved, but if a field was
+ entirely optimized away (no debug record), that slot contains zero.
+
+ In kernel code, structs are always passed by pointer, so this case
+ does not arise.
+
+Optimized builds
+ At ``-O2`` and above, LLVM may eliminate ``#dbg_value`` records for
+ arguments that are dead or fully inlined. Such arguments will emit a
+ trace with a null pointer (producing ``0xBADADD85`` in all field
+ positions), indicating the argument existed but its value was
+ unavailable at runtime.
diff --git a/Documentation/userspace-api/ioctl/ioctl-number.rst b/Documentation/userspace-api/ioctl/ioctl-number.rst
index 2fc53093752d1..7864b2e7fb476 100644
--- a/Documentation/userspace-api/ioctl/ioctl-number.rst
+++ b/Documentation/userspace-api/ioctl/ioctl-number.rst
@@ -240,6 +240,8 @@ Code Seq# Include File Comments
'd' 00-FF linux/char/drm/drm.h conflict!
'd' 02-40 pcmcia/ds.h conflict!
'd' F0-FF linux/digi1.h
+'d' 01 uapi/linux/kcov_dataflow.h conflict!
+'d' 64-67 uapi/linux/kcov_dataflow.h conflict!
'e' all linux/digi1.h conflict!
'f' 00-1F linux/ext2_fs.h conflict!
'f' 00-1F linux/ext3_fs.h conflict!
diff --git a/MAINTAINERS b/MAINTAINERS
index b91655b34f0ef..d79e04b108c1c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14092,7 +14092,9 @@ B: https://bugzilla.kernel.org/buglist.cgi?component=Sanitizers&product=Memory%2
F: Documentation/dev-tools/kcov.rst
F: include/linux/kcov.h
F: include/uapi/linux/kcov.h
+F: include/uapi/linux/kcov_dataflow.h
F: kernel/kcov.c
+F: kernel/kcov_dataflow.c
F: scripts/Makefile.kcov

KCSAN

--
2.47.3

Reply all
Reply to author
Forward
0 new messages