[RFC PATCH v2 6/6] kcov: add recursion guard and documentation for kcov-dataflow

From: Yunseong Kim

Date: Wed Jun 03 2026 - 13:48:28 EST


Add a per-task recursion guard to kcov_df_write() using the high bit of
kcov_dataflow_seq. This prevents infinite recursion when
CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL is enabled: functions called by the
callback itself (copy_from_kernel_nofault, xadd helpers) are also
instrumented and would re-enter kcov_df_write() without this guard.

The guard uses the sequence counter's bit 31 as a re-entrancy flag.
The low 24 bits (used for TLV record sequence numbers) are unaffected.

Also:
- Exclude kcov.o, extable.o, softirq.o from dataflow instrumentation
(same pattern as KCOV_INSTRUMENT exclusions)
- Add Documentation/dev-tools/kcov-dataflow.rst with:
- Prerequisites and Kconfig options
- Per-module instrumentation instructions
- Complete C example for data collection
- Ring buffer format specification
- Ioctl interface reference
- Fork interception example for child process tracing
- Rust module support via post-compilation pipeline

Signed-off-by: Yunseong Kim <yunseong.kim@xxxxxxxx>
---
Documentation/dev-tools/kcov-dataflow.rst | 282 ++++++++++++++++++++++++++++++
kernel/Makefile | 3 +
kernel/kcov.c | 14 +-
3 files changed, 298 insertions(+), 1 deletion(-)

