[PATCH RFC v3 2/8] Documentation: add kcov-dataflow.rst
From: Yunseong Kim
Date: Wed Sep 02 2026 - 15:19:19 EST
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 <yunseong.kim@xxxxxxxx>
---
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