diff --git a/Documentation/dev-tools/kcov-dataflow.rst b/Documentation/dev-tools/kcov-dataflow.rst
new file mode 100644
index 000000000000..5941df9f29e6
--- /dev/null
+++ b/Documentation/dev-tools/kcov-dataflow.rst
@@ -0,0 +1,282 @@
+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 ``dataflow-args`` and
+``dataflow-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
+
+For Rust modules, add to the crate's Makefile::
+
+ # drivers/android/binder/Makefile
+ KCOV_DATAFLOW := 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=dataflow-args,dataflow-ret -g``).
+
+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>
+
+ #define KCOV_DF_INIT_TRACE _IOR('d', 1, unsigned long)
+ #define KCOV_DF_ENABLE _IO('d', 100)
+ #define KCOV_DF_DISABLE _IO('d', 101)
+ #define BUF_SIZE (64 << 10) /* 64K words = 512KB */
+
+ 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_TRACE, 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 < n) {
+ uint64_t type_seq = buf[i];
+ uint64_t pc = buf[i + 1];
+ uint64_t meta = buf[i + 2];
+ uint32_t type = (type_seq >> 28) & 0xF;
+ uint32_t seq = type_seq & 0x00FFFFFF;
+ uint32_t arg_idx = (meta >> 56) & 0xFF;
+ uint32_t size = (meta >> 48) & 0xFF;
+
+ printf("[%s] seq=%u pc=0x%lx arg_idx=%u size=%u val=0x%lx\n",
+ type == 0xE ? "ENTRY" : "RET",
+ seq, pc, arg_idx, size, buf[i + 3]);
+ i += 4; /* minimum record size: 3 header + 1 value */
+ }
+
+ 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:
+
++--------+------------------+---------------------------------------------+
+| Offset | Field | Description |
++========+==================+=============================================+
+| 0 | type_and_seq | bits[31:28] = 0xE (entry) or 0xF (return), |
+| | | bits[23:0] = per-task sequence number |
++--------+------------------+---------------------------------------------+
+| 1 | pc | Instrumented function address |
++--------+------------------+---------------------------------------------+
+| 2 | meta | bits[63:56] = arg_idx (0 for return), |
+| | | bits[55:48] = size in bytes, |
+| | | bits[47:0] = raw pointer value |
++--------+------------------+---------------------------------------------+
+| 3..N | field_val[0..N] | Struct field values or single scalar |
++--------+------------------+---------------------------------------------+
+
+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
+---------------
+
++---------------------+----------------------------+---------------------------+
+| Command | Value | Description |
++=====================+============================+===========================+
+| KCOV_DF_INIT_TRACE | ``_IOR('d', 1, unsigned | Allocate buffer |
+| | long)`` | (size in u64 words) |
++---------------------+----------------------------+---------------------------+
+| KCOV_DF_ENABLE | ``_IO('d', 100)`` | Start collection for |
+| | | current task |
++---------------------+----------------------------+---------------------------+
+| KCOV_DF_DISABLE | ``_IO('d', 101)`` | Stop collection |
++---------------------+----------------------------+---------------------------+
+
+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 supported via a post-compilation pipeline::
+
+ rustc --emit=llvm-ir -g module.rs
+ opt -passes=sancov-module \
+ -sanitizer-coverage-dataflow-args \
+ -sanitizer-coverage-dataflow-ret module.ll -S -o module_inst.ll
+ llc -filetype=obj module_inst.ll -o module.o
+
+This is the good method for capturing Rust function arguments at runtime.
+
+
+Tracing child processes (fork interception)
+-------------------------------------------
+
+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>
+
+ #define KCOV_DF_INIT_TRACE _IOR('d', 1, unsigned long)
+ #define KCOV_DF_ENABLE _IO('d', 100)
+ #define KCOV_DF_DISABLE _IO('d', 101)
+ #define BUF_SIZE (64 << 10)
+
+ int main(int argc, char **argv)
+ {
+ int fd = open("/sys/kernel/debug/kcov_dataflow", O_RDWR);
+ ioctl(fd, KCOV_DF_INIT_TRACE, 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).
diff --git a/kernel/Makefile b/kernel/Makefile
index 1e1a31673577..9c56421c5390 100644
--- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -37,6 +37,7 @@ KCOV_INSTRUMENT_extable.o := n
KCOV_INSTRUMENT_stacktrace.o := n
# Don't self-instrument.
KCOV_INSTRUMENT_kcov.o := n
+KCOV_DATAFLOW_kcov.o := n
# If sanitizers detect any issues in kcov, it may lead to recursion
# via printk, etc.
KASAN_SANITIZE_kcov.o := n
@@ -207,3 +208,5 @@ $(obj)/kheaders.md5: $(obj)/kheaders-srclist FORCE
$(call filechk,kheaders_md5sum)

clean-files := kheaders.md5 kheaders-srclist kheaders-objlist
+KCOV_DATAFLOW_extable.o := n
+KCOV_DATAFLOW_softirq.o := n
diff --git a/kernel/kcov.c b/kernel/kcov.c
index 373b8034ca5c..8d9d5e33549f 100644
--- a/kernel/kcov.c
+++ b/kernel/kcov.c
@@ -413,6 +413,16 @@ kcov_df_write(u64 type_marker, u64 pc, u64 meta, void *ptr,
if (!in_task())
return;

+ /*
+ * Prevent recursion: functions called by this callback
+ * (copy_from_kernel_nofault, xadd helpers) may be instrumented
+ * with INSTRUMENT_ALL. Use a per-task guard via the sequence
+ * counter's high bit.
+ */
+ if (t->kcov_dataflow_seq & (1U << 31))
+ return;
+ t->kcov_dataflow_seq |= (1U << 31);
+
area = (u64 *)t->kcov_df_area;
if (!area)
return;
@@ -449,7 +459,7 @@ kcov_df_write(u64 type_marker, u64 pc, u64 meta, void *ptr,
if (KCOV_DF_IS_ERR(ptr)) {
for (i = 0; i < num_fields; i++)
area[pos + 3 + i] = KCOV_DF_MAGIC_BAD;
- return;
+ goto out;
}
for (i = 0; i < num_fields; i++) {
u64 off, sz, val = KCOV_DF_MAGIC_BAD;
@@ -469,6 +479,8 @@ kcov_df_write(u64 type_marker, u64 pc, u64 meta, void *ptr,
area[pos + 3 + i] = val;
}
}
+out:
+ t->kcov_dataflow_seq &= ~(1U << 31);
}

#ifdef CONFIG_KCOV_DATAFLOW_ARGS

--
2.43.0